Moved to error reporting framework.

This commit is contained in:
lwark
2024-09-12 12:03:02 -05:00
parent d52fa36150
commit 744394f236
10 changed files with 109 additions and 48 deletions

View File

@@ -1,6 +1,7 @@
## 202409.03 ## 202409.03
- Improved error messaging when saving submissions. - Upgraded sample search to (semi) realtime search.
- Improved error messaging.
## 202409.02 ## 202409.02

View File

@@ -19,7 +19,7 @@ def get_week_of_month() -> int:
__project__ = "submissions" __project__ = "submissions"
__version__ = f"{year}{str(month).zfill(2)}.{get_week_of_month()}b" __version__ = f"{year}{str(month).zfill(2)}.{get_week_of_month()}b"
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"} __author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
__copyright__ = f"2022-{date.today().year}, Government of Canada" __copyright__ = f"2022-{year}, Government of Canada"
__github__ = "https://github.com/landowark/submissions" __github__ = "https://github.com/landowark/submissions"
project_path = Path(__file__).parents[2].absolute() project_path = Path(__file__).parents[2].absolute()

View File

@@ -167,20 +167,20 @@ class BaseClass(Base):
except Exception as e: except Exception as e:
logger.critical(f"Problem saving object: {e}") logger.critical(f"Problem saving object: {e}")
logger.error(f"Error message: {type(e)}") logger.error(f"Error message: {type(e)}")
match e: # match e:
case sqlalcIntegrityError(): # case sqlalcIntegrityError():
origin = e.orig.__str__().lower() # origin = e.orig.__str__().lower()
logger.debug(f"Exception origin: {origin}") # logger.error(f"Exception origin: {origin}")
if "unique constraint failed:" in origin: # if "unique constraint failed:" in origin:
field = origin.split(".")[1].replace("_", " ").upper() # field = " ".join(origin.split(".")[1:]).replace("_", " ").upper()
logger.debug(field) # # logger.debug(field)
msg = f"{field} doesn't have a unique value.\nIt must be changed." # msg = f"{field} doesn't have a unique value.\nIt must be changed."
else: # else:
msg = f"Got unknown integrity error: {e}" # msg = f"Got unknown integrity error: {e}"
case _: # case _:
msg = f"Got generic error: {e}" # msg = f"Got generic error: {e}"
self.__database_session__.rollback() self.__database_session__.rollback()
report.add_result(Result(msg=msg, status="Critical")) report.add_result(Result(msg=e, status="Critical"))
return report return report

View File

@@ -9,7 +9,7 @@ from operator import itemgetter
from . import BaseClass from . import BaseClass
from tools import setup_lookup from tools import setup_lookup
from datetime import date, datetime from datetime import date, datetime
from typing import List from typing import List, Literal
from dateutil.parser import parse from dateutil.parser import parse
from re import Pattern from re import Pattern
@@ -53,7 +53,7 @@ class ControlType(BaseClass):
pass pass
return cls.execute_query(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
def get_subtypes(self, mode: str) -> List[str]: def get_subtypes(self, mode: Literal['kraken', 'matches', 'contains']) -> List[str]:
""" """
Get subtypes associated with this controltype (currently used only for Kraken) Get subtypes associated with this controltype (currently used only for Kraken)
@@ -161,7 +161,7 @@ class Control(BaseClass):
} }
return output return output
def convert_by_mode(self, mode: str) -> list[dict]: def convert_by_mode(self, mode: Literal['kraken', 'matches', 'contains']) -> list[dict]:
""" """
split this instance into analysis types for controls graphs split this instance into analysis types for controls graphs
@@ -195,7 +195,7 @@ class Control(BaseClass):
output.append(_dict) output.append(_dict)
# logger.debug("Have to triage kraken data to keep program from getting overwhelmed") # logger.debug("Have to triage kraken data to keep program from getting overwhelmed")
if "kraken" in mode: if "kraken" in mode:
output = sorted(output, key=lambda d: d[f"{mode}_count"], reverse=True)[:49] output = sorted(output, key=lambda d: d[f"{mode}_count"], reverse=True)[:50]
return output return output
@classmethod @classmethod

View File

@@ -7,6 +7,7 @@ import datetime
import json import json
from pprint import pprint, pformat from pprint import pprint, pformat
import yaml
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB
from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.orm import relationship, validates, Query
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
@@ -873,7 +874,12 @@ class SubmissionType(BaseClass):
logging.critical(f"Given file could not be found.") logging.critical(f"Given file could not be found.")
return None return None
with open(filepath, "r") as f: with open(filepath, "r") as f:
if filepath.suffix == ".json":
import_dict = json.load(fp=f) import_dict = json.load(fp=f)
elif filepath.suffix == ".yml":
import_dict = yaml.safe_load(stream=f)
else:
raise Exception(f"Filetype {filepath.suffix} not supported.")
logger.debug(pformat(import_dict)) logger.debug(pformat(import_dict))
submission_type = cls.query(name=import_dict['name']) submission_type = cls.query(name=import_dict['name'])
if submission_type: if submission_type:

View File

