Moved to error reporting framework.
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
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))
|
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:
|
||||||
|
|||||||
@@ -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__}")
|
||||||
|
|||||||
@@ -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,13 +44,17 @@ class SearchBox(QDialog):
|
|||||||
# logger.debug(deletes)
|
# logger.debug(deletes)
|
||||||
for item in deletes:
|
for item in deletes:
|
||||||
item.setParent(None)
|
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}")
|
# logger.debug(f"Sample type: {self.type}")
|
||||||
searchables = self.type.get_searchables()
|
searchables = self.type.get_searchables()
|
||||||
start_row = 1
|
start_row = 1
|
||||||
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)
|
||||||
|
|
||||||
|
|||||||
@@ -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!")
|
||||||
|
|||||||
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
|
||||||
@@ -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 = [
|
||||||
|
|||||||
Reference in New Issue
Block a user