Moved to error reporting framework.
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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__}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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!")
|
||||
|
||||
30
src/submissions/tools/error_messaging.py
Normal file
30
src/submissions/tools/error_messaging.py
Normal 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
|
||||
Reference in New Issue
Block a user