Middle of the road.

This commit is contained in:
lwark
2025-01-29 14:12:38 -06:00
parent 73752cde87
commit 3fed8daade
11 changed files with 495 additions and 188 deletions

View File

@@ -8,7 +8,7 @@ from pydantic import BaseModel
from sqlalchemy import Column, INTEGER, String, JSON
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session, InstrumentedAttribute
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.exc import ArgumentError
from sqlalchemy.exc import ArgumentError, InvalidRequestError
from typing import Any, List
from pathlib import Path
from tools import report_result, list_sort_dict
@@ -48,7 +48,7 @@ class BaseClass(Base):
__table_args__ = {'extend_existing': True} #: Will only add new columns
singles = ['id']
omni_removes = ['submissions']
omni_removes = ['submissions', "omnigui_class_dict", "omnigui_instance_dict"]
omni_sort = ["name"]
@classproperty
@@ -60,7 +60,7 @@ class BaseClass(Base):
@classproperty
def aliases(cls):
return [cls.__name__.lower()]
return [cls.query_alias]
@classproperty
def level(cls):
@@ -198,7 +198,7 @@ class BaseClass(Base):
try:
records = [obj.to_sub_dict(**kwargs) for obj in objects]
except AttributeError:
records = [{k:v['instance_attr'] for k, v in obj.to_omnigui_dict(**kwargs).items()} for obj in objects]
records = [{k:v['instance_attr'] for k, v in obj.omnigui_instance_dict.items()} for obj in objects]
return DataFrame.from_records(records)
@classmethod
@@ -233,7 +233,11 @@ class BaseClass(Base):
logger.info(f"Using key: {k} with value: {v}")
try:
attr = getattr(model, k)
query = query.filter(attr == v)
# NOTE: account for attrs that use list.
if attr.property.uselist:
query = query.filter(attr.contains(v))
else:
query = query.filter(attr == v)
except (ArgumentError, AttributeError) as e:
logger.error(f"Attribute {k} unavailable due to:\n\t{e}\nSkipping.")
if k in singles:
@@ -265,14 +269,13 @@ class BaseClass(Base):
return report
@property
def omnigui_dict(self) -> dict:
def omnigui_instance_dict(self) -> dict:
"""
For getting any object in an omni-thing friendly output.
Returns:
dict: Dictionary of object minus _sa_instance_state with id at the front.
"""
# dicto = {k: v for k, v in self.__dict__.items() if k not in ["_sa_instance_state"]}
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
@@ -306,7 +309,7 @@ class BaseClass(Base):
model = getattr(pydant, f"Pyd{cls.__name__}")
except AttributeError:
logger.warning(f"Couldn't get {cls.__name__} pydantic model.")
return None
return pydant.PydElastic
return model
@classproperty
@@ -324,6 +327,32 @@ class BaseClass(Base):
query_kwargs = {relationship_instance.query_alias:relationship_instance}
return cls.query(**query_kwargs)
def __setattr__(self, key, value):
try:
field_type = getattr(self.__class__, key)
except AttributeError:
return super().__setattr__(key, value)
try:
check = field_type.property.uselist
except AttributeError:
check = False
if check:
logger.debug(f"Setting with uselist")
if self.__getattribute__(key) is not None:
if isinstance(value, list):
value = self.__getattribute__(key) + value
else:
value = self.__getattribute__(key) + [value]
else:
value = [value]
else:
if isinstance(value, list):
if len(value) == 1:
value = value[0]
else:
raise ValueError("Object is too long to parse a single value.")
return super().__setattr__(key, value)

View File

