ReagentLot add/edit updated.

This commit is contained in:
lwark
2025-09-10 12:39:02 -05:00
parent c9396d6c41
commit ba4912cab7
18 changed files with 1862 additions and 1734 deletions

View File

@@ -2,11 +2,12 @@
Contains all models for sqlalchemy Contains all models for sqlalchemy
""" """
from __future__ import annotations from __future__ import annotations
import sys, logging, json import sys, logging, json, inspect
from dateutil.parser import parse from dateutil.parser import parse
from jinja2 import TemplateNotFound
from pandas import DataFrame from pandas import DataFrame
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import Column, INTEGER, String, JSON from sqlalchemy import Column, INTEGER, String, JSON, DATETIME
from sqlalchemy.ext.associationproxy import AssociationProxy from sqlalchemy.ext.associationproxy import AssociationProxy
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session, InstrumentedAttribute, ColumnProperty from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session, InstrumentedAttribute, ColumnProperty
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
@@ -36,10 +37,6 @@ class BaseClass(Base):
__table_args__ = {'extend_existing': True} #: NOTE Will only add new columns __table_args__ = {'extend_existing': True} #: NOTE Will only add new columns
singles = ['id'] singles = ['id']
# omni_removes = ["id", 'run', "omnigui_class_dict", "omnigui_instance_dict"]
# omni_sort = ["name"]
# omni_inheritable = []
searchables = []
_misc_info = Column(JSON) _misc_info = Column(JSON)
@@ -155,6 +152,33 @@ class BaseClass(Base):
except AttributeError: except AttributeError:
return [] return []
@classmethod
def get_omni_sort(cls):
output = [item[0] for item in inspect.getmembers(cls, lambda a: not (inspect.isroutine(a)))
if isinstance(item[1], InstrumentedAttribute)] # and not isinstance(item[1].property, _RelationshipDeclared)]
output = [item for item in output if item not in ['_misc_info']]
return output
@classmethod
def get_searchables(cls):
output = []
for item in inspect.getmembers(cls, lambda a: not (inspect.isroutine(a))):
if item[0] in ["_misc_info"]:
continue
if not isinstance(item[1], InstrumentedAttribute):
continue
if not isinstance(item[1].property, ColumnProperty):
continue
# if isinstance(item[1], _RelationshipDeclared):
# if "association" in item[0]:
# continue
if len(item[1].foreign_keys) > 0:
continue
if item[1].type.__class__.__name__ not in ["String"]:
continue
output.append(item[0])
return output
@classmethod @classmethod
def get_default_info(cls, *args) -> dict | list | str: def get_default_info(cls, *args) -> dict | list | str:
""" """
@@ -296,7 +320,6 @@ class BaseClass(Base):
except AttributeError: except AttributeError:
check = False check = False
if check: if check:
logger.debug("Got uselist")
try: try:
query = query.filter(attr.contains(v)) query = query.filter(attr.contains(v))
except ArgumentError: except ArgumentError:
@@ -358,16 +381,14 @@ class BaseClass(Base):
dict: Dictionary of object minus _sa_instance_state with id at the front. dict: Dictionary of object minus _sa_instance_state with id at the front.
""" """
dicto = {key: dict(class_attr=getattr(self.__class__, key), instance_attr=getattr(self, key)) dicto = {key: dict(class_attr=getattr(self.__class__, key), instance_attr=getattr(self, key))
for key in dir(self.__class__) if for key in self.get_omni_sort()}
isinstance(getattr(self.__class__, key), InstrumentedAttribute) and key not in self.omni_removes
}
for k, v in dicto.items(): for k, v in dicto.items():
try: try:
v['instance_attr'] = v['instance_attr'].name v['instance_attr'] = v['instance_attr'].name
except AttributeError: except AttributeError:
continue continue
try: try:
dicto = list_sort_dict(input_dict=dicto, sort_list=self.__class__.omni_sort) dicto = list_sort_dict(input_dict=dicto, sort_list=self.__class__.get_omni_sort())
except TypeError as e: except TypeError as e:
logger.error(f"Could not sort {self.__class__.__name__} by list due to :{e}") logger.error(f"Could not sort {self.__class__.__name__} by list due to :{e}")
try: try:
@@ -418,7 +439,7 @@ class BaseClass(Base):
try: try:
template = env.get_template(temp_name) template = env.get_template(temp_name)
except TemplateNotFound as e: except TemplateNotFound as e:
logger.error(f"Couldn't find template {e}") # logger.error(f"Couldn't find template {e}")
template = env.get_template("details.html") template = env.get_template("details.html")
return template return template
@@ -505,7 +526,6 @@ class BaseClass(Base):
existing = self.__getattribute__(key) existing = self.__getattribute__(key)
# NOTE: This is causing problems with removal of items from lists. Have to overhaul it. # NOTE: This is causing problems with removal of items from lists. Have to overhaul it.
if existing is not None: if existing is not None:
logger.debug(f"{key} Existing: {existing}, incoming: {value}")
if isinstance(value, list): if isinstance(value, list):
value = value value = value
else: else:
@@ -626,7 +646,7 @@ class BaseClass(Base):
from backend.validators import pydant from backend.validators import pydant
if not pyd_model_name: if not pyd_model_name:
pyd_model_name = f"Pyd{self.__class__.__name__}" pyd_model_name = f"Pyd{self.__class__.__name__}"
logger.debug(f"Looking for pydant model {pyd_model_name}") logger.info(f"Looking for pydant model {pyd_model_name}")
try: try:
pyd = getattr(pydant, pyd_model_name) pyd = getattr(pydant, pyd_model_name)
except AttributeError: except AttributeError:

View File