@@ -2425,15 +2425,18 @@ class BasicSample(BaseClass):
case _: case _:
model = cls.find_polymorphic_subclass(attrs=kwargs) model = cls.find_polymorphic_subclass(attrs=kwargs)
# logger.debug(f"Length of kwargs: {len(kwargs)}") # logger.debug(f"Length of kwargs: {len(kwargs)}")
# logger.debug(f"Fuzzy search received sample type: {sample_type}")
query: Query = cls.__database_session__.query(model) query: Query = cls.__database_session__.query(model)
# logger.debug(f"Queried model. Now running searches in {kwargs}")
for k, v in kwargs.items(): for k, v in kwargs.items():
# logger.debug(f"Running fuzzy search for attribute: {k} with value {v}")
search = f"%{v}%" search = f"%{v}%"
try: try:
attr = getattr(model, k) attr = getattr(model, k)
query = query.filter(attr.like(search)) query = query.filter(attr.like(search))
except (ArgumentError, AttributeError) as e: except (ArgumentError, AttributeError) as e:
logger.error(f"Attribute {k} unavailable due to:\n\t{e}\nSkipping.") logger.error(f"Attribute {k} unavailable due to:\n\t{e}\nSkipping.")
return query.all() return query.limit(50).all()
def delete(self): def delete(self):
raise AttributeError(f"Delete not implemented for {self.__class__}") raise AttributeError(f"Delete not implemented for {self.__class__}")

View File

@@ -24,7 +24,7 @@ class SearchBox(QDialog):
self.sample_type = QComboBox(self) self.sample_type = QComboBox(self)
self.sample_type.setObjectName("sample_type") self.sample_type.setObjectName("sample_type")
self.sample_type.currentTextChanged.connect(self.update_widgets) self.sample_type.currentTextChanged.connect(self.update_widgets)
options = [cls.__mapper_args__['polymorphic_identity'] for cls in BasicSample.__subclasses__()] options = ["Any"] + [cls.__mapper_args__['polymorphic_identity'] for cls in BasicSample.__subclasses__()]
self.sample_type.addItems(options) self.sample_type.addItems(options)
self.sample_type.setEditable(False) self.sample_type.setEditable(False)
self.setMinimumSize(600, 600) self.setMinimumSize(600, 600)
@@ -34,6 +34,7 @@ class SearchBox(QDialog):
self.layout.addWidget(self.results, 5, 0) self.layout.addWidget(self.results, 5, 0)
self.setLayout(self.layout) self.setLayout(self.layout)
self.update_widgets() self.update_widgets()
self.update_data()
def update_widgets(self): def update_widgets(self):
""" """
@@ -43,6 +44,9 @@ class SearchBox(QDialog):
# logger.debug(deletes) # logger.debug(deletes)
for item in deletes: for item in deletes:
item.setParent(None) item.setParent(None)
if self.sample_type.currentText() == "Any":
self.type = BasicSample
else:
self.type = BasicSample.find_polymorphic_subclass(self.sample_type.currentText()) self.type = BasicSample.find_polymorphic_subclass(self.sample_type.currentText())
# logger.debug(f"Sample type: {self.type}") # logger.debug(f"Sample type: {self.type}")
searchables = self.type.get_searchables() searchables = self.type.get_searchables()
@@ -50,6 +54,7 @@ class SearchBox(QDialog):
for iii, item in enumerate(searchables): for iii, item in enumerate(searchables):
widget = FieldSearch(parent=self, label=item['label'], field_name=item['field']) widget = FieldSearch(parent=self, label=item['label'], field_name=item['field'])
self.layout.addWidget(widget, start_row+iii, 0) self.layout.addWidget(widget, start_row+iii, 0)
widget.search_widget.textChanged.connect(self.update_data)
def parse_form(self) -> dict: def parse_form(self) -> dict:
""" """
@@ -65,9 +70,11 @@ class SearchBox(QDialog):
""" """
Shows dataframe of relevant samples. Shows dataframe of relevant samples.
""" """
# logger.debug(f"Running update_data with sample type: {self.type}")
fields = self.parse_form() fields = self.parse_form()
data = self.type.fuzzy_search(sample_type=self.type, **fields) # logger.debug(f"Got fields: {fields}")
data = self.type.samples_to_df(sample_list=data) sample_list_creator = self.type.fuzzy_search(sample_type=self.type, **fields)
data = self.type.samples_to_df(sample_list=sample_list_creator)
# logger.debug(f"Data: {data}") # logger.debug(f"Data: {data}")
self.results.setData(df=data) self.results.setData(df=data)

View File

@@ -25,6 +25,7 @@ from __init__ import project_path
from configparser import ConfigParser from configparser import ConfigParser
from tkinter import Tk # from tkinter import Tk for Python 3.x from tkinter import Tk # from tkinter import Tk for Python 3.x
from tkinter.filedialog import askdirectory from tkinter.filedialog import askdirectory
from .error_messaging import parse_error_to_message
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -765,10 +766,10 @@ def setup_lookup(func):
return wrapper return wrapper
class Result(BaseModel): class Result(BaseModel, arbitrary_types_allowed=True):
owner: str = Field(default="", validate_default=True) owner: str = Field(default="", validate_default=True)
code: int = Field(default=0) code: int = Field(default=0)
msg: str msg: str | Exception
status: Literal["NoIcon", "Question", "Information", "Warning", "Critical"] = Field(default="NoIcon") status: Literal["NoIcon", "Question", "Information", "Warning", "Critical"] = Field(default="NoIcon")
@field_validator('status', mode='before') @field_validator('status', mode='before')
@@ -779,6 +780,13 @@ class Result(BaseModel):
else: else:
return value.title() return value.title()
@field_validator('msg')
@classmethod
def set_message(cls, value):
if isinstance(value, Exception):
value = parse_error_to_message(value=value)
return value
def __repr__(self) -> str: def __repr__(self) -> str:
return f"Result({self.owner})" return f"Result({self.owner})"
@@ -822,7 +830,7 @@ class Report(BaseModel):
def rreplace(s: str, old: str, new: str) -> str: def rreplace(s: str, old: str, new: str) -> str:
""" """
Removes rightmost occurence of a substring Removes rightmost occurrence of a substring
Args: Args:
s (str): input string s (str): input string
@@ -873,20 +881,6 @@ def remove_key_from_list_of_dicts(input: list, key: str) -> list:
return input return input
# def workbook_2_csv(worksheet: Worksheet, filename: Path):
# """
# Export an excel worksheet (workbook is not correct) to csv file.
#
# Args:
# worksheet (Worksheet): Incoming worksheet
# filename (Path): Output csv filepath.
# """
# with open(filename, 'w', newline="") as f:
# c = csv.writer(f)
# for r in worksheet.rows:
# c.writerow([cell.value for cell in r])
ctx = get_config(None) ctx = get_config(None)
@@ -909,7 +903,7 @@ def check_authorization(func):
Decorator to check if user is authorized to access function Decorator to check if user is authorized to access function
Args: Args:
func (_type_): Function to be used. func (function): Function to be used.
""" """
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
@@ -918,15 +912,27 @@ def check_authorization(func):
return func(*args, **kwargs) return func(*args, **kwargs)
else: else:
logger.error(f"User {getpass.getuser()} is not authorized for this function.") logger.error(f"User {getpass.getuser()} is not authorized for this function.")
return dict(code=1, message="This user does not have permission for this function.", status="warning") # return dict(code=1, message="This user does not have permission for this function.", status="warning")
report = Report()
report.add_result(Result(owner=func.__str__(), code=1, msg="This user does not have permission for this function.", status="warning"))
return report
return wrapper return wrapper
def report_result(func): def report_result(func):
"""
Decorator to display any reports returned from a function.
Args:
func (function): Function being decorated
Returns:
__type__: Output from decorated function
"""
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
logger.debug(f"Arguments: {args}") # logger.debug(f"Arguments: {args}")
logger.debug(f"Keyword arguments: {kwargs}") # logger.debug(f"Keyword arguments: {kwargs}")
output = func(*args, **kwargs) output = func(*args, **kwargs)
match output: match output:
case Report(): case Report():
@@ -955,3 +961,9 @@ def report_result(func):
logger.debug(f"Returning: {output}") logger.debug(f"Returning: {output}")
return output return output
return wrapper return wrapper
@report_result
@check_authorization
def test_function():
print("Success!")

View File

@@ -0,0 +1,30 @@
from sqlalchemy.exc import ArgumentError, IntegrityError as sqlalcIntegrityError
import logging
logger = logging.getLogger(f"submissions.{__name__}")
def parse_error_to_message(value: Exception):
"""
Converts an except to a human-readable error message for display.
Args:
value (Exception): Input exception
Returns:
str: Output message for display
"""
match value:
case sqlalcIntegrityError():
origin = value.orig.__str__().lower()
logger.error(f"Exception origin: {origin}")
if "unique constraint failed:" in origin:
field = " ".join(origin.split(".")[1:]).replace("_", " ").upper()
# logger.debug(field)
value = f"{field} doesn't have a unique value.\nIt must be changed."
else:
value = f"Got unknown integrity error: {value}"
case _:
value = f"Got generic error: {value}"
return value

View File

@@ -3,6 +3,7 @@
block_cipher = None block_cipher = None
#### custom for automation of documentation building #### #### custom for automation of documentation building ####
import sys, subprocess import sys, subprocess
from pathlib import Path from pathlib import Path
sys.path.append(Path(__name__).parent.joinpath('src').absolute().__str__()) sys.path.append(Path(__name__).parent.joinpath('src').absolute().__str__())
@@ -17,6 +18,7 @@ print(bcolors.BOLD + "Running Sphinx subprocess to generate html docs..." + bcol
docs_build = doc_path.joinpath("build") docs_build = doc_path.joinpath("build")
#docs_build.mkdir(exist_ok=True, parents=True) #docs_build.mkdir(exist_ok=True, parents=True)
subprocess.run([build_path, doc_path.joinpath("source").__str__(), docs_build.__str__(), "-a"]) subprocess.run([build_path, doc_path.joinpath("source").__str__(), docs_build.__str__(), "-a"])
######################################################### #########################################################
options = [ options = [