ReagentLot add/edit updated.
This commit is contained in:
@@ -2,11 +2,12 @@
|
||||
Contains all models for sqlalchemy
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys, logging, json
|
||||
import sys, logging, json, inspect
|
||||
from dateutil.parser import parse
|
||||
from jinja2 import TemplateNotFound
|
||||
from pandas import DataFrame
|
||||
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.orm import DeclarativeMeta, declarative_base, Query, Session, InstrumentedAttribute, ColumnProperty
|
||||
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
|
||||
|
||||
singles = ['id']
|
||||
# omni_removes = ["id", 'run', "omnigui_class_dict", "omnigui_instance_dict"]
|
||||
# omni_sort = ["name"]
|
||||
# omni_inheritable = []
|
||||
searchables = []
|
||||
|
||||
_misc_info = Column(JSON)
|
||||
|
||||
@@ -155,6 +152,33 @@ class BaseClass(Base):
|
||||
except AttributeError:
|
||||
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
|
||||
def get_default_info(cls, *args) -> dict | list | str:
|
||||
"""
|
||||
@@ -296,7 +320,6 @@ class BaseClass(Base):
|
||||
except AttributeError:
|
||||
check = False
|
||||
if check:
|
||||
logger.debug("Got uselist")
|
||||
try:
|
||||
query = query.filter(attr.contains(v))
|
||||
except ArgumentError:
|
||||
@@ -358,16 +381,14 @@ class BaseClass(Base):
|
||||
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))
|
||||
for key in dir(self.__class__) if
|
||||
isinstance(getattr(self.__class__, key), InstrumentedAttribute) and key not in self.omni_removes
|
||||
}
|
||||
for key in self.get_omni_sort()}
|
||||
for k, v in dicto.items():
|
||||
try:
|
||||
v['instance_attr'] = v['instance_attr'].name
|
||||
except AttributeError:
|
||||
continue
|
||||
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:
|
||||
logger.error(f"Could not sort {self.__class__.__name__} by list due to :{e}")
|
||||
try:
|
||||
@@ -418,7 +439,7 @@ class BaseClass(Base):
|
||||
try:
|
||||
template = env.get_template(temp_name)
|
||||
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")
|
||||
return template
|
||||
|
||||
@@ -505,7 +526,6 @@ class BaseClass(Base):
|
||||
existing = self.__getattribute__(key)
|
||||
# NOTE: This is causing problems with removal of items from lists. Have to overhaul it.
|
||||
if existing is not None:
|
||||
logger.debug(f"{key} Existing: {existing}, incoming: {value}")
|
||||
if isinstance(value, list):
|
||||
value = value
|
||||
else:
|
||||
@@ -626,7 +646,7 @@ class BaseClass(Base):
|
||||
from backend.validators import pydant
|
||||
if not pyd_model_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:
|
||||
pyd = getattr(pydant, pyd_model_name)
|
||||
except AttributeError:
|
||||
|
||||
@@ -194,6 +194,7 @@ class Reagent(BaseClass, LogMixin):
|
||||
Concrete reagent instance
|
||||
"""
|
||||
|
||||
skip_on_edit = False
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
reagentrole = relationship("ReagentRole", back_populates="reagent",
|
||||
secondary=reagentrole_reagent) #: joined parent ReagentRole
|
||||
@@ -319,15 +320,7 @@ class Reagent(BaseClass, LogMixin):
|
||||
except AttributeError as 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
|
||||
def add_edit_tooltips(self):
|
||||
@@ -418,6 +411,15 @@ class ReagentLot(BaseClass):
|
||||
pass
|
||||
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):
|
||||
"""
|
||||
@@ -952,6 +954,7 @@ class Procedure(BaseClass):
|
||||
output['sample'] = active_samples + inactive_samples
|
||||
output['reagent'] = [reagent.details_dict() for reagent in output['procedurereagentlotassociation']]
|
||||
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['run'] = self.run.name
|
||||
output['excluded'] += self.get_default_info("details_ignore")
|
||||
@@ -963,14 +966,6 @@ class Procedure(BaseClass):
|
||||
def to_pydantic(self, **kwargs):
|
||||
from backend.validators.pydant import PydReagent
|
||||
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]
|
||||
reagents = []
|
||||
for reagent in output.reagent:
|
||||
@@ -985,6 +980,7 @@ class Procedure(BaseClass):
|
||||
output.result = [item.to_pydantic() for item in self.results]
|
||||
output.sample_results = flatten_list(
|
||||
[[result.to_pydantic() for result in item.results] for item in self.proceduresampleassociation])
|
||||
|
||||
return output
|
||||
|
||||
def create_proceduresampleassociations(self, sample):
|
||||
@@ -1607,7 +1603,7 @@ class Equipment(BaseClass, LogMixin):
|
||||
from backend.validators.pydant import PydEquipment
|
||||
creation_dict = self.details_dict()
|
||||
processes = self.get_processes(equipmentrole=equipmentrole)
|
||||
creation_dict['process'] = processes
|
||||
creation_dict['processes'] = processes
|
||||
creation_dict['equipmentrole'] = equipmentrole or creation_dict['equipmentrole']
|
||||
return PydEquipment(**creation_dict)
|
||||
|
||||
@@ -2187,6 +2183,7 @@ class ProcedureEquipmentAssociation(BaseClass):
|
||||
output.update(relevant)
|
||||
output['misc_info'] = misc
|
||||
output['equipment_role'] = self.equipmentrole
|
||||
output['processes'] = [item for item in self.equipment.get_processes(equipmentrole=output['equipment_role'])]
|
||||
try:
|
||||
output['processversion'] = self.processversion.details_dict()
|
||||
except AttributeError:
|
||||
|
||||
@@ -116,7 +116,6 @@ class ClientSubmission(BaseClass, LogMixin):
|
||||
if start_date is not None:
|
||||
start_date = cls.rectify_query_date(start_date)
|
||||
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))
|
||||
# NOTE: by rsl number (returns only a single value)
|
||||
match submitter_plate_id:
|
||||
@@ -303,7 +302,6 @@ class ClientSubmission(BaseClass, LogMixin):
|
||||
return {item: self.__getattribute__(item.lower().replace(" ", "_")) for item in names}
|
||||
|
||||
def add_run(self, obj):
|
||||
logger.debug("Add Run")
|
||||
from frontend.widgets.sample_checker import SampleChecker
|
||||
samples = [sample.to_pydantic() for sample in self.clientsubmissionsampleassociation]
|
||||
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}
|
||||
else:
|
||||
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):
|
||||
st = submissiontype
|
||||
else:
|
||||
st = cls.get_submission_type(submissiontype)
|
||||
if st is None:
|
||||
logger.error("No default info for Run.")
|
||||
# logger.error("No default info for Run.")
|
||||
pass
|
||||
else:
|
||||
output['submissiontype'] = st.name
|
||||
for k, v in st.defaults.items():
|
||||
|
||||
@@ -36,16 +36,17 @@ class DefaultWriter(object):
|
||||
value = value['name']
|
||||
except (KeyError, ValueError):
|
||||
return
|
||||
# logger.debug(f"Value type: {type(value)}")
|
||||
match value:
|
||||
case x if issubclass(value.__class__, BaseClass):
|
||||
value = value.name
|
||||
case x if issubclass(value.__class__, PydBaseClass):
|
||||
logger.warning(f"PydBaseClass: {value}")
|
||||
value = value.name
|
||||
case bytes() | list():
|
||||
value = None
|
||||
case datetime() | date():
|
||||
value = value.strftime("%Y-%m-%d")
|
||||
|
||||
case _:
|
||||
value = str(value)
|
||||
return value
|
||||
@@ -80,9 +81,19 @@ class DefaultWriter(object):
|
||||
self.worksheet = self.prewrite(self.worksheet, 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)
|
||||
logger.debug(f"Rows for {self.__class__.__name__}:\tstart: {self.start_row}, end: {self.end_row}")
|
||||
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):
|
||||
if all([item.value is None for item in row]):
|
||||
return iii
|
||||
@@ -135,6 +146,7 @@ class DefaultKEYVALUEWriter(DefaultWriter):
|
||||
dictionary = sort_dict_by_list(dictionary=dictionary, order_list=self.key_order)
|
||||
for ii, (k, v) in enumerate(dictionary.items(), start=self.start_row):
|
||||
value = self.stringify_value(value=v)
|
||||
logger.debug(f"{self.__class__.__name__} attempting to write {value}")
|
||||
if value is None:
|
||||
continue
|
||||
self.worksheet.cell(column=1, row=ii, value=self.prettify_key(k))
|
||||
@@ -159,7 +171,9 @@ class DefaultTABLEWriter(DefaultWriter):
|
||||
return row_count
|
||||
|
||||
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,
|
||||
mode: Literal["submission", "procedure"] = "submission"): #, column_names):
|
||||
@@ -206,6 +220,7 @@ class DefaultTABLEWriter(DefaultWriter):
|
||||
value = object.improved_dict()[header.lower().replace(" ", "_")]
|
||||
except (AttributeError, KeyError):
|
||||
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 = self.postwrite(self.worksheet)
|
||||
return workbook
|
||||
|
||||
@@ -15,7 +15,7 @@ class ProcedureInfoWriter(DefaultKEYVALUEWriter):
|
||||
header_order = []
|
||||
exclude = ['control', 'equipment', 'excluded', 'id', 'misc_info', 'plate_map', 'possible_kits',
|
||||
'procedureequipmentassociation', 'procedurereagentassociation', 'proceduresampleassociation', 'proceduretipsassociation', 'reagent',
|
||||
'reagentrole', 'results', 'sample', 'tips']
|
||||
'reagentrole', 'results', 'sample', 'tips', 'reagentlot']
|
||||
|
||||
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,
|
||||
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
|
||||
|
||||
|
||||
class ProcedureReagentWriter(DefaultTABLEWriter):
|
||||
|
||||
exclude = ["id", "comments", "missing"]
|
||||
header_order = ["reagentrole", "name", "lot", "expiry"]
|
||||
exclude = ["id", "comments", "missing", "active", "name"]
|
||||
header_order = ["reagentrole", "reagent_name", "lot", "expiry"]
|
||||
|
||||
def __init__(self, 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
|
||||
|
||||
def write_to_workbook(self, workbook: Workbook, sheet: str | None = None,
|
||||
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
|
||||
|
||||
|
||||
@@ -52,12 +52,12 @@ class ProcedureEquipmentWriter(DefaultTABLEWriter):
|
||||
|
||||
def __init__(self, pydant_obj, range_dict: dict | None = None, *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
|
||||
|
||||
def write_to_workbook(self, workbook: Workbook, sheet: str | None = None,
|
||||
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
|
||||
|
||||
|
||||
@@ -68,10 +68,10 @@ class ProcedureSampleWriter(DefaultTABLEWriter):
|
||||
|
||||
def __init__(self, pydant_obj, range_dict: dict | None = None, *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")
|
||||
|
||||
def write_to_workbook(self, workbook: Workbook, sheet: str | None = None,
|
||||
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
|
||||
|
||||
@@ -66,19 +66,19 @@ class DefaultProcedureManager(DefaultManager):
|
||||
except AttributeError:
|
||||
reagent_writer = procedure_writers.ProcedureReagentWriter
|
||||
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:
|
||||
equipment_writer = getattr(procedure_writers, f"{self.proceduretype.name.replace(' ', '')}EquipmentWriter")
|
||||
except AttributeError:
|
||||
equipment_writer = procedure_writers.ProcedureEquipmentWriter
|
||||
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:
|
||||
sample_writer = getattr(procedure_writers, f"{self.proceduretype.name.replace(' ', '')}SampleWriter")
|
||||
except AttributeError:
|
||||
sample_writer = procedure_writers.ProcedureSampleWriter
|
||||
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.
|
||||
for result in self.pyd.result:
|
||||
Writer = getattr(results_writers, f"{result.result_type}InfoWriter")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -321,7 +321,7 @@ class PydTips(PydBaseClass):
|
||||
SubmissionTipsAssociation: Association between queried tips and procedure
|
||||
"""
|
||||
report = Report()
|
||||
tips = TipsLot.query(name=self.name, limit=1)
|
||||
tips = TipsLot.query(lot=self.lot, limit=1)
|
||||
return tips, report
|
||||
|
||||
|
||||
@@ -329,7 +329,8 @@ class PydEquipment(PydBaseClass):
|
||||
asset_number: str
|
||||
name: str
|
||||
nickname: str | None
|
||||
process: List[PydProcess] | PydProcess | None
|
||||
processes: List[PydProcess] | PydProcess | None
|
||||
processversion: PydProcess | None
|
||||
equipmentrole: str | PydEquipmentRole | None
|
||||
tips: List[PydTips] | PydTips | None = Field(default=[])
|
||||
|
||||
@@ -347,7 +348,7 @@ class PydEquipment(PydBaseClass):
|
||||
value = value.name
|
||||
return value
|
||||
|
||||
@field_validator('process', mode='before')
|
||||
@field_validator('processes', mode='before')
|
||||
@classmethod
|
||||
def process_to_pydantic(cls, value, values):
|
||||
if isinstance(value, GeneratorType):
|
||||
@@ -355,16 +356,25 @@ class PydEquipment(PydBaseClass):
|
||||
value = convert_nans_to_nones(value)
|
||||
if not value:
|
||||
value = []
|
||||
if isinstance(value, ProcessVersion):
|
||||
match value:
|
||||
case ProcessVersion():
|
||||
value = value.to_pydantic(pyd_model_name="PydProcess")
|
||||
else:
|
||||
case _:
|
||||
try:
|
||||
d: Process = next((process for process in value if values.data['name'] in [item.name for item in process.equipment]), None)
|
||||
if d:
|
||||
value = d.to_pydantic()
|
||||
# d: Process = next((process for process in value if values.data['name'] in [item.name for item in process.equipment]), None)
|
||||
for process in value:
|
||||
match process:
|
||||
case Process():
|
||||
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}")
|
||||
pass
|
||||
value = []
|
||||
return value
|
||||
|
||||
@field_validator('tips', mode='before')
|
||||
@@ -386,7 +396,7 @@ class PydEquipment(PydBaseClass):
|
||||
value = d.to_pydantic()
|
||||
except AttributeError as e:
|
||||
logger.error(f"Process Validation error due to {e}")
|
||||
pass
|
||||
value = []
|
||||
return value
|
||||
|
||||
@report_result
|
||||
@@ -1347,6 +1357,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
|
||||
|
||||
def to_sql(self, new: bool = False):
|
||||
from backend.db.models import RunSampleAssociation, ProcedureSampleAssociation
|
||||
logger.debug(f"incoming pyd: {pformat([item.__dict__ for item in self.equipment])}")
|
||||
if new:
|
||||
sql = Procedure()
|
||||
else:
|
||||
@@ -1408,6 +1419,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
|
||||
procedure_rank=sample.procedure_rank)
|
||||
for equipment in self.equipment:
|
||||
equip, _ = equipment.to_sql()
|
||||
logger.debug(f"Equipment:\n{pformat(equip.__dict__)}")
|
||||
if isinstance(equipment.process, list):
|
||||
equipment.process = equipment.process[0]
|
||||
if isinstance(equipment.tips, list):
|
||||
@@ -1420,10 +1432,13 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
|
||||
equipmentrole=equip.equipmentrole[0])
|
||||
process = equipment.process.to_sql()
|
||||
equip_assoc.processversion = process
|
||||
logger.debug(f"Tips: {type(equipment.tips)}")
|
||||
try:
|
||||
tipslot = equipment.tips.to_sql()
|
||||
logger.debug(f"Tipslot: {tipslot.__dict__}")
|
||||
except AttributeError:
|
||||
tipslot = None
|
||||
|
||||
equip_assoc.tipslot = tipslot
|
||||
return sql, None
|
||||
|
||||
@@ -1560,6 +1575,13 @@ class PydClientSubmission(PydBaseClass):
|
||||
value = int(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):
|
||||
"""
|
||||
Converts this instance into a frontend.widgets.submission_widget.SubmissionFormWidget
|
||||
|
||||
@@ -35,7 +35,7 @@ class ConcentrationsChart(CustomFigure):
|
||||
color="positive", color_discrete_map={"positive": "red", "negative": "green", "sample":"orange"}
|
||||
)
|
||||
except (ValueError, AttributeError) as e:
|
||||
logger.error(f"Error constructing chart: {e}")
|
||||
# logger.error(f"Error constructing chart: {e}")
|
||||
scatter = px.scatter()
|
||||
# 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"))
|
||||
|
||||
@@ -13,7 +13,7 @@ from PyQt6.QtGui import QAction
|
||||
from pathlib import Path
|
||||
from markdown import markdown
|
||||
from pandas import ExcelWriter
|
||||
from backend.db.models import Reagent
|
||||
from backend.db.models import ReagentLot
|
||||
from tools import (
|
||||
check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user,
|
||||
under_development
|
||||
@@ -181,7 +181,7 @@ class App(QMainWindow):
|
||||
|
||||
@check_authorization
|
||||
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()
|
||||
|
||||
def update_data(self):
|
||||
|
||||
@@ -3,7 +3,7 @@ A widget to handle adding/updating any database object.
|
||||
"""
|
||||
from datetime import date
|
||||
from pprint import pformat
|
||||
from typing import Any, Tuple
|
||||
from typing import Any, Tuple, List
|
||||
from pydantic import BaseModel
|
||||
from PyQt6.QtWidgets import (
|
||||
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
|
||||
import logging
|
||||
from sqlalchemy.orm.relationships import _RelationshipDeclared
|
||||
from backend.db.models import BaseClass
|
||||
from backend.validators.pydant import PydBaseClass
|
||||
from tools import Report, report_result
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
@@ -20,20 +22,19 @@ logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
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)
|
||||
# logger.debug(f"Managers: {managers}")
|
||||
logger.debug(f"Disable = {disabled}")
|
||||
self.instance = instance
|
||||
self.object_type = instance.__class__
|
||||
self.managers = managers
|
||||
# logger.debug(f"Managers: {managers}")
|
||||
self.layout = QGridLayout(self)
|
||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
self.buttonBox = QDialogButtonBox(QBtn)
|
||||
self.buttonBox.accepted.connect(self.accept)
|
||||
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}
|
||||
logger.debug(f"Fields: {pformat(fields)}")
|
||||
# NOTE: Move 'name' to the front
|
||||
try:
|
||||
fields = {'name': fields.pop('name'), **fields}
|
||||
@@ -41,13 +42,13 @@ class AddEdit(QDialog):
|
||||
pass
|
||||
height_counter = 0
|
||||
for key, field in fields.items():
|
||||
disable = key in disabled
|
||||
try:
|
||||
value = getattr(self.instance, key)
|
||||
except AttributeError:
|
||||
value = None
|
||||
try:
|
||||
logger.debug(f"{key} property: {type(field['class_attr'].property)}")
|
||||
widget = EditProperty(self, key=key, column_type=field, value=value)
|
||||
widget = EditProperty(self, key=key, column_type=field, value=value, disable=disable)
|
||||
except AttributeError as e:
|
||||
logger.error(f"Problem setting widget {key}: {e}")
|
||||
continue
|
||||
@@ -55,7 +56,7 @@ class AddEdit(QDialog):
|
||||
self.layout.addWidget(widget, self.layout.rowCount(), 0)
|
||||
height_counter += 1
|
||||
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.setLayout(self.layout)
|
||||
|
||||
@@ -64,11 +65,8 @@ class AddEdit(QDialog):
|
||||
report = Report()
|
||||
parsed = {result[0].strip(":"): result[1] for result in
|
||||
[item.parse_form() for item in self.findChildren(EditProperty)] if result[0]}
|
||||
# logger.debug(f"Parsed form: {parsed}")
|
||||
model = self.object_type.pydantic_model
|
||||
# logger.debug(f"Model type: {model.__name__}")
|
||||
if model.__name__ == "PydElastic":
|
||||
# logger.debug(f"We have an elastic model.")
|
||||
parsed['instance'] = self.instance
|
||||
# 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.
|
||||
@@ -78,8 +76,9 @@ class AddEdit(QDialog):
|
||||
|
||||
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)
|
||||
logger.debug(f"Widget column type for {key}: {column_type}")
|
||||
self.name = key
|
||||
self.label = QLabel(key.title().replace("_", " "))
|
||||
self.layout = QGridLayout()
|
||||
@@ -88,6 +87,7 @@ class EditProperty(QWidget):
|
||||
self.property_class = column_type['class_attr'].property.entity.class_
|
||||
except AttributeError:
|
||||
self.property_class = None
|
||||
logger.debug(f"Property class: {self.property_class}")
|
||||
try:
|
||||
self.is_list = column_type['class_attr'].property.uselist
|
||||
except AttributeError:
|
||||
@@ -96,23 +96,26 @@ class EditProperty(QWidget):
|
||||
case ColumnProperty():
|
||||
self.column_property_set(column_type, value=value)
|
||||
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)
|
||||
else:
|
||||
return
|
||||
case _:
|
||||
logger.error(f"{column_type} not a supported type.")
|
||||
return
|
||||
self.widget.setDisabled(disable)
|
||||
self.layout.addWidget(self.label, 0, 0, 1, 1)
|
||||
self.layout.addWidget(self.widget, 0, 1, 1, 3)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def relationship_property_set(self, relationship, value=None):
|
||||
self.widget = QComboBox()
|
||||
# logger.debug(self.parent().managers)
|
||||
for manager in self.parent().managers:
|
||||
if self.name in manager.aliases:
|
||||
# logger.debug(f"Name: {self.name} is in aliases: {manager.aliases}")
|
||||
choices = [manager.name]
|
||||
self.widget.setEnabled(False)
|
||||
break
|
||||
@@ -127,11 +130,17 @@ class EditProperty(QWidget):
|
||||
if isinstance(instance_value, list):
|
||||
instance_value = next((item.name for item in instance_value), None)
|
||||
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)))
|
||||
self.widget.addItems(choices)
|
||||
|
||||
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:
|
||||
case String():
|
||||
if value is None:
|
||||
@@ -176,7 +185,6 @@ class EditProperty(QWidget):
|
||||
check = self.widget
|
||||
except AttributeError:
|
||||
return None, None
|
||||
# match self.widget
|
||||
match check:
|
||||
case QLineEdit():
|
||||
value = self.widget.text()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -72,18 +72,14 @@ class SearchBox(QDialog):
|
||||
self.object_type = self.original_type
|
||||
else:
|
||||
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}")
|
||||
# logger.debug(f"Original type: {self.original_type} - {self.original_type.searchables}")
|
||||
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']}")
|
||||
for item in self.object_type.get_searchables():
|
||||
if item in [thing for thing in search_fields]:
|
||||
continue
|
||||
else:
|
||||
search_fields.append(item)
|
||||
logger.debug(f"Search fields: {search_fields}")
|
||||
for iii, searchable in enumerate(search_fields):
|
||||
widget = FieldSearch(parent=self, label=searchable['label'], field_name=searchable['field'])
|
||||
widget.setObjectName(searchable['field'])
|
||||
widget = FieldSearch(parent=self, label=searchable, field_name=searchable)
|
||||
widget.setObjectName(searchable)
|
||||
self.layout.addWidget(widget, 1 + iii, 0)
|
||||
widget.search_widget.textChanged.connect(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)]
|
||||
except AttributeError:
|
||||
self.extras = extras
|
||||
# logger.debug(f"Extras: {self.extras}")
|
||||
|
||||
def setData(self, df: DataFrame) -> None:
|
||||
"""
|
||||
|
||||
@@ -163,4 +163,5 @@ class ProcedureCreation(QDialog):
|
||||
self.procedure.update_reagents(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry)
|
||||
|
||||
def return_sql(self, new: bool = False):
|
||||
return self.procedure.to_sql(new=new)
|
||||
output = self.procedure.to_sql(new=new)
|
||||
return output
|
||||
|
||||
@@ -264,7 +264,6 @@ class SubmissionComment(QDialog):
|
||||
"""
|
||||
|
||||
def __init__(self, parent, submission: Run) -> None:
|
||||
logger.debug(parent)
|
||||
super().__init__(parent)
|
||||
self.app = get_application_from_parent(parent)
|
||||
self.submission = submission
|
||||
@@ -284,7 +283,7 @@ class SubmissionComment(QDialog):
|
||||
self.layout.addWidget(self.buttonBox, alignment=Qt.AlignmentFlag.AlignBottom)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def parse_form(self) -> List[dict]:
|
||||
def parse_form(self) -> dict:
|
||||
"""
|
||||
Adds comment to procedure object.
|
||||
"""
|
||||
|
||||
@@ -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 backend.validators import PydReagent, PydClientSubmission, PydSample
|
||||
from backend.db import (
|
||||
ClientLab, SubmissionType, Reagent,
|
||||
ClientLab, SubmissionType, Reagent, ReagentLot,
|
||||
ReagentRole, ProcedureTypeReagentRoleAssociation, Run, ClientSubmission
|
||||
)
|
||||
from pprint import pformat
|
||||
@@ -137,7 +137,7 @@ class SubmissionFormContainer(QWidget):
|
||||
return report
|
||||
|
||||
@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.
|
||||
|
||||
@@ -149,7 +149,7 @@ class SubmissionFormContainer(QWidget):
|
||||
"""
|
||||
report = Report()
|
||||
if not instance:
|
||||
instance = Reagent()
|
||||
instance = ReagentLot()
|
||||
dlg = AddEdit(parent=self, instance=instance)
|
||||
if dlg.exec():
|
||||
reagent = dlg.parse_form()
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
'''
|
||||
"""
|
||||
Contains miscellaenous functions used by both frontend and backend.
|
||||
'''
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import builtins, importlib, time, logging, re, yaml, sys, os, stat, platform, getpass, json, numpy as np, pandas as pd
|
||||
import itertools
|
||||
import builtins, importlib, time, logging, re, yaml, sys, os, stat, platform, getpass, json, numpy as np, pandas as pd, \
|
||||
itertools, openpyxl
|
||||
from copy import copy
|
||||
from collections import OrderedDict
|
||||
from datetime import date, datetime, timedelta
|
||||
from json import JSONDecodeError
|
||||
from threading import Thread
|
||||
from inspect import getmembers, isfunction, stack
|
||||
from types import NoneType
|
||||
|
||||
from dateutil.easter import easter
|
||||
from dateutil.parser import parse
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from logging import handlers, Logger
|
||||
from pathlib import Path
|
||||
@@ -60,7 +58,6 @@ main_form_style = '''
|
||||
|
||||
page_size = 250
|
||||
|
||||
# micro_char = uni_char = "\u03BC"
|
||||
|
||||
def divide_chunks(input_list: list, chunk_count: int) -> Generator[Any, Any, None]:
|
||||
"""
|
||||
@@ -447,16 +444,18 @@ def jinja_template_loading() -> Environment:
|
||||
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):
|
||||
css_in = [css_in]
|
||||
env = jinja_template_loading()
|
||||
html_folder = Path(env.loader.__getattribute__("searchpath")[0])
|
||||
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):
|
||||
js_in = [js_in]
|
||||
js_in = ["details"] + js_in
|
||||
js_in = [project_path.joinpath("src", "submissions", "templates", "js", f"{j}.js") for j in js_in]
|
||||
env = jinja_template_loading()
|
||||
js_in = [html_folder.joinpath("js", f"{j}.js") for j in js_in]
|
||||
template = env.get_template(f"{template_name}.html")
|
||||
# template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
|
||||
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:
|
||||
with open(js, "r") as f:
|
||||
js_out.append(f.read())
|
||||
# logger.debug(f"Kwargs: {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 row, column
|
||||
|
||||
|
||||
# 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.
|
||||
import openpyxl
|
||||
from copy import copy
|
||||
|
||||
|
||||
|
||||
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.freeze_panes = copy(source_sheet.freeze_panes)
|
||||
|
||||
# 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: set row dimensions
|
||||
# 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)):
|
||||
target_sheet.row_dimensions[rn] = copy(source_sheet.row_dimensions[rn])
|
||||
|
||||
@@ -519,12 +517,15 @@ def copy_sheet_attributes(source_sheet, target_sheet):
|
||||
else:
|
||||
target_sheet.sheet_format.defaultColWidth = copy(source_sheet.sheet_format.defaultColWidth)
|
||||
|
||||
# set specific column width and hidden property
|
||||
# we cannot copy the entire column_dimensions attribute so we copy selected attributes
|
||||
# NOTE: set specific column width and hidden property
|
||||
# NOTE: we cannot copy the entire column_dimensions attribute so we copy selected attributes
|
||||
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].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].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].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)
|
||||
|
||||
|
||||
@@ -535,10 +536,8 @@ def copy_cells(source_sheet, target_sheet):
|
||||
if isinstance(source_cell, openpyxl.cell.read_only.EmptyCell):
|
||||
continue
|
||||
target_cell = target_sheet.cell(column=c + 1, row=r + 1)
|
||||
|
||||
target_cell._value = source_cell._value
|
||||
target_cell.data_type = source_cell.data_type
|
||||
|
||||
if source_cell.has_style:
|
||||
target_cell.font = copy(source_cell.font)
|
||||
target_cell.border = copy(source_cell.border)
|
||||
@@ -546,10 +545,8 @@ def copy_cells(source_sheet, target_sheet):
|
||||
target_cell.number_format = copy(source_cell.number_format)
|
||||
target_cell.protection = copy(source_cell.protection)
|
||||
target_cell.alignment = copy(source_cell.alignment)
|
||||
|
||||
if not isinstance(source_cell, openpyxl.cell.ReadOnlyCell) and source_cell.hyperlink:
|
||||
target_cell._hyperlink = copy(source_cell.hyperlink)
|
||||
|
||||
if not isinstance(source_cell, openpyxl.cell.ReadOnlyCell) and source_cell.comment:
|
||||
target_cell.comment = copy(source_cell.comment)
|
||||
|
||||
@@ -567,6 +564,7 @@ def list_str_comparator(input_str:str, listy: List[str], mode: Literal["starts_w
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def sort_dict_by_list(dictionary: dict, order_list: list) -> dict:
|
||||
output = OrderedDict()
|
||||
for item in order_list:
|
||||
@@ -602,14 +600,12 @@ def setup_lookup(func):
|
||||
elif v is not None:
|
||||
sanitized_kwargs[k] = v
|
||||
return func(*args, **sanitized_kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def check_object_in_manager(manager: list, object_name: object) -> Tuple[Any, bool]:
|
||||
if manager is None:
|
||||
return None, False
|
||||
# logger.debug(f"Manager: {manager}, aliases: {manager.aliases}, Key: {object_name}")
|
||||
if object_name in manager.aliases:
|
||||
return manager, True
|
||||
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)]
|
||||
for relationship in relationships:
|
||||
if relationship.key == object_name and "association" not in relationship.key:
|
||||
logger.debug(f"Checking {relationship.key}")
|
||||
try:
|
||||
rel_obj = getattr(manager, relationship.key)
|
||||
if rel_obj is not None:
|
||||
logger.debug(f"Returning {rel_obj}")
|
||||
return rel_obj, False
|
||||
except AttributeError:
|
||||
pass
|
||||
if "association" in relationship.key:
|
||||
try:
|
||||
logger.debug(f"Checking association {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 rel_obj is not None:
|
||||
logger.debug(f"Returning {rel_obj}")
|
||||
return rel_obj, False
|
||||
except AttributeError:
|
||||
pass
|
||||
@@ -862,7 +854,6 @@ def check_authorization(func):
|
||||
report.add_result(
|
||||
Result(owner=func.__str__(), code=1, msg=error_msg, status="warning"))
|
||||
return report, kwargs
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@@ -888,7 +879,6 @@ def under_development(func):
|
||||
Result(owner=func.__str__(), code=1, msg=error_msg,
|
||||
status="warning"))
|
||||
return report
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@@ -906,7 +896,6 @@ def report_result(func):
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
# logger.info(f"Report result being called by {func.__name__}")
|
||||
output = func(*args, **kwargs)
|
||||
match output:
|
||||
case Report():
|
||||
@@ -931,6 +920,7 @@ def report_result(func):
|
||||
logger.error(f"Problem reporting due to {e}")
|
||||
logger.error(result.msg)
|
||||
if output:
|
||||
logger.info(f"Report result being called by {func.__name__}")
|
||||
if is_list_etc(output):
|
||||
true_output = tuple(item for item in output if not isinstance(item, Report))
|
||||
if len(true_output) == 1:
|
||||
@@ -943,7 +933,6 @@ def report_result(func):
|
||||
else:
|
||||
true_output = None
|
||||
return true_output
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@@ -962,11 +951,32 @@ def is_list_etc(object):
|
||||
|
||||
|
||||
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:
|
||||
occurence = 1
|
||||
"""
|
||||
Gives stat holidays for the input year.
|
||||
|
||||
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:
|
||||
day = occurence * 7
|
||||
day = occurrence * 7
|
||||
max_days = (date(2012, month + 1, 1) - date(2012, month, 1)).days
|
||||
if 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
|
||||
output = d + timedelta(offset)
|
||||
return output.date()
|
||||
|
||||
if not 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),
|
||||
date(year, 11, 11), date(year, 12, 25), date(year, 12, 26),
|
||||
date(year + 1, 1, 1)]
|
||||
# NOTE: Labour Day
|
||||
holidays.append(find_nth_monday(year, 9))
|
||||
# NOTE: Thanksgiving
|
||||
holidays.append(find_nth_monday(year, 10, occurence=2))
|
||||
holidays.append(find_nth_monday(year, 10, occurrence=2))
|
||||
# NOTE: Victoria Day
|
||||
holidays.append(find_nth_monday(year, 5, day=25))
|
||||
# NOTE: Easter, etc
|
||||
@@ -1007,7 +1016,6 @@ def check_dictionary_inclusion_equality(listo: List[dict] | dict, dicto: dict) -
|
||||
Returns:
|
||||
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):
|
||||
return listo == dicto
|
||||
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)}")
|
||||
|
||||
|
||||
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))
|
||||
|
||||
|
||||
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):
|
||||
match input_dict:
|
||||
case int() | float() | bool():
|
||||
pass
|
||||
case _:
|
||||
try:
|
||||
js = json.dumps(input_dict)
|
||||
input_dict = json.dumps(input_dict)
|
||||
except TypeError:
|
||||
input_dict = str(input_dict)
|
||||
return input_dict
|
||||
# return input_dict
|
||||
output = {}
|
||||
for key, value in input_dict.items():
|
||||
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]
|
||||
case dict():
|
||||
value = sanitize_object_for_json(value)
|
||||
|
||||
case _:
|
||||
try:
|
||||
js = json.dumps(value)
|
||||
value = json.dumps(value)
|
||||
except TypeError:
|
||||
value = str(value)
|
||||
output[key] = value
|
||||
return output
|
||||
|
||||
|
||||
def create_plate_grid(rows: int, columns: int):
|
||||
matrix = np.array([[0 for yyy in range(1, columns + 1)] for xxx in range(1, rows + 1)])
|
||||
def create_plate_grid(rows: int, columns: int) -> dict:
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Allows for properties on classes as well as objects.
|
||||
"""
|
||||
def __get__(self, owner_self, owner_cls):
|
||||
return self.fget(owner_cls)
|
||||
|
||||
@@ -1396,6 +1436,16 @@ class Settings(BaseSettings, extra="allow"):
|
||||
|
||||
@classmethod
|
||||
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.read(alembic_path)
|
||||
url = c['alembic']['sqlalchemy.url']
|
||||
|
||||
@@ -32,8 +32,10 @@ a = Analysis(
|
||||
binaries=[],
|
||||
datas=[
|
||||
("src\\config.yml", "files"),
|
||||
("src\\submissions\\templates\\*", "files\\templates"),
|
||||
("src\\submissions\\templates\\css\\*", "files\\templates\\css"),
|
||||
("src\\submissions\\templates\\*.html", "files\\templates"),
|
||||
("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"),
|
||||
("src\\submissions\\resources\\*", "files\\resources"),
|
||||
("alembic.ini", "files"),
|
||||
@@ -51,12 +53,32 @@ a = Analysis(
|
||||
)
|
||||
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(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name=f"{__project__}_{__version__}",
|
||||
name=f"{__project__}_{__version__}_2",
|
||||
debug=True,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
|
||||
Reference in New Issue
Block a user