Pydantic switchover for omni is largely complete. Will need some debugging.

This commit is contained in:
lwark
2025-02-27 10:00:43 -06:00
parent f57aa3c3f0
commit abcdbac5b8
8 changed files with 431 additions and 33 deletions

View File

@@ -333,6 +333,7 @@ class BaseClass(Base):
def check_all_attributes(self, attributes: dict) -> bool:
"""
Checks this instance against a dictionary of attributes to determine if they are a match.
Args:
attributes (dict): A dictionary of attributes to be check for equivalence
@@ -345,9 +346,16 @@ class BaseClass(Base):
# print(getattr(self.__class__, key).property)
if value.lower() == "none":
value = None
logger.debug(f"Attempting to grab attribute: {key}")
self_value = getattr(self, key)
class_attr = getattr(self.__class__, key)
match class_attr.property:
logger.debug(f"Self value: {self_value}, class attr: {class_attr} of type: {type(class_attr)}")
if isinstance(class_attr, property):
filter = "property"
else:
filter = class_attr.property
match filter:
# match class_attr:
case ColumnProperty():
match class_attr.type:
case INTEGER():
@@ -359,13 +367,25 @@ class BaseClass(Base):
value = int(value)
case FLOAT():
value = float(value)
case "property":
pass
case _RelationshipDeclared():
logger.debug(f"Checking {self_value}")
try:
self_value = self_value.name
except AttributeError:
pass
if class_attr.property.uselist:
self_value = self_value.__str__()
try:
logger.debug(f"Check if {self_value.__class__} is subclass of {self.__class__}")
check = issubclass(self_value.__class__, self.__class__)
except TypeError as e:
logger.error(f"Couldn't check if {self_value.__class__} is subclass of {self.__class__} due to {e}")
check = False
if check:
logger.debug(f"Checking for subclass name.")
self_value = self_value.name
logger.debug(f"Checking self_value {self_value} of type {type(self_value)} against attribute {value} of type {type(value)}")
if self_value != value:
output = False
@@ -393,13 +413,15 @@ class BaseClass(Base):
logger.debug(f"Setting _RelationshipDeclared to {value}")
if field_type.property.uselist:
logger.debug(f"Setting with uselist")
if self.__getattribute__(key) is not None:
existing = self.__getattribute__(key)
if existing is not None:
if isinstance(value, list):
value = self.__getattribute__(key) + value
value = existing + value
else:
value = self.__getattribute__(key) + [value]
value = existing + [value]
else:
value = [value]
value = list(set(value))
return super().__setattr__(key, value)
else:
if isinstance(value, list):

View File

@@ -15,6 +15,7 @@ from pandas import ExcelFile
from pathlib import Path
from . import Base, BaseClass, Organization, LogMixin
from io import BytesIO
from inspect import getouterframes, currentframe
logger = logging.getLogger(f'submissions.{__name__}')
@@ -227,6 +228,20 @@ class KitType(BaseClass):
# logger.debug(f"Output: {output}")
return output, new_kit
@classmethod
def query_or_create(cls, **kwargs) -> Tuple[KitType, bool]:
from backend.validators.pydant import PydKitType
new = False
disallowed = ['expiry']
sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
instance = cls.query(**sanitized_kwargs)
if not instance or isinstance(instance, list):
instance = PydKitType(**kwargs)
new = True
instance = instance.to_sql()
logger.info(f"Instance from query or create: {instance}")
return instance, new
@classmethod
@setup_lookup
def query(cls,
@@ -380,8 +395,27 @@ class KitType(BaseClass):
new_process.equipment_roles.append(new_role)
return new_kit
def to_pydantic(self):
pass
def to_omni(self, expand: bool = False) -> "OmniKitType":
from backend.validators.omni_gui_objects import OmniKitType
# logger.debug(f"self.name: {self.name}")
# level = len(getouterframes(currentframe()))
# logger.warning(f"Function level is {level}")
if expand:
processes = [item.to_omni() for item in self.processes]
kit_reagentrole_associations = [item.to_omni() for item in self.kit_reagentrole_associations]
kit_submissiontype_associations = [item.to_omni() for item in self.kit_submissiontype_associations]
else:
processes = [item.name for item in self.processes]
kit_reagentrole_associations = [item.name for item in self.kit_reagentrole_associations]
kit_submissiontype_associations = [item.name for item in self.kit_submissiontype_associations]
data = dict(
name=self.name,
processes=processes,
kit_reagentrole_associations=kit_reagentrole_associations,
kit_submissiontype_associations=kit_submissiontype_associations
)
logger.debug(f"Creating omni for {pformat(data)}")
return OmniKitType(instance_object=self, **data)
class ReagentRole(BaseClass):
@@ -413,6 +447,20 @@ class ReagentRole(BaseClass):
"""
return f"<ReagentRole({self.name})>"
@classmethod
def query_or_create(cls, **kwargs) -> Tuple[ReagentRole, bool]:
new = False
disallowed = ['expiry']
sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
instance = cls.query(**sanitized_kwargs)
if not instance or isinstance(instance, list):
instance = cls()
new = True
for k, v in sanitized_kwargs.items():
setattr(instance, k, v)
logger.info(f"Instance from query or create: {instance}")
return instance, new
@classmethod
@setup_lookup
def query(cls,
@@ -496,6 +544,11 @@ class ReagentRole(BaseClass):
def save(self):
super().save()
def to_omni(self, expand: bool=False):
from backend.validators.omni_gui_objects import OmniReagentRole
logger.debug(f"Constructing OmniReagentRole with name {self.name}")
return OmniReagentRole(instance_object=self, name=self.name, eol_ext=self.eol_ext)
class Reagent(BaseClass, LogMixin):
"""
@@ -1010,6 +1063,20 @@ class SubmissionType(BaseClass):
from .submissions import BasicSubmission
return BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.name)
@classmethod
def query_or_create(cls, **kwargs) -> Tuple[SubmissionType, bool]:
new = False
disallowed = ['expiry']
sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
instance = cls.query(**sanitized_kwargs)
if not instance or isinstance(instance, list):
instance = cls()
new = True
for k, v in sanitized_kwargs.items():
setattr(instance, k, v)
logger.info(f"Instance from query or create: {instance}")
return instance, new
@classmethod
@setup_lookup
def query(cls,
@@ -1112,6 +1179,43 @@ class SubmissionType(BaseClass):
Organization.import_from_yml(filepath=filepath)
return submission_type
def to_omni(self, expand: bool = False):
from backend.validators.omni_gui_objects import OmniSubmissionType
# level = len(getouterframes(currentframe()))
# logger.warning(f"Function level is {level}")
# try:
# info_map = self.submission_type.info_map
# except AttributeError:
# info_map = {}
# try:
# defaults = self.submission_type.defaults
# except AttributeError:
# defaults = {}
# try:
# sample_map = self.submission_type.sample_map
# except AttributeError:
# sample_map = {}
try:
template_file = self.template_file
except AttributeError:
template_file = bytes()
if expand:
try:
processes = [item.to_omni() for item in self.processes]
except AttributeError:
processes = []
else:
processes = [item.name for item in self.processes]
return OmniSubmissionType(
instance_object=self,
name=self.name,
info_map=self.info_map,
defaults=self.defaults,
template_file=template_file,
processes=processes,
sample_map=self.sample_map
)
class SubmissionTypeKitTypeAssociation(BaseClass):
"""
@@ -1164,10 +1268,18 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
def kittype(self):
return self.kit_type
@kittype.setter
def kittype(self, value):
self.kit_type = value
@hybrid_property
def submissiontype(self):
return self.submission_type
@submissiontype.setter
def submissiontype(self, value):
self.submission_type = value
@property
def name(self):
try:
@@ -1175,6 +1287,20 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
except AttributeError:
return "Blank SubmissionTypeKitTypeAssociation"
@classmethod
def query_or_create(cls, **kwargs) -> Tuple[SubmissionTypeKitTypeAssociation, bool]:
new = False
disallowed = ['expiry']
sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
instance = cls.query(**sanitized_kwargs)
if not instance or isinstance(instance, list):
instance = cls()
new = True
for k, v in sanitized_kwargs.items():
setattr(instance, k, v)
logger.info(f"Instance from query or create: {instance}")
return instance, new
@classmethod
@setup_lookup
def query(cls,
@@ -1226,6 +1352,36 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
base_dict['kit_type'] = self.kit_type.to_export_dict(submission_type=self.submission_type)
return base_dict
def to_omni(self, expand: bool = False):
from backend.validators.omni_gui_objects import OmniSubmissionTypeKitTypeAssociation
# level = len(getouterframes(currentframe()))
# logger.warning(f"Function level is {level}")
if expand:
try:
submissiontype = self.submission_type.to_omni()
except AttributeError:
submissiontype = ""
try:
kittype = self.kit_type.to_omni()
except AttributeError:
kittype = ""
else:
submissiontype = self.submission_type.name
kittype = self.kit_type.name
# try:
# processes = [item.to_omni() for item in self.submission_type.processes]
# except AttributeError:
# processes = []
return OmniSubmissionTypeKitTypeAssociation(
instance_object=self,
submissiontype=submissiontype,
kittype=kittype,
mutable_cost_column=self.mutable_cost_column,
mutable_cost_sample=self.mutable_cost_sample,
constant_cost=self.constant_cost
# processes=processes,
)
class KitTypeReagentRoleAssociation(BaseClass):
"""
@@ -1312,11 +1468,26 @@ class KitTypeReagentRoleAssociation(BaseClass):
raise ValueError(f'{value} is not a reagentrole')
return value
@classmethod
def query_or_create(cls, **kwargs) -> Tuple[KitTypeReagentRoleAssociation, bool]:
new = False
disallowed = ['expiry']
sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
instance = cls.query(**sanitized_kwargs)
if not instance or isinstance(instance, list):
instance = cls()
new = True
for k, v in sanitized_kwargs.items():
setattr(instance, k, v)
logger.info(f"Instance from query or create: {instance}")
return instance, new
@classmethod
@setup_lookup
def query(cls,
kittype: KitType | str | None = None,
reagentrole: ReagentRole | str | None = None,
submissiontype: SubmissionType | str | None = None,
limit: int = 0,
**kwargs
) -> KitTypeReagentRoleAssociation | List[KitTypeReagentRoleAssociation]:
@@ -1346,6 +1517,14 @@ class KitTypeReagentRoleAssociation(BaseClass):
query = query.join(ReagentRole).filter(ReagentRole.name == reagentrole)
case _:
pass
match submissiontype:
case SubmissionType():
query = query.filter(cls.submission_type == submissiontype)
case str():
query = query.join(SubmissionType).filter(SubmissionType.name == submissiontype)
case _:
pass
pass
if kittype is not None and reagentrole is not None:
limit = 1
return cls.execute_query(query=query, limit=limit)
@@ -1388,13 +1567,46 @@ class KitTypeReagentRoleAssociation(BaseClass):
@classproperty
def json_edit_fields(cls) -> dict:
dicto = dict(
sheet="str",
expiry=dict(column="int", row="int"),
lot=dict(column="int", row="int"),
name=dict(column="int", row="int")
)
sheet="str",
expiry=dict(column="int", row="int"),
lot=dict(column="int", row="int"),
name=dict(column="int", row="int")
)
return dicto
def to_omni(self, expand: bool = False) -> "OmniReagentRole":
from backend.validators.omni_gui_objects import OmniKitTypeReagentRoleAssociation
try:
eol_ext = self.reagent_role.eol_ext
except AttributeError:
eol_ext = timedelta(days=0)
if expand:
try:
submission_type = self.submission_type.to_omni()
except AttributeError:
submission_type = ""
try:
kit_type = self.kit_type.to_omni()
except AttributeError:
kit_type = ""
try:
reagent_role = self.reagent_role.to_omni()
except AttributeError:
reagent_role = ""
else:
submission_type = self.submission_type.name
kit_type = self.kit_type.name
reagent_role = self.reagent_role.name
return OmniKitTypeReagentRoleAssociation(
instance_object=self,
reagent_role=reagent_role,
eol_ext=eol_ext,
required=self.required,
submission_type=submission_type,
kit_type=kit_type,
uses=self.uses
)
class SubmissionReagentAssociation(BaseClass):
"""
@@ -1716,9 +1928,28 @@ class EquipmentRole(BaseClass):
pyd_dict['processes'] = self.get_processes(submission_type=submission_type, extraction_kit=extraction_kit)
return PydEquipmentRole(equipment=equipment, **pyd_dict)
@classmethod
def query_or_create(cls, **kwargs) -> Tuple[EquipmentRole, bool]:
new = False
disallowed = ['expiry']
sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
instance = cls.query(**sanitized_kwargs)
if not instance or isinstance(instance, list):
instance = cls()
new = True
for k, v in sanitized_kwargs.items():
setattr(instance, k, v)
logger.info(f"Instance from query or create: {instance}")
return instance, new
@classmethod
@setup_lookup
def query(cls, name: str | None = None, id: int | None = None, limit: int = 0) -> EquipmentRole | List[
def query(cls,
name: str | None = None,
id: int | None = None,
limit: int = 0,
**kwargs
) -> EquipmentRole | List[
EquipmentRole]:
"""
Lookup Equipment roles.
@@ -1779,6 +2010,10 @@ class EquipmentRole(BaseClass):
processes = self.get_processes(submission_type=submission_type, extraction_kit=kit_type)
return dict(role=self.name, processes=[item for item in processes])
def to_omni(self, expand: bool = False) -> "OmniEquipmentRole":
from backend.validators.omni_gui_objects import OmniEquipmentRole
return OmniEquipmentRole(instance_object=self, name=self.name)
class SubmissionEquipmentAssociation(BaseClass):
"""
@@ -1949,6 +2184,20 @@ class Process(BaseClass):
if value not in field:
field.append(value)
@classmethod
def query_or_create(cls, **kwargs) -> Tuple[Process, bool]:
new = False
disallowed = ['expiry']
sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
instance = cls.query(**sanitized_kwargs)
if not instance or isinstance(instance, list):
instance = cls()
new = True
for k, v in sanitized_kwargs.items():
setattr(instance, k, v)
logger.info(f"Instance from query or create: {instance}")
return instance, new
@classmethod
@setup_lookup
def query(cls,
@@ -2013,7 +2262,15 @@ class Process(BaseClass):
def save(self):
super().save()
# @classmethod
def to_omni(self, expand: bool = False):
from backend.validators.omni_gui_objects import OmniProcess
return OmniProcess(
instance_object=self,
name=self.name,
submission_types=[item.to_omni() for item in self.submission_types],
equipment_roles=[item.to_omni() for item in self.equipment_roles],
tip_roles=[item.to_omni() for item in self.tip_roles]
)
class TipRole(BaseClass):
@@ -2034,13 +2291,51 @@ class TipRole(BaseClass):
submission_types = association_proxy("tiprole_submissiontype_associations", "submission_type")
@hybrid_property
def tips(self):
return self.instances
def __repr__(self):
return f"<TipRole({self.name})>"
@classmethod
def query_or_create(cls, **kwargs) -> Tuple[TipRole, bool]:
new = False
disallowed = ['expiry']
sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
instance = cls.query(**sanitized_kwargs)
if not instance or isinstance(instance, list):
instance = cls()
new = True
for k, v in sanitized_kwargs.items():
setattr(instance, k, v)
logger.info(f"Instance from query or create: {instance}")
return instance, new
@classmethod
@setup_lookup
def query(cls, name: str | None = None, limit: int = 0, **kwargs) -> TipRole | List[TipRole]:
query = cls.__database_session__.query(cls)
match name:
case str():
query = query.filter(cls.name == name)
limit = 1
case _:
pass
return cls.execute_query(query=query, limit=limit)
@check_authorization
def save(self):
super().save()
def to_omni(self, expand: bool = False):
from backend.validators.omni_gui_objects import OmniTipRole
return OmniTipRole(
instance_object=self,
name=self.name,
tips=[item.to_omni() for item in self.tips]
)
class Tips(BaseClass, LogMixin):
"""
@@ -2070,6 +2365,20 @@ class Tips(BaseClass, LogMixin):
def __repr__(self):
return f"<Tips({self.name})>"
@classmethod
def query_or_create(cls, **kwargs) -> Tuple[Tips, bool]:
new = False
disallowed = ['expiry']
sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
instance = cls.query(**sanitized_kwargs)
if not instance or isinstance(instance, list):
instance = cls()
new = True
for k, v in sanitized_kwargs.items():
setattr(instance, k, v)
logger.info(f"Instance from query or create: {instance}")
return instance, new
@classmethod
def query(cls, name: str | None = None, lot: str | None = None, limit: int = 0, **kwargs) -> Tips | List[Tips]:
"""
@@ -2101,6 +2410,13 @@ class Tips(BaseClass, LogMixin):
def save(self):
super().save()
def to_omni(self, expand: bool = True):
from backend.validators.omni_gui_objects import OmniTips
return OmniTips(
instance_object=self,
name=self.name
)
class SubmissionTypeTipRoleAssociation(BaseClass):
"""
@@ -2129,6 +2445,9 @@ class SubmissionTypeTipRoleAssociation(BaseClass):
def save(self):
super().save()
def to_omni(self):
pass
class SubmissionTipsAssociation(BaseClass):
"""

View File

@@ -10,9 +10,7 @@ from zipfile import ZipFile, BadZipfile
from tempfile import TemporaryDirectory, TemporaryFile
from operator import itemgetter
from pprint import pformat
from sqlalchemy.ext.hybrid import hybrid_property
from . import BaseClass, Reagent, SubmissionType, KitType, Organization, Contact, LogMixin, SubmissionReagentAssociation
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, func
from sqlalchemy.orm import relationship, validates, Query
@@ -24,7 +22,7 @@ from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as S
from openpyxl import Workbook
from openpyxl.drawing.image import Image as OpenpyxlImage
from tools import row_map, setup_lookup, jinja_template_loading, rreplace, row_keys, check_key_or_attr, Result, Report, \
report_result, create_holidays_for_year
report_result, create_holidays_for_year, check_dictionary_inclusion_equality
from datetime import datetime, date, timedelta
from typing import List, Any, Tuple, Literal, Generator, Type
from dateutil.parser import parse
@@ -558,12 +556,14 @@ class BasicSubmission(BaseClass, LogMixin):
existing = value
case _:
existing = self.__getattribute__(key)
logger.debug(f"Existing value is {pformat(existing)}")
if value in ['', 'null', None]:
logger.error(f"No value given, not setting.")
return
if existing is None:
existing = []
if value in existing:
# if value in existing:
if check_dictionary_inclusion_equality(existing, value):
logger.warning("Value already exists. Preventing duplicate addition.")
return
else:
@@ -572,6 +572,7 @@ class BasicSubmission(BaseClass, LogMixin):
else:
if value:
existing.append(value)
self.__setattr__(key, existing)
# NOTE: Make sure this gets updated by telling SQLAlchemy it's been modified.
flag_modified(self, key)
@@ -1223,10 +1224,19 @@ class BasicSubmission(BaseClass, LogMixin):
if "submitted_date" not in kwargs.keys():
instance.submitted_date = date.today()
else:
from frontend.widgets.pop_ups import QuestionAsker
logger.warning(f"Found existing instance: {instance}, asking to overwrite.")
code = 1
msg = "This submission already exists.\nWould you like to overwrite?"
report.add_result(Result(msg=msg, code=code))
# code = 1
# msg = "This submission already exists.\nWould you like to overwrite?"
# report.add_result(Result(msg=msg, code=code))
dlg = QuestionAsker(title="Overwrite?", message="This submission already exists.\nWould you like to overwrite?")
if dlg.exec():
pass
else:
code = 1
msg = "This submission already exists.\nWould you like to overwrite?"
report.add_result(Result(msg=msg, code=code))
return None, report
return instance, report
# NOTE: Custom context events for the ui
@@ -1528,6 +1538,7 @@ class Wastewater(BasicSubmission):
dummy_samples.append(thing)
output['origin_plate'] = self.__class__.make_plate_map(sample_list=dummy_samples, plate_rows=4,
plate_columns=6)
# logger.debug(f"PCR info: {output['pcr_info']}")
return output
@classmethod

View File

@@ -483,7 +483,6 @@ class PydSubmission(BaseModel, extra='allow'):
value['value'] = output.replace(tzinfo=timezone)
return value
@field_validator("submitting_lab", mode="before")
@classmethod
def rescue_submitting_lab(cls, value):
@@ -772,7 +771,7 @@ class PydSubmission(BaseModel, extra='allow'):
return missing_info, missing_reagents
@report_result
def to_sql(self) -> Tuple[BasicSubmission, Report]:
def to_sql(self) -> Tuple[BasicSubmission | None, Report]:
"""
Converts this instance into a backend.db.models.submissions.BasicSubmission instance
@@ -782,12 +781,19 @@ class PydSubmission(BaseModel, extra='allow'):
report = Report()
dicto = self.improved_dict()
logger.debug(f"Pydantic submission type: {self.submission_type['value']}")
logger.debug(f"Pydantic improved_dict: {pformat(dicto)}")
# At this point, pcr_info is not duplicated
instance, result = BasicSubmission.query_or_create(submission_type=self.submission_type['value'],
rsl_plate_num=self.rsl_plate_num['value'])
logger.debug(f"Created or queried instance: {instance}")
# logger.debug(f"Created or queried instance: {instance}")
if instance is None:
report.add_result(Result(msg="Overwrite Cancelled."))
return None, report
report.add_result(result)
self.handle_duplicate_samples()
for key, value in dicto.items():
logger.debug(f"Checking key {key}, value {value}")
# At this point, pcr_info is not duplicated.
if isinstance(value, dict):
try:
value = value['value']
@@ -843,6 +849,8 @@ class PydSubmission(BaseModel, extra='allow'):
value = value
instance.set_attribute(key=key, value=value)
case item if item in instance.jsons:
# At this point pcr_info is not duplicated
logger.debug(f"Validating json value: {item} to value:{pformat(value)}")
try:
ii = value.items()
except AttributeError:
@@ -851,7 +859,9 @@ class PydSubmission(BaseModel, extra='allow'):
if isinstance(v, datetime):
value[k] = v.strftime("%Y-%m-%d %H:%M:%S")
else:
value[k] = v
pass
logger.debug(f"Setting json value: {item} to value:{pformat(value)}")
# At this point, pcr_info is not duplicated.
instance.set_attribute(key=key, value=value)
case _:
try:
@@ -899,6 +909,10 @@ class PydSubmission(BaseModel, extra='allow'):
SubmissionFormWidget: Submission form widget
"""
from frontend.widgets.submission_widget import SubmissionFormWidget
try:
logger.debug(f"PCR info: {self.pcr_info}")
except AttributeError:
pass
return SubmissionFormWidget(parent=parent, submission=self, disable=disable)
def to_writer(self) -> "SheetWriter":
@@ -1124,7 +1138,7 @@ class PydReagentRole(BaseModel):
class PydKitType(BaseModel):
name: str
reagent_roles: List[PydReagentRole] = []
reagent_roles: List[PydReagent] = []
@report_result
def to_sql(self) -> Tuple[KitType, Report]: