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
- Improved error messaging when saving submissions.
- Upgraded sample search to (semi) realtime search.
- Improved error messaging.
## 202409.02

View File

@@ -19,7 +19,7 @@ def get_week_of_month() -> int:
__project__ = "submissions"
__version__ = f"{year}{str(month).zfill(2)}.{get_week_of_month()}b"
__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"
project_path = Path(__file__).parents[2].absolute()

View File

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

View File

@@ -9,7 +9,7 @@ from operator import itemgetter
from . import BaseClass
from tools import setup_lookup
from datetime import date, datetime
from typing import List
from typing import List, Literal
from dateutil.parser import parse
from re import Pattern
@@ -53,7 +53,7 @@ class ControlType(BaseClass):
pass
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)
@@ -161,7 +161,7 @@ class Control(BaseClass):
}
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
@@ -195,7 +195,7 @@ class Control(BaseClass):
output.append(_dict)
# logger.debug("Have to triage kraken data to keep program from getting overwhelmed")
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
@classmethod

View File

@@ -7,6 +7,7 @@ import datetime
import json
from pprint import pprint, pformat
import yaml
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB
from sqlalchemy.orm import relationship, validates, Query
from sqlalchemy.ext.associationproxy import association_proxy
@@ -873,7 +874,12 @@ class SubmissionType(BaseClass):
logging.critical(f"Given file could not be found.")
return None
with open(filepath, "r") as f:
import_dict = json.load(fp=f)
if filepath.suffix == ".json":
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))
submission_type = cls.query(name=import_dict['name'])
if submission_type:

View File

@@ -2425,15 +2425,18 @@ class BasicSample(BaseClass):
case _:
model = cls.find_polymorphic_subclass(attrs=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)
# logger.debug(f"Queried model. Now running searches in {kwargs}")
for k, v in kwargs.items():
# logger.debug(f"Running fuzzy search for attribute: {k} with value {v}")
search = f"%{v}%"
try:
attr = getattr(model, k)
query = query.filter(attr.like(search))
except (ArgumentError, AttributeError) as e:
logger.error(f"Attribute {k} unavailable due to:\n\t{e}\nSkipping.")
return query.all()
return query.limit(50).all()
def delete(self):
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.setObjectName("sample_type")
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.setEditable(False)
self.setMinimumSize(600, 600)
@@ -34,6 +34,7 @@ class SearchBox(QDialog):
self.layout.addWidget(self.results, 5, 0)
self.setLayout(self.layout)
self.update_widgets()
self.update_data()
def update_widgets(self):
"""
@@ -43,13 +44,17 @@ class SearchBox(QDialog):
# logger.debug(deletes)
for item in deletes:
item.setParent(None)
self.type = BasicSample.find_polymorphic_subclass(self.sample_type.currentText())
if self.sample_type.currentText() == "Any":
self.type = BasicSample
else:
self.type = BasicSample.find_polymorphic_subclass(self.sample_type.currentText())
# logger.debug(f"Sample type: {self.type}")
searchables = self.type.get_searchables()
start_row = 1
for iii, item in enumerate(searchables):
widget = FieldSearch(parent=self, label=item['label'], field_name=item['field'])
self.layout.addWidget(widget, start_row+iii, 0)
widget.search_widget.textChanged.connect(self.update_data)
def parse_form(self) -> dict:
"""
@@ -64,10 +69,12 @@ class SearchBox(QDialog):
def update_data(self):
"""
Shows dataframe of relevant samples.
"""
"""
# logger.debug(f"Running update_data with sample type: {self.type}")
fields = self.parse_form()
data = self.type.fuzzy_search(sample_type=self.type, **fields)
data = self.type.samples_to_df(sample_list=data)
# logger.debug(f"Got fields: {fields}")
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}")
self.results.setData(df=data)

View File

@@ -25,6 +25,7 @@ from __init__ import project_path
from configparser import ConfigParser
from tkinter import Tk # from tkinter import Tk for Python 3.x
from tkinter.filedialog import askdirectory
from .error_messaging import parse_error_to_message
logger = logging.getLogger(f"submissions.{__name__}")
@@ -765,10 +766,10 @@ def setup_lookup(func):
return wrapper
class Result(BaseModel):
class Result(BaseModel, arbitrary_types_allowed=True):
owner: str = Field(default="", validate_default=True)
code: int = Field(default=0)
msg: str
msg: str | Exception
status: Literal["NoIcon", "Question", "Information", "Warning", "Critical"] = Field(default="NoIcon")
@field_validator('status', mode='before')
@@ -779,6 +780,13 @@ class Result(BaseModel):
else:
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:
return f"Result({self.owner})"
@@ -822,7 +830,7 @@ class Report(BaseModel):
def rreplace(s: str, old: str, new: str) -> str:
"""
Removes rightmost occurence of a substring
Removes rightmost occurrence of a substring
Args:
s (str): input string
@@ -873,20 +881,6 @@ def remove_key_from_list_of_dicts(input: list, key: str) -> list:
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)
@@ -909,7 +903,7 @@ def check_authorization(func):
Decorator to check if user is authorized to access function
Args:
func (_type_): Function to be used.
func (function): Function to be used.
"""
def wrapper(*args, **kwargs):
@@ -918,15 +912,27 @@ def check_authorization(func):
return func(*args, **kwargs)
else:
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
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):
logger.debug(f"Arguments: {args}")
logger.debug(f"Keyword arguments: {kwargs}")
# logger.debug(f"Arguments: {args}")
# logger.debug(f"Keyword arguments: {kwargs}")
output = func(*args, **kwargs)
match output:
case Report():
@@ -955,3 +961,9 @@ def report_result(func):
logger.debug(f"Returning: {output}")
return output
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
#### custom for automation of documentation building ####
import sys, subprocess
from pathlib import Path
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.mkdir(exist_ok=True, parents=True)
subprocess.run([build_path, doc_path.joinpath("source").__str__(), docs_build.__str__(), "-a"])
#########################################################
options = [