@@ -146,7 +146,7 @@ class Control(BaseClass):
@classmethod
@setup_lookup
def query(cls,
submission_type: str | None = None,
submissiontype: str | None = None,
subtype: str | None = None,
start_date: date | str | int | None = None,
end_date: date | str | int | None = None,
@@ -169,13 +169,13 @@ class Control(BaseClass):
"""
from backend.db import SubmissionType
query: Query = cls.__database_session__.query(cls)
match submission_type:
match submissiontype:
case str():
from backend import BasicSubmission, SubmissionType
query = query.join(BasicSubmission).join(SubmissionType).filter(SubmissionType.name == submission_type)
query = query.join(BasicSubmission).join(SubmissionType).filter(SubmissionType.name == submissiontype)
case SubmissionType():
from backend import BasicSubmission
query = query.join(BasicSubmission).filter(BasicSubmission.submission_type_name == submission_type.name)
query = query.join(BasicSubmission).filter(BasicSubmission.submission_type_name == submissiontype.name)
case _:
pass
# NOTE: by control type
@@ -347,7 +347,7 @@ class PCRControl(Control):
parent.mode_typer.clear()
parent.mode_typer.setEnabled(False)
report = Report()
controls = cls.query(submission_type=chart_settings['sub_type'], start_date=chart_settings['start_date'],
controls = cls.query(submissiontype=chart_settings['sub_type'], start_date=chart_settings['start_date'],
end_date=chart_settings['end_date'])
data = [control.to_sub_dict() for control in controls]
df = DataFrame.from_records(data)

View File

@@ -96,7 +96,8 @@ class KitType(BaseClass):
Base of kits used in submission processing
"""
query_alias = "kit_type"
# query_alias = "kit_type"
omni_sort = BaseClass.omni_sort + ["kit_submissiontype_associations", "kit_reagentrole_associations", "processes"]
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64), unique=True) #: name of kit
@@ -134,7 +135,12 @@ class KitType(BaseClass):
@classproperty
def aliases(cls):
return super().aliases + [cls.query_alias, "kit_types"]
return super().aliases + [cls.query_alias, "kit_types", "kit_type"]
@hybrid_property
def submissiontype(self):
"""Alias used_for field to allow query with SubmissionType query alias"""
return self.used_for
def get_reagents(self,
required: bool = False,
@@ -227,7 +233,8 @@ class KitType(BaseClass):
name: str = None,
used_for: str | SubmissionType | None = None,
id: int | None = None,
limit: int = 0
limit: int = 0,
**kwargs
) -> KitType | List[KitType]:
"""
Lookup a list of or single KitType.
@@ -264,7 +271,7 @@ class KitType(BaseClass):
limit = 1
case _:
pass
return cls.execute_query(query=query, limit=limit)
return cls.execute_query(query=query, limit=limit, **kwargs)
@check_authorization
def save(self):
@@ -407,10 +414,11 @@ class ReagentRole(BaseClass):
@setup_lookup
def query(cls,
name: str | None = None,
kit_type: KitType | str | None = None,
kittype: KitType | str | None = None,
reagent: Reagent | str | None = None,
id: int | None = None,
limit: int = 0,
**kwargs
) -> ReagentRole | List[ReagentRole]:
"""
Lookup reagent types in the database.
@@ -429,14 +437,14 @@ class ReagentRole(BaseClass):
ReagentRole|List[ReagentRole]: ReagentRole or list of ReagentRoles matching filter.
"""
query: Query = cls.__database_session__.query(cls)
if (kit_type is not None and reagent is None) or (reagent is not None and kit_type is None):
if (kittype is not None and reagent is None) or (reagent is not None and kittype is None):
raise ValueError("Cannot filter without both reagent and kit type.")
elif kit_type is None and reagent is None:
elif kittype is None and reagent is None:
pass
else:
match kit_type:
match kittype:
case str():
kit_type = KitType.query(name=kit_type)
kittype = KitType.query(name=kittype)
case _:
pass
match reagent:
@@ -446,7 +454,7 @@ class ReagentRole(BaseClass):
pass
assert reagent.role
# NOTE: Get all roles common to the reagent and the kit.
result = set(kit_type.reagent_roles).intersection(reagent.role)
result = set(kittype.reagent_roles).intersection(reagent.role)
return next((item for item in result), None)
match name:
case str():
@@ -491,8 +499,6 @@ class Reagent(BaseClass, LogMixin):
Concrete reagent instance
"""
id = Column(INTEGER, primary_key=True) #: primary key
role = relationship("ReagentRole", back_populates="instances",
secondary=reagentroles_reagents) #: joined parent reagent type
@@ -523,6 +529,11 @@ class Reagent(BaseClass, LogMixin):
def searchables(cls):
return [dict(label="Lot", field="lot")]
@hybrid_property
def reagentrole(self):
"""Alias role field to allow query with ReagentRole query alias"""
return self.role
def to_sub_dict(self, extraction_kit: KitType = None, full_data: bool = False, **kwargs) -> dict:
"""
dictionary containing values necessary for gui
@@ -583,9 +594,9 @@ class Reagent(BaseClass, LogMixin):
Report: Result of operation
"""
report = Report()
rt = ReagentRole.query(kit_type=kit, reagent=self, limit=1)
rt = ReagentRole.query(kittype=kit, reagent=self, limit=1)
if rt is not None:
assoc = KitTypeReagentRoleAssociation.query(kit_type=kit, reagent_role=rt)
assoc = KitTypeReagentRoleAssociation.query(kittype=kit, reagentrole=rt)
if assoc is not None:
if assoc.last_used != self.lot:
assoc.last_used = self.lot
@@ -621,7 +632,8 @@ class Reagent(BaseClass, LogMixin):
role: str | ReagentRole | None = None,
lot: str | None = None,
name: str | None = None,
limit: int = 0
limit: int = 0,
**kwargs
) -> Reagent | List[Reagent]:
"""
Lookup a list of reagents from the database.
@@ -643,6 +655,8 @@ class Reagent(BaseClass, LogMixin):
limit = 1
case _:
pass
# if not role and "reagentrole" in kwargs.keys():
# role = kwargs['reagentrole']
match role:
case str():
query = query.join(cls.role).filter(ReagentRole.name == role)
@@ -663,7 +677,7 @@ class Reagent(BaseClass, LogMixin):
limit = 1
case _:
pass
return cls.execute_query(query=query, limit=limit)
return cls.execute_query(query=query, limit=limit, **kwargs)
def set_attribute(self, key, value):
match key:
@@ -741,7 +755,7 @@ class Discount(BaseClass):
@setup_lookup
def query(cls,
organization: Organization | str | int | None = None,
kit_type: KitType | str | int | None = None,
kittype: KitType | str | int | None = None,
) -> Discount | List[Discount]:
"""
Lookup discount objects (union of kit and organization)
@@ -763,13 +777,13 @@ class Discount(BaseClass):
query = query.join(Organization).filter(Organization.id == organization)
case _:
pass
match kit_type:
match kittype:
case KitType():
query = query.filter(cls.kit == kit_type)
query = query.filter(cls.kit == kittype)
case str():
query = query.join(KitType).filter(KitType.name == kit_type)
query = query.join(KitType).filter(KitType.name == kittype)
case int():
query = query.join(KitType).filter(KitType.id == kit_type)
query = query.join(KitType).filter(KitType.id == kittype)
case _:
pass
return cls.execute_query(query=query)
@@ -833,6 +847,14 @@ class SubmissionType(BaseClass):
"""
return f"<SubmissionType({self.name})>"
@hybrid_property
def kittype(self):
return self.kit_types
@hybrid_property
def process(self):
return self.processes
@classproperty
def aliases(cls):
return super().aliases + ["submission_types", "submission_type"]
@@ -990,7 +1012,8 @@ class SubmissionType(BaseClass):
def query(cls,
name: str | None = None,
key: str | None = None,
limit: int = 0
limit: int = 0,
**kwargs
) -> SubmissionType | List[SubmissionType]:
"""
Lookup submission type in the database by a number of parameters
@@ -1124,7 +1147,23 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
Returns:
str: Representation of this object
"""
return f"<SubmissionTypeKitTypeAssociation({self.submission_type.name}&{self.kit_type.name})>"
try:
submission_type_name = self.submission_type.name
except AttributeError:
submission_type_name = "None"
try:
kit_type_name = self.kit_type.name
except AttributeError:
kit_type_name = "None"
return f"<SubmissionTypeKitTypeAssociation({submission_type_name}&{kit_type_name})>"
@hybrid_property
def kittype(self):
return self.kit_type
@hybrid_property
def submissiontype(self):
return self.submission_type
@property
def name(self):
@@ -1133,8 +1172,8 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
@classmethod
@setup_lookup
def query(cls,
submission_type: SubmissionType | str | int | None = None,
kit_type: KitType | str | int | None = None,
submissiontype: SubmissionType | str | int | None = None,
kittype: KitType | str | int | None = None,
limit: int = 0,
**kwargs
) -> SubmissionTypeKitTypeAssociation | List[SubmissionTypeKitTypeAssociation]:
@@ -1150,21 +1189,21 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
SubmissionTypeKitTypeAssociation|List[SubmissionTypeKitTypeAssociation]: SubmissionTypeKitTypeAssociation(s) of interest
"""
query: Query = cls.__database_session__.query(cls)
match submission_type:
match submissiontype:
case SubmissionType():
query = query.filter(cls.submission_type == submission_type)
query = query.filter(cls.submission_type == submissiontype)
case str():
query = query.join(SubmissionType).filter(SubmissionType.name == submission_type)
query = query.join(SubmissionType).filter(SubmissionType.name == submissiontype)
case int():
query = query.join(SubmissionType).filter(SubmissionType.id == submission_type)
match kit_type:
query = query.join(SubmissionType).filter(SubmissionType.id == submissiontype)
match kittype:
case KitType():
query = query.filter(cls.kit_type == kit_type)
query = query.filter(cls.kit_type == kittype)
case str():
query = query.join(KitType).filter(KitType.name == kit_type)
query = query.join(KitType).filter(KitType.name == kittype)
case int():
query = query.join(KitType).filter(KitType.id == kit_type)
if kit_type is not None and submission_type is not None:
query = query.join(KitType).filter(KitType.id == kittype)
if kittype is not None and submissiontype is not None:
limit = 1
# limit = query.count()
return cls.execute_query(query=query, limit=limit)
@@ -1221,7 +1260,10 @@ class KitTypeReagentRoleAssociation(BaseClass):
@property
def name(self):
return f"{self.kit_type.name} -> {self.reagent_role.name}"
try:
return f"{self.kit_type.name} -> {self.reagent_role.name}"
except AttributeError:
return "Blank KitTypeReagentRole"
@validates('required')
def validate_required(self, key, value):
@@ -1238,6 +1280,8 @@ class KitTypeReagentRoleAssociation(BaseClass):
Returns:
_type_: value
"""
if isinstance(value, bool):
value = int(value)
if not 0 <= value < 2:
raise ValueError(f'Invalid required value {value}. Must be 0 or 1.')
return value
@@ -1264,8 +1308,8 @@ class KitTypeReagentRoleAssociation(BaseClass):
@classmethod
@setup_lookup
def query(cls,
kit_type: KitType | str | None = None,
reagent_role: ReagentRole | str | None = None,
kittype: KitType | str | None = None,
reagentrole: ReagentRole | str | None = None,
limit: int = 0,
**kwargs
) -> KitTypeReagentRoleAssociation | List[KitTypeReagentRoleAssociation]:
@@ -1281,21 +1325,21 @@ class KitTypeReagentRoleAssociation(BaseClass):
models.KitTypeReagentTypeAssociation|List[models.KitTypeReagentTypeAssociation]: Junction of interest.
"""
query: Query = cls.__database_session__.query(cls)
match kit_type:
match kittype:
case KitType():
query = query.filter(cls.kit_type == kit_type)
query = query.filter(cls.kit_type == kittype)
case str():
query = query.join(KitType).filter(KitType.name == kit_type)
query = query.join(KitType).filter(KitType.name == kittype)
case _:
pass
match reagent_role:
match reagentrole:
case ReagentRole():
query = query.filter(cls.reagent_role == reagent_role)
query = query.filter(cls.reagent_role == reagentrole)
case str():
query = query.join(ReagentRole).filter(ReagentRole.name == reagent_role)
query = query.join(ReagentRole).filter(ReagentRole.name == reagentrole)
case _:
pass
if kit_type is not None and reagent_role is not None:
if kittype is not None and reagentrole is not None:
limit = 1
return cls.execute_query(query=query, limit=limit)
@@ -1329,8 +1373,8 @@ class KitTypeReagentRoleAssociation(BaseClass):
yield rel_reagent
@property
def omnigui_dict(self) -> dict:
dicto = super().omnigui_dict
def omnigui_instance_dict(self) -> dict:
dicto = super().omnigui_instance_dict
dicto['required']['instance_attr'] = bool(dicto['required']['instance_attr'])
return dicto
@@ -1801,6 +1845,14 @@ class SubmissionTypeEquipmentRoleAssociation(BaseClass):
equipment_role = relationship(EquipmentRole,
back_populates="equipmentrole_submissiontype_associations") #: associated equipment
@hybrid_property
def submissiontype(self):
return self.submission_type
@hybrid_property
def equipmentrole(self):
return self.equipment_role
@validates('static')
def validate_static(self, key, value):
"""
@@ -1847,6 +1899,8 @@ class Process(BaseClass):
A Process is a method used by a piece of equipment.
"""
level = 2
id = Column(INTEGER, primary_key=True) #: Process id, primary key
name = Column(String(64), unique=True) #: Process name
submission_types = relationship("SubmissionType", back_populates='processes',
@@ -1869,14 +1923,24 @@ class Process(BaseClass):
"""
return f"<Process({self.name})>"
def set_attribute(self, key, value):
match key:
case "name":
self.name = value
case _:
field = getattr(self, key)
if value not in field:
field.append(value)
@classmethod
@setup_lookup
def query(cls,
name: str | None = None,
id: int | None = None,
submission_type: str | SubmissionType | None = None,
extraction_kit: str | KitType | None = None,
equipment_role: str | KitType | None = None,
submissiontype: str | SubmissionType | None = None,
kittype: str | KitType | None = None,
equipmentrole: str | KitType | None = None,
limit: int = 0,
**kwargs) -> Process | List[Process]:
"""
@@ -1891,28 +1955,28 @@ class Process(BaseClass):
Process|List[Process]: Process(es) matching criteria
"""
query = cls.__database_session__.query(cls)
match submission_type:
match submissiontype:
case str():
submission_type = SubmissionType.query(name=submission_type)
query = query.filter(cls.submission_types.contains(submission_type))
submissiontype = SubmissionType.query(name=submissiontype)
query = query.filter(cls.submission_types.contains(submissiontype))
case SubmissionType():
query = query.filter(cls.submission_types.contains(submission_type))
query = query.filter(cls.submission_types.contains(submissiontype))
case _:
pass
match extraction_kit:
match kittype:
case str():
extraction_kit = KitType.query(name=extraction_kit)
query = query.filter(cls.kit_types.contains(extraction_kit))
kittype = KitType.query(name=kittype)
query = query.filter(cls.kit_types.contains(kittype))
case KitType():
query = query.filter(cls.kit_types.contains(extraction_kit))
query = query.filter(cls.kit_types.contains(kittype))
case _:
pass
match equipment_role:
match equipmentrole:
case str():
equipment_role = EquipmentRole.query(name=equipment_role)
query = query.filter(cls.equipment_roles.contains(equipment_role))
equipmentrole = EquipmentRole.query(name=equipmentrole)
query = query.filter(cls.equipment_roles.contains(equipmentrole))
case EquipmentRole():
query = query.filter(cls.equipment_roles.contains(equipment_role))
query = query.filter(cls.equipment_roles.contains(equipmentrole))
case _:
pass
match name:
@@ -1983,6 +2047,10 @@ class Tips(BaseClass, LogMixin):
submissions = association_proxy("tips_submission_associations", 'submission')
@hybrid_property
def tiprole(self):
return self.role
def __repr__(self):
return f"<Tips({self.name})>"
@@ -2033,6 +2101,14 @@ class SubmissionTypeTipRoleAssociation(BaseClass):
tip_role = relationship(TipRole,
back_populates="tiprole_submissiontype_associations") #: associated equipment
@hybrid_property
def submissiontype(self):
return self.submission_type
@hybrid_property
def tiprole(self):
return self.tip_role
@check_authorization
def save(self):
super().save()

View File

@@ -6,6 +6,7 @@ import json, yaml, logging
from pathlib import Path
from pprint import pformat
from sqlalchemy import Column, String, INTEGER, ForeignKey, Table
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship, Query
from . import Base, BaseClass
from tools import check_authorization, setup_lookup, yaml_regex_creator
@@ -38,6 +39,10 @@ class Organization(BaseClass):
contacts = relationship("Contact", back_populates="organization",
secondary=orgs_contacts) #: contacts involved with this org
@hybrid_property
def contact(self):
return self.contacts
def __repr__(self) -> str:
return f"<Organization({self.name})>"

View File

@@ -10,6 +10,9 @@ 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
@@ -126,6 +129,14 @@ class BasicSubmission(BaseClass, LogMixin):
def __repr__(self) -> str:
return f"<Submission({self.rsl_plate_num})>"
@hybrid_property
def kittype(self):
return self.extraction_kit
@hybrid_property
def organization(self):
return self.submitting_lab
@classproperty
def jsons(cls) -> List[str]:
"""
@@ -488,7 +499,7 @@ class BasicSubmission(BaseClass, LogMixin):
"""
# NOTE: use lookup function to create list of dicts
subs = [item.to_dict() for item in
cls.query(submission_type=submission_type, limit=limit, chronologic=chronologic, page=page,
cls.query(submissiontype=submission_type, limit=limit, chronologic=chronologic, page=page,
page_size=page_size)]
df = pd.DataFrame.from_records(subs)
# NOTE: Exclude sub information
@@ -1056,7 +1067,7 @@ class BasicSubmission(BaseClass, LogMixin):
@classmethod
@setup_lookup
def query(cls,
submission_type: str | SubmissionType | None = None,
submissiontype: str | SubmissionType | None = None,
submission_type_name: str | None = None,
id: int | str | None = None,
rsl_plate_num: str | None = None,
@@ -1087,8 +1098,8 @@ class BasicSubmission(BaseClass, LogMixin):
"""
from ... import SubmissionReagentAssociation
# NOTE: if you go back to using 'model' change the appropriate cls to model in the query filters
if submission_type is not None:
model = cls.find_polymorphic_subclass(polymorphic_identity=submission_type)
if submissiontype is not None:
model = cls.find_polymorphic_subclass(polymorphic_identity=submissiontype)
elif len(kwargs) > 0:
# NOTE: find the subclass containing the relevant attributes
model = cls.find_polymorphic_subclass(attrs=kwargs)
@@ -1196,7 +1207,7 @@ class BasicSubmission(BaseClass, LogMixin):
if kwargs == {}:
raise ValueError("Need to narrow down query or the first available instance will be returned.")
sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
instance = cls.query(submission_type=submission_type, limit=1, **sanitized_kwargs)
instance = cls.query(submissiontype=submission_type, limit=1, **sanitized_kwargs)
if instance is None:
used_class = cls.find_polymorphic_subclass(attrs=kwargs, polymorphic_identity=submission_type)
instance = used_class(**sanitized_kwargs)