@@ -194,6 +194,7 @@ class Reagent(BaseClass, LogMixin):
Concrete reagent instance Concrete reagent instance
""" """
skip_on_edit = False
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
reagentrole = relationship("ReagentRole", back_populates="reagent", reagentrole = relationship("ReagentRole", back_populates="reagent",
secondary=reagentrole_reagent) #: joined parent ReagentRole secondary=reagentrole_reagent) #: joined parent ReagentRole
@@ -319,15 +320,7 @@ class Reagent(BaseClass, LogMixin):
except AttributeError as e: except AttributeError as e:
logger.error(f"Could not set {key} due to {e}") logger.error(f"Could not set {key} due to {e}")
@check_authorization
def edit_from_search(self, obj, **kwargs):
from frontend.widgets.omni_add_edit import AddEdit
dlg = AddEdit(parent=None, instance=self)
if dlg.exec():
pyd = dlg.parse_form()
for field in pyd.model_fields:
self.set_attribute(field, pyd.__getattribute__(field))
self.save()
@classproperty @classproperty
def add_edit_tooltips(self): def add_edit_tooltips(self):
@@ -418,6 +411,15 @@ class ReagentLot(BaseClass):
pass pass
setattr(self, key, value) setattr(self, key, value)
@check_authorization
def edit_from_search(self, obj, **kwargs):
from frontend.widgets.omni_add_edit import AddEdit
dlg = AddEdit(parent=None, instance=self, disabled=['reagent'])
if dlg.exec():
pyd = dlg.parse_form()
for field in pyd.model_fields:
self.set_attribute(field, pyd.__getattribute__(field))
self.save()
class Discount(BaseClass): class Discount(BaseClass):
""" """
@@ -952,6 +954,7 @@ class Procedure(BaseClass):
output['sample'] = active_samples + inactive_samples output['sample'] = active_samples + inactive_samples
output['reagent'] = [reagent.details_dict() for reagent in output['procedurereagentlotassociation']] output['reagent'] = [reagent.details_dict() for reagent in output['procedurereagentlotassociation']]
output['equipment'] = [equipment.details_dict() for equipment in output['procedureequipmentassociation']] output['equipment'] = [equipment.details_dict() for equipment in output['procedureequipmentassociation']]
logger.debug(f"equipment: {pformat([item for item in output['equipment']])}")
output['repeat'] = self.repeat output['repeat'] = self.repeat
output['run'] = self.run.name output['run'] = self.run.name
output['excluded'] += self.get_default_info("details_ignore") output['excluded'] += self.get_default_info("details_ignore")
@@ -963,14 +966,6 @@ class Procedure(BaseClass):
def to_pydantic(self, **kwargs): def to_pydantic(self, **kwargs):
from backend.validators.pydant import PydReagent from backend.validators.pydant import PydReagent
output = super().to_pydantic() output = super().to_pydantic()
try:
output.kittype = dict(value=output.kittype['name'], missing=False)
except KeyError:
try:
output.kittype = dict(value=output.kittype['value'], missing=False)
except KeyError as e:
logger.error(f"Output.kittype: {output.kittype}")
raise e
output.sample = [item.to_pydantic() for item in output.proceduresampleassociation] output.sample = [item.to_pydantic() for item in output.proceduresampleassociation]
reagents = [] reagents = []
for reagent in output.reagent: for reagent in output.reagent:
@@ -985,6 +980,7 @@ class Procedure(BaseClass):
output.result = [item.to_pydantic() for item in self.results] output.result = [item.to_pydantic() for item in self.results]
output.sample_results = flatten_list( output.sample_results = flatten_list(
[[result.to_pydantic() for result in item.results] for item in self.proceduresampleassociation]) [[result.to_pydantic() for result in item.results] for item in self.proceduresampleassociation])
return output return output
def create_proceduresampleassociations(self, sample): def create_proceduresampleassociations(self, sample):
@@ -1607,7 +1603,7 @@ class Equipment(BaseClass, LogMixin):
from backend.validators.pydant import PydEquipment from backend.validators.pydant import PydEquipment
creation_dict = self.details_dict() creation_dict = self.details_dict()
processes = self.get_processes(equipmentrole=equipmentrole) processes = self.get_processes(equipmentrole=equipmentrole)
creation_dict['process'] = processes creation_dict['processes'] = processes
creation_dict['equipmentrole'] = equipmentrole or creation_dict['equipmentrole'] creation_dict['equipmentrole'] = equipmentrole or creation_dict['equipmentrole']
return PydEquipment(**creation_dict) return PydEquipment(**creation_dict)
@@ -2187,6 +2183,7 @@ class ProcedureEquipmentAssociation(BaseClass):
output.update(relevant) output.update(relevant)
output['misc_info'] = misc output['misc_info'] = misc
output['equipment_role'] = self.equipmentrole output['equipment_role'] = self.equipmentrole
output['processes'] = [item for item in self.equipment.get_processes(equipmentrole=output['equipment_role'])]
try: try:
output['processversion'] = self.processversion.details_dict() output['processversion'] = self.processversion.details_dict()
except AttributeError: except AttributeError:

View File

@@ -116,7 +116,6 @@ class ClientSubmission(BaseClass, LogMixin):
if start_date is not None: if start_date is not None:
start_date = cls.rectify_query_date(start_date) start_date = cls.rectify_query_date(start_date)
end_date = cls.rectify_query_date(end_date, eod=True) end_date = cls.rectify_query_date(end_date, eod=True)
logger.debug(f"Start date: {start_date}, end date: {end_date}")
query = query.filter(cls.submitted_date.between(start_date, end_date)) query = query.filter(cls.submitted_date.between(start_date, end_date))
# NOTE: by rsl number (returns only a single value) # NOTE: by rsl number (returns only a single value)
match submitter_plate_id: match submitter_plate_id:
@@ -303,7 +302,6 @@ class ClientSubmission(BaseClass, LogMixin):
return {item: self.__getattribute__(item.lower().replace(" ", "_")) for item in names} return {item: self.__getattribute__(item.lower().replace(" ", "_")) for item in names}
def add_run(self, obj): def add_run(self, obj):
logger.debug("Add Run")
from frontend.widgets.sample_checker import SampleChecker from frontend.widgets.sample_checker import SampleChecker
samples = [sample.to_pydantic() for sample in self.clientsubmissionsampleassociation] samples = [sample.to_pydantic() for sample in self.clientsubmissionsampleassociation]
checker = SampleChecker(parent=None, title="Create Run", samples=samples, clientsubmission=self) checker = SampleChecker(parent=None, title="Create Run", samples=samples, clientsubmission=self)
@@ -455,13 +453,14 @@ class Run(BaseClass, LogMixin):
output = {k: v for k, v in dicto.items() if k in args} output = {k: v for k, v in dicto.items() if k in args}
else: else:
output = {k: v for k, v in dicto.items()} output = {k: v for k, v in dicto.items()}
logger.debug(f"Submission type for get default info: {submissiontype}") # logger.debug(f"Submission type for get default info: {submissiontype}")
if isinstance(submissiontype, SubmissionType): if isinstance(submissiontype, SubmissionType):
st = submissiontype st = submissiontype
else: else:
st = cls.get_submission_type(submissiontype) st = cls.get_submission_type(submissiontype)
if st is None: if st is None:
logger.error("No default info for Run.") # logger.error("No default info for Run.")
pass
else: else:
output['submissiontype'] = st.name output['submissiontype'] = st.name
for k, v in st.defaults.items(): for k, v in st.defaults.items():

View File

@@ -36,16 +36,17 @@ class DefaultWriter(object):
value = value['name'] value = value['name']
except (KeyError, ValueError): except (KeyError, ValueError):
return return
# logger.debug(f"Value type: {type(value)}")
match value: match value:
case x if issubclass(value.__class__, BaseClass): case x if issubclass(value.__class__, BaseClass):
value = value.name value = value.name
case x if issubclass(value.__class__, PydBaseClass): case x if issubclass(value.__class__, PydBaseClass):
logger.warning(f"PydBaseClass: {value}")
value = value.name value = value.name
case bytes() | list(): case bytes() | list():
value = None value = None
case datetime() | date(): case datetime() | date():
value = value.strftime("%Y-%m-%d") value = value.strftime("%Y-%m-%d")
case _: case _:
value = str(value) value = str(value)
return value return value
@@ -80,9 +81,19 @@ class DefaultWriter(object):
self.worksheet = self.prewrite(self.worksheet, start_row=start_row) self.worksheet = self.prewrite(self.worksheet, start_row=start_row)
self.start_row = self.delineate_start_row(start_row=start_row) self.start_row = self.delineate_start_row(start_row=start_row)
self.end_row = self.delineate_end_row(start_row=start_row) self.end_row = self.delineate_end_row(start_row=start_row)
logger.debug(f"Rows for {self.__class__.__name__}:\tstart: {self.start_row}, end: {self.end_row}")
return workbook return workbook
def delineate_start_row(self, start_row: int = 1): def delineate_start_row(self, start_row: int = 1) -> int:
"""
Gets the first black row.
Args:
start_row (int): row to start looking at.
Returns:
int
"""
logger.debug(f"{self.__class__.__name__} will start looking for blank rows at {start_row}")
for iii, row in enumerate(self.worksheet.iter_rows(min_row=start_row), start=start_row): for iii, row in enumerate(self.worksheet.iter_rows(min_row=start_row), start=start_row):
if all([item.value is None for item in row]): if all([item.value is None for item in row]):
return iii return iii
@@ -135,6 +146,7 @@ class DefaultKEYVALUEWriter(DefaultWriter):
dictionary = sort_dict_by_list(dictionary=dictionary, order_list=self.key_order) dictionary = sort_dict_by_list(dictionary=dictionary, order_list=self.key_order)
for ii, (k, v) in enumerate(dictionary.items(), start=self.start_row): for ii, (k, v) in enumerate(dictionary.items(), start=self.start_row):
value = self.stringify_value(value=v) value = self.stringify_value(value=v)
logger.debug(f"{self.__class__.__name__} attempting to write {value}")
if value is None: if value is None:
continue continue
self.worksheet.cell(column=1, row=ii, value=self.prettify_key(k)) self.worksheet.cell(column=1, row=ii, value=self.prettify_key(k))
@@ -159,7 +171,9 @@ class DefaultTABLEWriter(DefaultWriter):
return row_count return row_count
def delineate_end_row(self, start_row: int = 1) -> int: def delineate_end_row(self, start_row: int = 1) -> int:
return start_row + len(self.pydant_obj) + 1 end_row = start_row + len(self.pydant_obj) + 1
logger.debug(f"End row has been delineated as {start_row} + {len(self.pydant_obj)} + 1 = {end_row}")
return end_row
def pad_samples_to_length(self, row_count, def pad_samples_to_length(self, row_count,
mode: Literal["submission", "procedure"] = "submission"): #, column_names): mode: Literal["submission", "procedure"] = "submission"): #, column_names):
@@ -206,6 +220,7 @@ class DefaultTABLEWriter(DefaultWriter):
value = object.improved_dict()[header.lower().replace(" ", "_")] value = object.improved_dict()[header.lower().replace(" ", "_")]
except (AttributeError, KeyError): except (AttributeError, KeyError):
value = "" value = ""
# logger.debug(f"{self.__class__.__name__} attempting to write {value}")
self.worksheet.cell(row=write_row, column=column, value=self.stringify_value(value)) self.worksheet.cell(row=write_row, column=column, value=self.stringify_value(value))
self.worksheet = self.postwrite(self.worksheet) self.worksheet = self.postwrite(self.worksheet)
return workbook return workbook

View File

@@ -15,7 +15,7 @@ class ProcedureInfoWriter(DefaultKEYVALUEWriter):
header_order = [] header_order = []
exclude = ['control', 'equipment', 'excluded', 'id', 'misc_info', 'plate_map', 'possible_kits', exclude = ['control', 'equipment', 'excluded', 'id', 'misc_info', 'plate_map', 'possible_kits',
'procedureequipmentassociation', 'procedurereagentassociation', 'proceduresampleassociation', 'proceduretipsassociation', 'reagent', 'procedureequipmentassociation', 'procedurereagentassociation', 'proceduresampleassociation', 'proceduretipsassociation', 'reagent',
'reagentrole', 'results', 'sample', 'tips'] 'reagentrole', 'results', 'sample', 'tips', 'reagentlot']
def __init__(self, pydant_obj, *args, **kwargs): def __init__(self, pydant_obj, *args, **kwargs):
@@ -25,23 +25,23 @@ class ProcedureInfoWriter(DefaultKEYVALUEWriter):
def write_to_workbook(self, workbook: Workbook, sheet: str | None = None, def write_to_workbook(self, workbook: Workbook, sheet: str | None = None,
start_row: int = 1, *args, **kwargs) -> Workbook: start_row: int = 1, *args, **kwargs) -> Workbook:
workbook = super().write_to_workbook(workbook=workbook, sheet=f"{self.pydant_obj.proceduretype.name} Quality") workbook = super().write_to_workbook(workbook=workbook, sheet=f"{self.pydant_obj.proceduretype.name[:20]} Quality")
return workbook return workbook
class ProcedureReagentWriter(DefaultTABLEWriter): class ProcedureReagentWriter(DefaultTABLEWriter):
exclude = ["id", "comments", "missing"] exclude = ["id", "comments", "missing", "active", "name"]
header_order = ["reagentrole", "name", "lot", "expiry"] header_order = ["reagentrole", "reagent_name", "lot", "expiry"]
def __init__(self, pydant_obj, *args, **kwargs): def __init__(self, pydant_obj, *args, **kwargs):
super().__init__(pydant_obj=pydant_obj, *args, **kwargs) super().__init__(pydant_obj=pydant_obj, *args, **kwargs)
self.sheet = f"{self.pydant_obj.proceduretype.name} Quality" self.sheet = f"{self.pydant_obj.proceduretype.name[:20]} Quality"
self.pydant_obj = self.pydant_obj.reagent self.pydant_obj = self.pydant_obj.reagent
def write_to_workbook(self, workbook: Workbook, sheet: str | None = None, def write_to_workbook(self, workbook: Workbook, sheet: str | None = None,
start_row: int = 1, *args, **kwargs) -> Workbook: start_row: int = 1, *args, **kwargs) -> Workbook:
workbook = super().write_to_workbook(workbook=workbook, sheet=self.sheet) workbook = super().write_to_workbook(workbook=workbook, sheet=self.sheet, start_row=start_row)
return workbook return workbook
@@ -52,12 +52,12 @@ class ProcedureEquipmentWriter(DefaultTABLEWriter):
def __init__(self, pydant_obj, range_dict: dict | None = None, *args, **kwargs): def __init__(self, pydant_obj, range_dict: dict | None = None, *args, **kwargs):
super().__init__(pydant_obj=pydant_obj, range_dict=range_dict, *args, **kwargs) super().__init__(pydant_obj=pydant_obj, range_dict=range_dict, *args, **kwargs)
self.sheet = f"{self.pydant_obj.proceduretype.name} Quality" self.sheet = f"{self.pydant_obj.proceduretype.name[:20]} Quality"
self.pydant_obj = self.pydant_obj.equipment self.pydant_obj = self.pydant_obj.equipment
def write_to_workbook(self, workbook: Workbook, sheet: str | None = None, def write_to_workbook(self, workbook: Workbook, sheet: str | None = None,
start_row: int = 1, *args, **kwargs) -> Workbook: start_row: int = 1, *args, **kwargs) -> Workbook:
workbook = super().write_to_workbook(workbook=workbook, sheet=self.sheet) workbook = super().write_to_workbook(workbook=workbook, sheet=self.sheet, start_row=start_row)
return workbook return workbook
@@ -68,10 +68,10 @@ class ProcedureSampleWriter(DefaultTABLEWriter):
def __init__(self, pydant_obj, range_dict: dict | None = None, *args, **kwargs): def __init__(self, pydant_obj, range_dict: dict | None = None, *args, **kwargs):
super().__init__(pydant_obj=pydant_obj, range_dict=range_dict, *args, **kwargs) super().__init__(pydant_obj=pydant_obj, range_dict=range_dict, *args, **kwargs)
self.sheet = f"{self.pydant_obj.proceduretype.name} Quality" self.sheet = f"{self.pydant_obj.proceduretype.name[:20]} Quality"
self.pydant_obj = self.pad_samples_to_length(row_count=pydant_obj.max_sample_rank, mode="procedure") self.pydant_obj = self.pad_samples_to_length(row_count=pydant_obj.max_sample_rank, mode="procedure")
def write_to_workbook(self, workbook: Workbook, sheet: str | None = None, def write_to_workbook(self, workbook: Workbook, sheet: str | None = None,
start_row: int = 1, *args, **kwargs) -> Workbook: start_row: int = 1, *args, **kwargs) -> Workbook:
workbook = super().write_to_workbook(workbook=workbook, sheet=self.sheet) workbook = super().write_to_workbook(workbook=workbook, sheet=self.sheet, start_row=start_row)
return workbook return workbook

View File

@@ -66,19 +66,19 @@ class DefaultProcedureManager(DefaultManager):
except AttributeError: except AttributeError:
reagent_writer = procedure_writers.ProcedureReagentWriter reagent_writer = procedure_writers.ProcedureReagentWriter
self.reagent_writer = reagent_writer(pydant_obj=self.pyd) self.reagent_writer = reagent_writer(pydant_obj=self.pyd)
workbook = self.reagent_writer.write_to_workbook(workbook) workbook = self.reagent_writer.write_to_workbook(workbook, start_row=self.info_writer.end_row)
try: try:
equipment_writer = getattr(procedure_writers, f"{self.proceduretype.name.replace(' ', '')}EquipmentWriter") equipment_writer = getattr(procedure_writers, f"{self.proceduretype.name.replace(' ', '')}EquipmentWriter")
except AttributeError: except AttributeError:
equipment_writer = procedure_writers.ProcedureEquipmentWriter equipment_writer = procedure_writers.ProcedureEquipmentWriter
self.equipment_writer = equipment_writer(pydant_obj=self.pyd) self.equipment_writer = equipment_writer(pydant_obj=self.pyd)
workbook = self.equipment_writer.write_to_workbook(workbook) workbook = self.equipment_writer.write_to_workbook(workbook, start_row=self.reagent_writer.end_row)
try: try:
sample_writer = getattr(procedure_writers, f"{self.proceduretype.name.replace(' ', '')}SampleWriter") sample_writer = getattr(procedure_writers, f"{self.proceduretype.name.replace(' ', '')}SampleWriter")
except AttributeError: except AttributeError:
sample_writer = procedure_writers.ProcedureSampleWriter sample_writer = procedure_writers.ProcedureSampleWriter
self.sample_writer = sample_writer(pydant_obj=self.pyd) self.sample_writer = sample_writer(pydant_obj=self.pyd)
workbook = self.sample_writer.write_to_workbook(workbook) workbook = self.sample_writer.write_to_workbook(workbook, start_row=self.equipment_writer.end_row)
# # TODO: Find way to group results by result_type. # # TODO: Find way to group results by result_type.
for result in self.pyd.result: for result in self.pyd.result:
Writer = getattr(results_writers, f"{result.result_type}InfoWriter") Writer = getattr(results_writers, f"{result.result_type}InfoWriter")

File diff suppressed because it is too large Load Diff

View File

@@ -321,7 +321,7 @@ class PydTips(PydBaseClass):
SubmissionTipsAssociation: Association between queried tips and procedure SubmissionTipsAssociation: Association between queried tips and procedure
""" """
report = Report() report = Report()
tips = TipsLot.query(name=self.name, limit=1) tips = TipsLot.query(lot=self.lot, limit=1)
return tips, report return tips, report
@@ -329,7 +329,8 @@ class PydEquipment(PydBaseClass):
asset_number: str asset_number: str
name: str name: str
nickname: str | None nickname: str | None
process: List[PydProcess] | PydProcess | None processes: List[PydProcess] | PydProcess | None
processversion: PydProcess | None
equipmentrole: str | PydEquipmentRole | None equipmentrole: str | PydEquipmentRole | None
tips: List[PydTips] | PydTips | None = Field(default=[]) tips: List[PydTips] | PydTips | None = Field(default=[])
@@ -347,7 +348,7 @@ class PydEquipment(PydBaseClass):
value = value.name value = value.name
return value return value
@field_validator('process', mode='before') @field_validator('processes', mode='before')
@classmethod @classmethod
def process_to_pydantic(cls, value, values): def process_to_pydantic(cls, value, values):
if isinstance(value, GeneratorType): if isinstance(value, GeneratorType):
@@ -355,16 +356,25 @@ class PydEquipment(PydBaseClass):
value = convert_nans_to_nones(value) value = convert_nans_to_nones(value)
if not value: if not value:
value = [] value = []
if isinstance(value, ProcessVersion): match value:
value = value.to_pydantic(pyd_model_name="PydProcess") case ProcessVersion():
else: value = value.to_pydantic(pyd_model_name="PydProcess")
try: case _:
d: Process = next((process for process in value if values.data['name'] in [item.name for item in process.equipment]), None) try:
if d: # d: Process = next((process for process in value if values.data['name'] in [item.name for item in process.equipment]), None)
value = d.to_pydantic() for process in value:
except AttributeError as e: match process:
logger.error(f"Process Validation error due to {e}") case Process():
pass if values.data['name'] in [item.name for item in process.equipment]:
return process.to_pydantic()
return None
case str():
return process
# else:
# value = []
except AttributeError as e:
logger.error(f"Process Validation error due to {e}")
value = []
return value return value
@field_validator('tips', mode='before') @field_validator('tips', mode='before')
@@ -386,7 +396,7 @@ class PydEquipment(PydBaseClass):
value = d.to_pydantic() value = d.to_pydantic()
except AttributeError as e: except AttributeError as e:
logger.error(f"Process Validation error due to {e}") logger.error(f"Process Validation error due to {e}")
pass value = []
return value return value
@report_result @report_result
@@ -1347,6 +1357,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
def to_sql(self, new: bool = False): def to_sql(self, new: bool = False):
from backend.db.models import RunSampleAssociation, ProcedureSampleAssociation from backend.db.models import RunSampleAssociation, ProcedureSampleAssociation
logger.debug(f"incoming pyd: {pformat([item.__dict__ for item in self.equipment])}")
if new: if new:
sql = Procedure() sql = Procedure()
else: else:
@@ -1408,6 +1419,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
procedure_rank=sample.procedure_rank) procedure_rank=sample.procedure_rank)
for equipment in self.equipment: for equipment in self.equipment:
equip, _ = equipment.to_sql() equip, _ = equipment.to_sql()
logger.debug(f"Equipment:\n{pformat(equip.__dict__)}")
if isinstance(equipment.process, list): if isinstance(equipment.process, list):
equipment.process = equipment.process[0] equipment.process = equipment.process[0]
if isinstance(equipment.tips, list): if isinstance(equipment.tips, list):
@@ -1420,10 +1432,13 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
equipmentrole=equip.equipmentrole[0]) equipmentrole=equip.equipmentrole[0])
process = equipment.process.to_sql() process = equipment.process.to_sql()
equip_assoc.processversion = process equip_assoc.processversion = process
logger.debug(f"Tips: {type(equipment.tips)}")
try: try:
tipslot = equipment.tips.to_sql() tipslot = equipment.tips.to_sql()
logger.debug(f"Tipslot: {tipslot.__dict__}")
except AttributeError: except AttributeError:
tipslot = None tipslot = None
equip_assoc.tipslot = tipslot equip_assoc.tipslot = tipslot
return sql, None return sql, None
@@ -1560,6 +1575,13 @@ class PydClientSubmission(PydBaseClass):
value = int(value) value = int(value)
return value return value
@field_validator("cost_centre", mode="before")
@classmethod
def str_to_dict(cls, value):
if isinstance(value, str):
value = dict(value=value)
return value
def to_form(self, parent: QWidget, samples: List = [], disable: list | None = None): def to_form(self, parent: QWidget, samples: List = [], disable: list | None = None):
""" """
Converts this instance into a frontend.widgets.submission_widget.SubmissionFormWidget Converts this instance into a frontend.widgets.submission_widget.SubmissionFormWidget

View File

@@ -35,7 +35,7 @@ class ConcentrationsChart(CustomFigure):
color="positive", color_discrete_map={"positive": "red", "negative": "green", "sample":"orange"} color="positive", color_discrete_map={"positive": "red", "negative": "green", "sample":"orange"}
) )
except (ValueError, AttributeError) as e: except (ValueError, AttributeError) as e:
logger.error(f"Error constructing chart: {e}") # logger.error(f"Error constructing chart: {e}")
scatter = px.scatter() scatter = px.scatter()
# NOTE: For some reason if data is allowed to sort itself it leads to wrong ordering of x axis. # NOTE: For some reason if data is allowed to sort itself it leads to wrong ordering of x axis.
traces = sorted(scatter.data, key=itemgetter("name")) traces = sorted(scatter.data, key=itemgetter("name"))

View File

@@ -13,7 +13,7 @@ from PyQt6.QtGui import QAction
from pathlib import Path from pathlib import Path
from markdown import markdown from markdown import markdown
from pandas import ExcelWriter from pandas import ExcelWriter
from backend.db.models import Reagent from backend.db.models import ReagentLot
from tools import ( from tools import (
check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user, check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user,
under_development under_development
@@ -181,7 +181,7 @@ class App(QMainWindow):
@check_authorization @check_authorization
def edit_reagent(self, *args, **kwargs): def edit_reagent(self, *args, **kwargs):
dlg = SearchBox(parent=self, object_type=Reagent, extras=[dict(name='Role', field="reagentrole")]) dlg = SearchBox(parent=self, object_type=ReagentLot, extras=[dict(name='Role', field="reagentrole")])
dlg.exec() dlg.exec()
def update_data(self): def update_data(self):

View File

@@ -3,7 +3,7 @@ A widget to handle adding/updating any database object.
""" """
from datetime import date from datetime import date
from pprint import pformat from pprint import pformat
from typing import Any, Tuple from typing import Any, Tuple, List
from pydantic import BaseModel from pydantic import BaseModel
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QLabel, QDialog, QWidget, QLineEdit, QGridLayout, QComboBox, QDialogButtonBox, QDateEdit, QSpinBox, QDoubleSpinBox, QLabel, QDialog, QWidget, QLineEdit, QGridLayout, QComboBox, QDialogButtonBox, QDateEdit, QSpinBox, QDoubleSpinBox,
@@ -13,6 +13,8 @@ from sqlalchemy import String, TIMESTAMP, INTEGER, FLOAT, JSON, BLOB
from sqlalchemy.orm import ColumnProperty from sqlalchemy.orm import ColumnProperty
import logging import logging
from sqlalchemy.orm.relationships import _RelationshipDeclared from sqlalchemy.orm.relationships import _RelationshipDeclared
from backend.db.models import BaseClass
from backend.validators.pydant import PydBaseClass
from tools import Report, report_result from tools import Report, report_result
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -20,20 +22,19 @@ logger = logging.getLogger(f"submissions.{__name__}")
class AddEdit(QDialog): class AddEdit(QDialog):
def __init__(self, parent, instance: Any | None = None, managers: set = set()): def __init__(self, parent, instance: Any | None = None, managers: set = set(), disabled: List[str] = []):
super().__init__(parent) super().__init__(parent)
# logger.debug(f"Managers: {managers}") logger.debug(f"Disable = {disabled}")
self.instance = instance self.instance = instance
self.object_type = instance.__class__ self.object_type = instance.__class__
self.managers = managers self.managers = managers
# logger.debug(f"Managers: {managers}")
self.layout = QGridLayout(self) self.layout = QGridLayout(self)
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
# logger.debug(f"Fields: {pformat(self.instance.omnigui_instance_dict)}")
fields = {k: v for k, v in self.instance.omnigui_instance_dict.items() if "id" not in k} fields = {k: v for k, v in self.instance.omnigui_instance_dict.items() if "id" not in k}
logger.debug(f"Fields: {pformat(fields)}")
# NOTE: Move 'name' to the front # NOTE: Move 'name' to the front
try: try:
fields = {'name': fields.pop('name'), **fields} fields = {'name': fields.pop('name'), **fields}
@@ -41,13 +42,13 @@ class AddEdit(QDialog):
pass pass
height_counter = 0 height_counter = 0
for key, field in fields.items(): for key, field in fields.items():
disable = key in disabled
try: try:
value = getattr(self.instance, key) value = getattr(self.instance, key)
except AttributeError: except AttributeError:
value = None value = None
try: try:
logger.debug(f"{key} property: {type(field['class_attr'].property)}") widget = EditProperty(self, key=key, column_type=field, value=value, disable=disable)
widget = EditProperty(self, key=key, column_type=field, value=value)
except AttributeError as e: except AttributeError as e:
logger.error(f"Problem setting widget {key}: {e}") logger.error(f"Problem setting widget {key}: {e}")
continue continue
@@ -55,7 +56,7 @@ class AddEdit(QDialog):
self.layout.addWidget(widget, self.layout.rowCount(), 0) self.layout.addWidget(widget, self.layout.rowCount(), 0)
height_counter += 1 height_counter += 1
self.layout.addWidget(self.buttonBox) self.layout.addWidget(self.buttonBox)
self.setWindowTitle(f"Add/Edit {self.object_type.__name__} - Manager: {self.managers}") self.setWindowTitle(f"Add/Edit {self.object_type.__name__}")# - Manager: {self.managers}")
self.setMinimumSize(600, 50 * height_counter) self.setMinimumSize(600, 50 * height_counter)
self.setLayout(self.layout) self.setLayout(self.layout)
@@ -64,11 +65,8 @@ class AddEdit(QDialog):
report = Report() report = Report()
parsed = {result[0].strip(":"): result[1] for result in parsed = {result[0].strip(":"): result[1] for result in
[item.parse_form() for item in self.findChildren(EditProperty)] if result[0]} [item.parse_form() for item in self.findChildren(EditProperty)] if result[0]}
# logger.debug(f"Parsed form: {parsed}")
model = self.object_type.pydantic_model model = self.object_type.pydantic_model
# logger.debug(f"Model type: {model.__name__}")
if model.__name__ == "PydElastic": if model.__name__ == "PydElastic":
# logger.debug(f"We have an elastic model.")
parsed['instance'] = self.instance parsed['instance'] = self.instance
# NOTE: Hand-off to pydantic model for validation. # NOTE: Hand-off to pydantic model for validation.
# NOTE: Also, why am I not just using the toSQL method here. I could write one for contact. # NOTE: Also, why am I not just using the toSQL method here. I could write one for contact.
@@ -78,8 +76,9 @@ class AddEdit(QDialog):
class EditProperty(QWidget): class EditProperty(QWidget):
def __init__(self, parent: AddEdit, key: str, column_type: Any, value): def __init__(self, parent: AddEdit, key: str, column_type: Any, value, disable: bool):
super().__init__(parent) super().__init__(parent)
logger.debug(f"Widget column type for {key}: {column_type}")
self.name = key self.name = key
self.label = QLabel(key.title().replace("_", " ")) self.label = QLabel(key.title().replace("_", " "))
self.layout = QGridLayout() self.layout = QGridLayout()
@@ -88,6 +87,7 @@ class EditProperty(QWidget):
self.property_class = column_type['class_attr'].property.entity.class_ self.property_class = column_type['class_attr'].property.entity.class_
except AttributeError: except AttributeError:
self.property_class = None self.property_class = None
logger.debug(f"Property class: {self.property_class}")
try: try:
self.is_list = column_type['class_attr'].property.uselist self.is_list = column_type['class_attr'].property.uselist
except AttributeError: except AttributeError:
@@ -96,23 +96,26 @@ class EditProperty(QWidget):
case ColumnProperty(): case ColumnProperty():
self.column_property_set(column_type, value=value) self.column_property_set(column_type, value=value)
case _RelationshipDeclared(): case _RelationshipDeclared():
if not self.property_class.skip_on_edit: try:
check = self.property_class.skip_on_edit
except AttributeError:
check = False
if not check:
self.relationship_property_set(column_type, value=value) self.relationship_property_set(column_type, value=value)
else: else:
return return
case _: case _:
logger.error(f"{column_type} not a supported type.") logger.error(f"{column_type} not a supported type.")
return return
self.widget.setDisabled(disable)
self.layout.addWidget(self.label, 0, 0, 1, 1) self.layout.addWidget(self.label, 0, 0, 1, 1)
self.layout.addWidget(self.widget, 0, 1, 1, 3) self.layout.addWidget(self.widget, 0, 1, 1, 3)
self.setLayout(self.layout) self.setLayout(self.layout)
def relationship_property_set(self, relationship, value=None): def relationship_property_set(self, relationship, value=None):
self.widget = QComboBox() self.widget = QComboBox()
# logger.debug(self.parent().managers)
for manager in self.parent().managers: for manager in self.parent().managers:
if self.name in manager.aliases: if self.name in manager.aliases:
# logger.debug(f"Name: {self.name} is in aliases: {manager.aliases}")
choices = [manager.name] choices = [manager.name]
self.widget.setEnabled(False) self.widget.setEnabled(False)
break break
@@ -127,11 +130,17 @@ class EditProperty(QWidget):
if isinstance(instance_value, list): if isinstance(instance_value, list):
instance_value = next((item.name for item in instance_value), None) instance_value = next((item.name for item in instance_value), None)
if instance_value: if instance_value:
match instance_value:
case x if issubclass(instance_value.__class__, BaseClass):
instance_value = instance_value.name
case x if issubclass(instance_value.__class__, PydBaseClass):
instance_value = instance_value.name
case _:
pass
choices.insert(0, choices.pop(choices.index(instance_value))) choices.insert(0, choices.pop(choices.index(instance_value)))
self.widget.addItems(choices) self.widget.addItems(choices)
def column_property_set(self, column_property, value=None): def column_property_set(self, column_property, value=None):
# logger.debug(f"Column Property: {column_property['class_attr'].expression} {column_property}, Value: {value}")
match column_property['class_attr'].expression.type: match column_property['class_attr'].expression.type:
case String(): case String():
if value is None: if value is None:
@@ -176,7 +185,6 @@ class EditProperty(QWidget):
check = self.widget check = self.widget
except AttributeError: except AttributeError:
return None, None return None, None
# match self.widget
match check: match check:
case QLineEdit(): case QLineEdit():
value = self.widget.text() value = self.widget.text()

File diff suppressed because it is too large Load Diff

View File

@@ -72,18 +72,14 @@ class SearchBox(QDialog):
self.object_type = self.original_type self.object_type = self.original_type
else: else:
self.object_type = self.original_type.find_regular_subclass(self.sub_class.currentText()) self.object_type = self.original_type.find_regular_subclass(self.sub_class.currentText())
# logger.debug(f"Object type: {self.object_type} - {self.object_type.searchables}") for item in self.object_type.get_searchables():
# logger.debug(f"Original type: {self.original_type} - {self.original_type.searchables}") if item in [thing for thing in search_fields]:
for item in self.object_type.searchables:
if item['field'] in [item['field'] for item in search_fields]:
logger.debug(f"Already have {item['field']}")
continue continue
else: else:
search_fields.append(item) search_fields.append(item)
logger.debug(f"Search fields: {search_fields}")
for iii, searchable in enumerate(search_fields): for iii, searchable in enumerate(search_fields):
widget = FieldSearch(parent=self, label=searchable['label'], field_name=searchable['field']) widget = FieldSearch(parent=self, label=searchable, field_name=searchable)
widget.setObjectName(searchable['field']) widget.setObjectName(searchable)
self.layout.addWidget(widget, 1 + iii, 0) self.layout.addWidget(widget, 1 + iii, 0)
widget.search_widget.textChanged.connect(self.update_data) widget.search_widget.textChanged.connect(self.update_data)
self.update_data() self.update_data()
@@ -172,7 +168,6 @@ class SearchResults(QTableView):
self.extras = extras + [item for item in deepcopy(self.object_type.searchables)] self.extras = extras + [item for item in deepcopy(self.object_type.searchables)]
except AttributeError: except AttributeError:
self.extras = extras self.extras = extras
# logger.debug(f"Extras: {self.extras}")
def setData(self, df: DataFrame) -> None: def setData(self, df: DataFrame) -> None:
""" """

View File

@@ -163,4 +163,5 @@ class ProcedureCreation(QDialog):
self.procedure.update_reagents(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry) self.procedure.update_reagents(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry)
def return_sql(self, new: bool = False): def return_sql(self, new: bool = False):
return self.procedure.to_sql(new=new) output = self.procedure.to_sql(new=new)
return output

View File

@@ -264,7 +264,6 @@ class SubmissionComment(QDialog):
""" """
def __init__(self, parent, submission: Run) -> None: def __init__(self, parent, submission: Run) -> None:
logger.debug(parent)
super().__init__(parent) super().__init__(parent)
self.app = get_application_from_parent(parent) self.app = get_application_from_parent(parent)
self.submission = submission self.submission = submission
@@ -284,7 +283,7 @@ class SubmissionComment(QDialog):
self.layout.addWidget(self.buttonBox, alignment=Qt.AlignmentFlag.AlignBottom) self.layout.addWidget(self.buttonBox, alignment=Qt.AlignmentFlag.AlignBottom)
self.setLayout(self.layout) self.setLayout(self.layout)
def parse_form(self) -> List[dict]: def parse_form(self) -> dict:
""" """
Adds comment to procedure object. Adds comment to procedure object.
""" """

View File

@@ -12,7 +12,7 @@ from pathlib import Path
from tools import Report, Result, check_not_nan, main_form_style, report_result, get_application_from_parent from tools import Report, Result, check_not_nan, main_form_style, report_result, get_application_from_parent
from backend.validators import PydReagent, PydClientSubmission, PydSample from backend.validators import PydReagent, PydClientSubmission, PydSample
from backend.db import ( from backend.db import (
ClientLab, SubmissionType, Reagent, ClientLab, SubmissionType, Reagent, ReagentLot,
ReagentRole, ProcedureTypeReagentRoleAssociation, Run, ClientSubmission ReagentRole, ProcedureTypeReagentRoleAssociation, Run, ClientSubmission
) )
from pprint import pformat from pprint import pformat
@@ -137,7 +137,7 @@ class SubmissionFormContainer(QWidget):
return report return report
@report_result @report_result
def add_reagent(self, instance: Reagent | None = None): def add_reagent(self, instance: ReagentLot | None = None):
""" """
Action to create new reagent in DB. Action to create new reagent in DB.
@@ -149,7 +149,7 @@ class SubmissionFormContainer(QWidget):
""" """
report = Report() report = Report()
if not instance: if not instance:
instance = Reagent() instance = ReagentLot()
dlg = AddEdit(parent=self, instance=instance) dlg = AddEdit(parent=self, instance=instance)
if dlg.exec(): if dlg.exec():
reagent = dlg.parse_form() reagent = dlg.parse_form()

View File

@@ -1,18 +1,16 @@
''' """
Contains miscellaenous functions used by both frontend and backend. Contains miscellaenous functions used by both frontend and backend.
''' """
from __future__ import annotations from __future__ import annotations
import builtins, importlib, time, logging, re, yaml, sys, os, stat, platform, getpass, json, numpy as np, pandas as pd import builtins, importlib, time, logging, re, yaml, sys, os, stat, platform, getpass, json, numpy as np, pandas as pd, \
import itertools itertools, openpyxl
from copy import copy
from collections import OrderedDict from collections import OrderedDict
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from json import JSONDecodeError from json import JSONDecodeError
from threading import Thread from threading import Thread
from inspect import getmembers, isfunction, stack from inspect import getmembers, isfunction, stack
from types import NoneType
from dateutil.easter import easter from dateutil.easter import easter
from dateutil.parser import parse
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from logging import handlers, Logger from logging import handlers, Logger
from pathlib import Path from pathlib import Path
@@ -60,7 +58,6 @@ main_form_style = '''
page_size = 250 page_size = 250
# micro_char = uni_char = "\u03BC"
def divide_chunks(input_list: list, chunk_count: int) -> Generator[Any, Any, None]: def divide_chunks(input_list: list, chunk_count: int) -> Generator[Any, Any, None]:
""" """
@@ -447,16 +444,18 @@ def jinja_template_loading() -> Environment:
return env return env
def render_details_template(template_name:str, css_in:List[str]|str=[], js_in:List[str]|str=[], **kwargs) -> str: def render_details_template(template_name: str, css_in: List[str] | str = [], js_in: List[str] | str = [],
**kwargs) -> str:
if isinstance(css_in, str): if isinstance(css_in, str):
css_in = [css_in] css_in = [css_in]
env = jinja_template_loading()
html_folder = Path(env.loader.__getattribute__("searchpath")[0])
css_in = ["styles"] + css_in css_in = ["styles"] + css_in
css_in = [project_path.joinpath("src", "submissions", "templates", "css", f"{c}.css") for c in css_in] css_in = [html_folder.joinpath("css", f"{c}.css") for c in css_in]
if isinstance(js_in, str): if isinstance(js_in, str):
js_in = [js_in] js_in = [js_in]
js_in = ["details"] + js_in js_in = ["details"] + js_in
js_in = [project_path.joinpath("src", "submissions", "templates", "js", f"{j}.js") for j in js_in] js_in = [html_folder.joinpath("js", f"{j}.js") for j in js_in]
env = jinja_template_loading()
template = env.get_template(f"{template_name}.html") template = env.get_template(f"{template_name}.html")
# template_path = Path(template.environment.loader.__getattribute__("searchpath")[0]) # template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
css_out = [] css_out = []
@@ -467,7 +466,6 @@ def render_details_template(template_name:str, css_in:List[str]|str=[], js_in:Li
for js in js_in: for js in js_in:
with open(js, "r") as f: with open(js, "r") as f:
js_out.append(f.read()) js_out.append(f.read())
# logger.debug(f"Kwargs: {kwargs}")
return template.render(css=css_out, js=js_out, **kwargs) return template.render(css=css_out, js=js_out, **kwargs)
@@ -489,10 +487,10 @@ def convert_well_to_row_column(input_str: str) -> Tuple[int, int]:
return None, None return None, None
return row, column return row, column
# Copy a sheet with style, format, layout, ect. from one Excel file to another Excel file # Copy a sheet with style, format, layout, ect. from one Excel file to another Excel file
# Please add the ..path\\+\\file.. and ..sheet_name.. according to your desire. # Please add the ..path\\+\\file.. and ..sheet_name.. according to your desire.
import openpyxl
from copy import copy
def copy_xl_sheet(source_sheet, target_sheet): def copy_xl_sheet(source_sheet, target_sheet):
@@ -509,8 +507,8 @@ def copy_sheet_attributes(source_sheet, target_sheet):
target_sheet.page_margins = copy(source_sheet.page_margins) target_sheet.page_margins = copy(source_sheet.page_margins)
target_sheet.freeze_panes = copy(source_sheet.freeze_panes) target_sheet.freeze_panes = copy(source_sheet.freeze_panes)
# set row dimensions # NOTE: set row dimensions
# So you cannot copy the row_dimensions attribute. Does not work (because of meta data in the attribute I think). So we copy every row's row_dimensions. That seems to work. # NOTE: So you cannot copy the row_dimensions attribute. Does not work (because of meta data in the attribute I think). So we copy every row's row_dimensions. That seems to work.
for rn in range(len(source_sheet.row_dimensions)): for rn in range(len(source_sheet.row_dimensions)):
target_sheet.row_dimensions[rn] = copy(source_sheet.row_dimensions[rn]) target_sheet.row_dimensions[rn] = copy(source_sheet.row_dimensions[rn])
@@ -519,12 +517,15 @@ def copy_sheet_attributes(source_sheet, target_sheet):
else: else:
target_sheet.sheet_format.defaultColWidth = copy(source_sheet.sheet_format.defaultColWidth) target_sheet.sheet_format.defaultColWidth = copy(source_sheet.sheet_format.defaultColWidth)
# set specific column width and hidden property # NOTE: set specific column width and hidden property
# we cannot copy the entire column_dimensions attribute so we copy selected attributes # NOTE: we cannot copy the entire column_dimensions attribute so we copy selected attributes
for key, value in source_sheet.column_dimensions.items(): for key, value in source_sheet.column_dimensions.items():
target_sheet.column_dimensions[key].min = copy(source_sheet.column_dimensions[key].min) # Excel actually groups multiple columns under 1 key. Use the min max attribute to also group the columns in the targetSheet target_sheet.column_dimensions[key].min = copy(source_sheet.column_dimensions[
target_sheet.column_dimensions[key].max = copy(source_sheet.column_dimensions[key].max) # https://stackoverflow.com/questions/36417278/openpyxl-can-not-read-consecutive-hidden-columns discussed the issue. Note that this is also the case for the width, not onl;y the hidden property key].min) # Excel actually groups multiple columns under 1 key. Use the min max attribute to also group the columns in the targetSheet
target_sheet.column_dimensions[key].width = copy(source_sheet.column_dimensions[key].width) # set width for every column target_sheet.column_dimensions[key].max = copy(source_sheet.column_dimensions[
key].max) # https://stackoverflow.com/questions/36417278/openpyxl-can-not-read-consecutive-hidden-columns discussed the issue. Note that this is also the case for the width, not onl;y the hidden property
target_sheet.column_dimensions[key].width = copy(
source_sheet.column_dimensions[key].width) # set width for every column
target_sheet.column_dimensions[key].hidden = copy(source_sheet.column_dimensions[key].hidden) target_sheet.column_dimensions[key].hidden = copy(source_sheet.column_dimensions[key].hidden)
@@ -534,11 +535,9 @@ def copy_cells(source_sheet, target_sheet):
source_cell = cell source_cell = cell
if isinstance(source_cell, openpyxl.cell.read_only.EmptyCell): if isinstance(source_cell, openpyxl.cell.read_only.EmptyCell):
continue continue
target_cell = target_sheet.cell(column=c+1, row=r+1) target_cell = target_sheet.cell(column=c + 1, row=r + 1)
target_cell._value = source_cell._value target_cell._value = source_cell._value
target_cell.data_type = source_cell.data_type target_cell.data_type = source_cell.data_type
if source_cell.has_style: if source_cell.has_style:
target_cell.font = copy(source_cell.font) target_cell.font = copy(source_cell.font)
target_cell.border = copy(source_cell.border) target_cell.border = copy(source_cell.border)
@@ -546,15 +545,13 @@ def copy_cells(source_sheet, target_sheet):
target_cell.number_format = copy(source_cell.number_format) target_cell.number_format = copy(source_cell.number_format)
target_cell.protection = copy(source_cell.protection) target_cell.protection = copy(source_cell.protection)
target_cell.alignment = copy(source_cell.alignment) target_cell.alignment = copy(source_cell.alignment)
if not isinstance(source_cell, openpyxl.cell.ReadOnlyCell) and source_cell.hyperlink: if not isinstance(source_cell, openpyxl.cell.ReadOnlyCell) and source_cell.hyperlink:
target_cell._hyperlink = copy(source_cell.hyperlink) target_cell._hyperlink = copy(source_cell.hyperlink)
if not isinstance(source_cell, openpyxl.cell.ReadOnlyCell) and source_cell.comment: if not isinstance(source_cell, openpyxl.cell.ReadOnlyCell) and source_cell.comment:
target_cell.comment = copy(source_cell.comment) target_cell.comment = copy(source_cell.comment)
def list_str_comparator(input_str:str, listy: List[str], mode: Literal["starts_with", "contains"]) -> bool: def list_str_comparator(input_str: str, listy: List[str], mode: Literal["starts_with", "contains"]) -> bool:
match mode: match mode:
case "starts_with": case "starts_with":
if any([input_str.startswith(item) for item in listy]): if any([input_str.startswith(item) for item in listy]):
@@ -567,6 +564,7 @@ def list_str_comparator(input_str:str, listy: List[str], mode: Literal["starts_w
else: else:
return False return False
def sort_dict_by_list(dictionary: dict, order_list: list) -> dict: def sort_dict_by_list(dictionary: dict, order_list: list) -> dict:
output = OrderedDict() output = OrderedDict()
for item in order_list: for item in order_list:
@@ -602,14 +600,12 @@ def setup_lookup(func):
elif v is not None: elif v is not None:
sanitized_kwargs[k] = v sanitized_kwargs[k] = v
return func(*args, **sanitized_kwargs) return func(*args, **sanitized_kwargs)
return wrapper return wrapper
def check_object_in_manager(manager: list, object_name: object) -> Tuple[Any, bool]: def check_object_in_manager(manager: list, object_name: object) -> Tuple[Any, bool]:
if manager is None: if manager is None:
return None, False return None, False
# logger.debug(f"Manager: {manager}, aliases: {manager.aliases}, Key: {object_name}")
if object_name in manager.aliases: if object_name in manager.aliases:
return manager, True return manager, True
relationships = [getattr(manager.__class__, item) for item in dir(manager.__class__) relationships = [getattr(manager.__class__, item) for item in dir(manager.__class__)
@@ -617,21 +613,17 @@ def check_object_in_manager(manager: list, object_name: object) -> Tuple[Any, bo
relationships = [item for item in relationships if isinstance(item.property, _RelationshipDeclared)] relationships = [item for item in relationships if isinstance(item.property, _RelationshipDeclared)]
for relationship in relationships: for relationship in relationships:
if relationship.key == object_name and "association" not in relationship.key: if relationship.key == object_name and "association" not in relationship.key:
logger.debug(f"Checking {relationship.key}")
try: try:
rel_obj = getattr(manager, relationship.key) rel_obj = getattr(manager, relationship.key)
if rel_obj is not None: if rel_obj is not None:
logger.debug(f"Returning {rel_obj}")
return rel_obj, False return rel_obj, False
except AttributeError: except AttributeError:
pass pass
if "association" in relationship.key: if "association" in relationship.key:
try: try:
logger.debug(f"Checking association {relationship.key}")
rel_obj = next((getattr(item, object_name) for item in getattr(manager, relationship.key) rel_obj = next((getattr(item, object_name) for item in getattr(manager, relationship.key)
if getattr(item, object_name) is not None), None) if getattr(item, object_name) is not None), None)
if rel_obj is not None: if rel_obj is not None:
logger.debug(f"Returning {rel_obj}")
return rel_obj, False return rel_obj, False
except AttributeError: except AttributeError:
pass pass
@@ -862,7 +854,6 @@ def check_authorization(func):
report.add_result( report.add_result(
Result(owner=func.__str__(), code=1, msg=error_msg, status="warning")) Result(owner=func.__str__(), code=1, msg=error_msg, status="warning"))
return report, kwargs return report, kwargs
return wrapper return wrapper
@@ -888,7 +879,6 @@ def under_development(func):
Result(owner=func.__str__(), code=1, msg=error_msg, Result(owner=func.__str__(), code=1, msg=error_msg,
status="warning")) status="warning"))
return report return report
return wrapper return wrapper
@@ -906,7 +896,6 @@ def report_result(func):
@wraps(func) @wraps(func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
# logger.info(f"Report result being called by {func.__name__}")
output = func(*args, **kwargs) output = func(*args, **kwargs)
match output: match output:
case Report(): case Report():
@@ -931,6 +920,7 @@ def report_result(func):
logger.error(f"Problem reporting due to {e}") logger.error(f"Problem reporting due to {e}")
logger.error(result.msg) logger.error(result.msg)
if output: if output:
logger.info(f"Report result being called by {func.__name__}")
if is_list_etc(output): if is_list_etc(output):
true_output = tuple(item for item in output if not isinstance(item, Report)) true_output = tuple(item for item in output if not isinstance(item, Report))
if len(true_output) == 1: if len(true_output) == 1:
@@ -943,7 +933,6 @@ def report_result(func):
else: else:
true_output = None true_output = None
return true_output return true_output
return wrapper return wrapper
@@ -962,11 +951,32 @@ def is_list_etc(object):
def create_holidays_for_year(year: int | None = None) -> List[date]: def create_holidays_for_year(year: int | None = None) -> List[date]:
def find_nth_monday(year, month, occurence: int | None = None, day: int | None = None): """
if not occurence: Gives stat holidays for the input year.
occurence = 1
Args:
year (int | None, optional): The input year as an integer. Defaults to None.
Returns:
List[date]
"""
def find_nth_monday(year, month, occurrence: int | None = None, day: int | None = None) -> date:
"""
Gets the nth (eg 2nd) monday of the given month.
Args:
year (int): The year the month occurs in.
month (int): The month of interest.
occurrence (int): The n in nth.
day (int): The day of the month to start after.
Returns:
date
"""
if not occurrence:
occurrence = 1
if not day: if not day:
day = occurence * 7 day = occurrence * 7
max_days = (date(2012, month + 1, 1) - date(2012, month, 1)).days max_days = (date(2012, month + 1, 1) - date(2012, month, 1)).days
if day > max_days: if day > max_days:
day = max_days day = max_days
@@ -977,17 +987,16 @@ def create_holidays_for_year(year: int | None = None) -> List[date]:
offset = -d.weekday() # weekday == 0 means Monday offset = -d.weekday() # weekday == 0 means Monday
output = d + timedelta(offset) output = d + timedelta(offset)
return output.date() return output.date()
if not year: if not year:
year = date.today().year year = date.today().year
# NOTE: Includes New Year's day for next year. # NOTE: Static holidays. Includes New Year's day for next year.
holidays = [date(year, 1, 1), date(year, 7, 1), date(year, 9, 30), holidays = [date(year, 1, 1), date(year, 7, 1), date(year, 9, 30),
date(year, 11, 11), date(year, 12, 25), date(year, 12, 26), date(year, 11, 11), date(year, 12, 25), date(year, 12, 26),
date(year + 1, 1, 1)] date(year + 1, 1, 1)]
# NOTE: Labour Day # NOTE: Labour Day
holidays.append(find_nth_monday(year, 9)) holidays.append(find_nth_monday(year, 9))
# NOTE: Thanksgiving # NOTE: Thanksgiving
holidays.append(find_nth_monday(year, 10, occurence=2)) holidays.append(find_nth_monday(year, 10, occurrence=2))
# NOTE: Victoria Day # NOTE: Victoria Day
holidays.append(find_nth_monday(year, 5, day=25)) holidays.append(find_nth_monday(year, 5, day=25))
# NOTE: Easter, etc # NOTE: Easter, etc
@@ -1007,7 +1016,6 @@ def check_dictionary_inclusion_equality(listo: List[dict] | dict, dicto: dict) -
Returns: Returns:
bool: True if dicto is equal to any dictionary in the list. bool: True if dicto is equal to any dictionary in the list.
""" """
# logger.debug(f"Comparing: {listo} and {dicto}")
if isinstance(dicto, list) and isinstance(listo, list): if isinstance(dicto, list) and isinstance(listo, list):
return listo == dicto return listo == dicto
elif isinstance(dicto, dict) and isinstance(listo, dict): elif isinstance(dicto, dict) and isinstance(listo, dict):
@@ -1018,22 +1026,39 @@ def check_dictionary_inclusion_equality(listo: List[dict] | dict, dicto: dict) -
raise TypeError(f"Unsupported variable: {type(listo)}") raise TypeError(f"Unsupported variable: {type(listo)}")
def flatten_list(input_list: list): def flatten_list(input_list: list) -> list:
"""
Takes nested lists and returns a single flat list.
Args:
input_list (list): input nested list.
Returns:
list:
"""
return list(itertools.chain.from_iterable(input_list)) return list(itertools.chain.from_iterable(input_list))
def sanitize_object_for_json(input_dict: dict) -> dict: def sanitize_object_for_json(input_dict: dict) -> dict:
"""
Takes an object and makes sure its components can be converted to JSON
Args:
input_dict (dict): Dictionary of interest
Returns:
dict:
"""
if not isinstance(input_dict, dict): if not isinstance(input_dict, dict):
match input_dict: match input_dict:
case int() | float() | bool(): case int() | float() | bool():
pass pass
case _: case _:
try: try:
js = json.dumps(input_dict) input_dict = json.dumps(input_dict)
except TypeError: except TypeError:
input_dict = str(input_dict) input_dict = str(input_dict)
return input_dict return input_dict
# return input_dict
output = {} output = {}
for key, value in input_dict.items(): for key, value in input_dict.items():
match value: match value:
@@ -1041,22 +1066,37 @@ def sanitize_object_for_json(input_dict: dict) -> dict:
value = [sanitize_object_for_json(object) for object in value] value = [sanitize_object_for_json(object) for object in value]
case dict(): case dict():
value = sanitize_object_for_json(value) value = sanitize_object_for_json(value)
case _: case _:
try: try:
js = json.dumps(value) value = json.dumps(value)
except TypeError: except TypeError:
value = str(value) value = str(value)
output[key] = value output[key] = value
return output return output
def create_plate_grid(rows: int, columns: int): def create_plate_grid(rows: int, columns: int) -> dict:
matrix = np.array([[0 for yyy in range(1, columns + 1)] for xxx in range(1, rows + 1)]) """
return {iii: (item[0][1]+1, item[0][0]+1) for iii, item in enumerate(np.ndenumerate(matrix), start=1)} Makes an x by y array to represent a plate.
Args:
rows (int): Number of rows.
columns (int): Number of columns
Returns:
dict: cell number : (row, column)
"""
# NOTE: columns/rows
# matrix = np.array([[0 for yyy in range(1, columns + 1)] for xxx in range(1, rows + 1)])
# NOTE: rows/columns
matrix = np.array([[0 for xxx in range(1, rows + 1)] for yyy in range(1, columns + 1)])
return {iii: (item[0][1] + 1, item[0][0] + 1) for iii, item in enumerate(np.ndenumerate(matrix), start=1)}
class classproperty(property): class classproperty(property):
"""
Allows for properties on classes as well as objects.
"""
def __get__(self, owner_self, owner_cls): def __get__(self, owner_self, owner_cls):
return self.fget(owner_cls) return self.fget(owner_cls)
@@ -1396,6 +1436,16 @@ class Settings(BaseSettings, extra="allow"):
@classmethod @classmethod
def get_alembic_db_path(cls, alembic_path, mode=Literal['path', 'schema', 'user', 'pass']) -> Path | str: def get_alembic_db_path(cls, alembic_path, mode=Literal['path', 'schema', 'user', 'pass']) -> Path | str:
"""
Retrieves database variables from alembic.ini file.
Args:
alembic_path (Any): Path of the alembic.ini file.
mode (Literal['path', 'schema', 'user', 'pass']): Variable of interest.
Returns:
Path | str
"""
c = ConfigParser() c = ConfigParser()
c.read(alembic_path) c.read(alembic_path)
url = c['alembic']['sqlalchemy.url'] url = c['alembic']['sqlalchemy.url']

View File

@@ -32,8 +32,10 @@ a = Analysis(
binaries=[], binaries=[],
datas=[ datas=[
("src\\config.yml", "files"), ("src\\config.yml", "files"),
("src\\submissions\\templates\\*", "files\\templates"), ("src\\submissions\\templates\\*.html", "files\\templates"),
("src\\submissions\\templates\\css\\*", "files\\templates\\css"), ("src\\submissions\\templates\\css\\*.css", "files\\templates\\css"),
("src\\submissions\\templates\\js\\*.js", "files\\templates\\js"),
("src\\submissions\\templates\\support\\*", "files\\templates\\support"),
("docs\\build", "files\\docs"), ("docs\\build", "files\\docs"),
("src\\submissions\\resources\\*", "files\\resources"), ("src\\submissions\\resources\\*", "files\\resources"),
("alembic.ini", "files"), ("alembic.ini", "files"),
@@ -51,12 +53,32 @@ a = Analysis(
) )
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
#exe = EXE(
# pyz,
# a.scripts,
# a.binaries,
# a.datas,
# [],
# name=f"{__project__}_{__version__}",
# debug=True,
# bootloader_ignore_signals=False,
# strip=False,
# upx=True,
# upx_exclude=[],
# runtime_tmpdir=None,
# console=True,
# disable_windowed_traceback=False,
# argv_emulation=False,
# target_arch=None,
# codesign_identity=None,
# entitlements_file=None,
#)
exe = EXE( exe = EXE(
pyz, pyz,
a.scripts, a.scripts,
[], [],
exclude_binaries=True, exclude_binaries=True,
name=f"{__project__}_{__version__}", name=f"{__project__}_{__version__}_2",
debug=True, debug=True,
bootloader_ignore_signals=False, bootloader_ignore_signals=False,
strip=False, strip=False,