Prior to updating queries to use query alias.
This commit is contained in:
@@ -6,13 +6,12 @@ import sys, logging
|
||||
from pandas import DataFrame
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import Column, INTEGER, String, JSON
|
||||
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session
|
||||
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session, InstrumentedAttribute
|
||||
from sqlalchemy.ext.declarative import declared_attr
|
||||
from sqlalchemy.exc import ArgumentError
|
||||
from typing import Any, List
|
||||
from pathlib import Path
|
||||
from tools import report_result
|
||||
|
||||
from tools import report_result, list_sort_dict
|
||||
|
||||
# NOTE: Load testing environment
|
||||
if 'pytest' in sys.modules:
|
||||
@@ -49,6 +48,30 @@ class BaseClass(Base):
|
||||
__table_args__ = {'extend_existing': True} #: Will only add new columns
|
||||
|
||||
singles = ['id']
|
||||
omni_removes = ['submissions']
|
||||
omni_sort = ["name"]
|
||||
|
||||
@classproperty
|
||||
def skip_on_edit(cls):
|
||||
if "association" in cls.__name__.lower() or cls.__name__.lower() == "discount":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@classproperty
|
||||
def aliases(cls):
|
||||
return [cls.__name__.lower()]
|
||||
|
||||
@classproperty
|
||||
def level(cls):
|
||||
if "association" in cls.__name__.lower() or cls.__name__.lower() == "discount":
|
||||
return 2
|
||||
else:
|
||||
return 1
|
||||
|
||||
@classproperty
|
||||
def query_alias(cls):
|
||||
return cls.__name__.lower()
|
||||
|
||||
@classmethod
|
||||
@declared_attr
|
||||
@@ -175,7 +198,7 @@ class BaseClass(Base):
|
||||
try:
|
||||
records = [obj.to_sub_dict(**kwargs) for obj in objects]
|
||||
except AttributeError:
|
||||
records = [obj.omnigui_dict for obj in objects]
|
||||
records = [{k:v['instance_attr'] for k, v in obj.to_omnigui_dict(**kwargs).items()} for obj in objects]
|
||||
return DataFrame.from_records(records)
|
||||
|
||||
@classmethod
|
||||
@@ -249,11 +272,25 @@ class BaseClass(Base):
|
||||
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 = {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
|
||||
}
|
||||
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)
|
||||
except TypeError as e:
|
||||
logger.error(f"Could not sort {self.__class__.__name__} by list due to :{e}")
|
||||
try:
|
||||
dicto = {'id': dicto.pop('id'), **dicto}
|
||||
except KeyError:
|
||||
pass
|
||||
# logger.debug(f"{self.__class__.__name__} omnigui dict:\n\n{pformat(dicto)}")
|
||||
return dicto
|
||||
|
||||
@classproperty
|
||||
@@ -268,6 +305,7 @@ class BaseClass(Base):
|
||||
try:
|
||||
model = getattr(pydant, f"Pyd{cls.__name__}")
|
||||
except AttributeError:
|
||||
logger.warning(f"Couldn't get {cls.__name__} pydantic model.")
|
||||
return None
|
||||
return model
|
||||
|
||||
@@ -281,6 +319,13 @@ class BaseClass(Base):
|
||||
"""
|
||||
return dict()
|
||||
|
||||
@classmethod
|
||||
def relevant_relationships(cls, relationship_instance):
|
||||
query_kwargs = {relationship_instance.query_alias:relationship_instance}
|
||||
return cls.query(**query_kwargs)
|
||||
|
||||
|
||||
|
||||
|
||||
class ConfigItem(BaseClass):
|
||||
"""
|
||||
|
||||
@@ -7,6 +7,7 @@ from pprint import pformat
|
||||
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB
|
||||
from sqlalchemy.orm import relationship, validates, Query
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from datetime import date, datetime, timedelta
|
||||
from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, yaml_regex_creator, timezone
|
||||
from typing import List, Literal, Generator, Any, Tuple
|
||||
@@ -95,6 +96,8 @@ class KitType(BaseClass):
|
||||
Base of kits used in submission processing
|
||||
"""
|
||||
|
||||
query_alias = "kit_type"
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
name = Column(String(64), unique=True) #: name of kit
|
||||
submissions = relationship("BasicSubmission", back_populates="extraction_kit") #: submissions this kit was used for
|
||||
@@ -129,6 +132,10 @@ class KitType(BaseClass):
|
||||
"""
|
||||
return f"<KitType({self.name})>"
|
||||
|
||||
@classproperty
|
||||
def aliases(cls):
|
||||
return super().aliases + [cls.query_alias, "kit_types"]
|
||||
|
||||
def get_reagents(self,
|
||||
required: bool = False,
|
||||
submission_type: str | SubmissionType | None = None
|
||||
@@ -157,7 +164,7 @@ class KitType(BaseClass):
|
||||
else:
|
||||
return (item.reagent_role for item in relevant_associations)
|
||||
|
||||
def construct_xl_map_for_use(self, submission_type: str | SubmissionType) -> Tuple[dict|None, KitType]:
|
||||
def construct_xl_map_for_use(self, submission_type: str | SubmissionType) -> Tuple[dict | None, KitType]:
|
||||
"""
|
||||
Creates map of locations in Excel workbook for a SubmissionType
|
||||
|
||||
@@ -402,12 +409,14 @@ class ReagentRole(BaseClass):
|
||||
name: str | None = None,
|
||||
kit_type: KitType | str | None = None,
|
||||
reagent: Reagent | str | None = None,
|
||||
id: int | None = None,
|
||||
limit: int = 0,
|
||||
) -> ReagentRole | List[ReagentRole]:
|
||||
"""
|
||||
Lookup reagent types in the database.
|
||||
|
||||
Args:
|
||||
id (id | None, optional): Id of the object. Defaults to None.
|
||||
name (str | None, optional): Reagent type name. Defaults to None.
|
||||
kit_type (KitType | str | None, optional): Kit the type of interest belongs to. Defaults to None.
|
||||
reagent (Reagent | str | None, optional): Concrete instance of the type of interest. Defaults to None.
|
||||
@@ -445,6 +454,12 @@ class ReagentRole(BaseClass):
|
||||
limit = 1
|
||||
case _:
|
||||
pass
|
||||
match id:
|
||||
case int():
|
||||
query = query.filter(cls.id == id)
|
||||
limit = 1
|
||||
case _:
|
||||
pass
|
||||
return cls.execute_query(query=query, limit=limit)
|
||||
|
||||
def to_pydantic(self) -> "PydReagent":
|
||||
@@ -476,7 +491,7 @@ class Reagent(BaseClass, LogMixin):
|
||||
Concrete reagent instance
|
||||
"""
|
||||
|
||||
searchables = [dict(label="Lot", field="lot")]
|
||||
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
role = relationship("ReagentRole", back_populates="instances",
|
||||
@@ -504,6 +519,10 @@ class Reagent(BaseClass, LogMixin):
|
||||
name = f"<Reagent({self.role.name}-{self.lot})>"
|
||||
return name
|
||||
|
||||
@classproperty
|
||||
def searchables(cls):
|
||||
return [dict(label="Lot", field="lot")]
|
||||
|
||||
def to_sub_dict(self, extraction_kit: KitType = None, full_data: bool = False, **kwargs) -> dict:
|
||||
"""
|
||||
dictionary containing values necessary for gui
|
||||
@@ -581,7 +600,7 @@ class Reagent(BaseClass, LogMixin):
|
||||
from backend.validators.pydant import PydReagent
|
||||
new = False
|
||||
disallowed = ['expiry']
|
||||
sanitized_kwargs = {k:v for k,v in kwargs.items() if k not in disallowed}
|
||||
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):
|
||||
if "role" not in kwargs:
|
||||
@@ -700,6 +719,8 @@ class Discount(BaseClass):
|
||||
Relationship table for client labs for certain kits.
|
||||
"""
|
||||
|
||||
skip_on_edit = True
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
kit = relationship("KitType") #: joined parent reagent type
|
||||
kit_id = Column(INTEGER, ForeignKey("_kittype.id", ondelete='SET NULL', name="fk_kit_type_id")) #: id of joined kit
|
||||
@@ -812,6 +833,14 @@ class SubmissionType(BaseClass):
|
||||
"""
|
||||
return f"<SubmissionType({self.name})>"
|
||||
|
||||
@classproperty
|
||||
def aliases(cls):
|
||||
return super().aliases + ["submission_types", "submission_type"]
|
||||
|
||||
@classproperty
|
||||
def omni_removes(cls):
|
||||
return super().omni_removes + ["template_file", "defaults", "instances"]
|
||||
|
||||
@classproperty
|
||||
def basic_template(cls) -> bytes:
|
||||
"""
|
||||
@@ -1063,6 +1092,10 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
|
||||
Abstract of relationship between kits and their submission type.
|
||||
"""
|
||||
|
||||
omni_removes = BaseClass.omni_removes + ["submission_types_id", "kits_id"]
|
||||
omni_sort = ["submission_type", "kit_type"]
|
||||
level = 2
|
||||
|
||||
submission_types_id = Column(INTEGER, ForeignKey("_submissiontype.id"),
|
||||
primary_key=True) #: id of joined submission type
|
||||
kits_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of joined kit
|
||||
@@ -1093,12 +1126,17 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
|
||||
"""
|
||||
return f"<SubmissionTypeKitTypeAssociation({self.submission_type.name}&{self.kit_type.name})>"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return f"{self.submission_type.name} -> {self.kit_type.name}"
|
||||
|
||||
@classmethod
|
||||
@setup_lookup
|
||||
def query(cls,
|
||||
submission_type: SubmissionType | str | int | None = None,
|
||||
kit_type: KitType | str | int | None = None,
|
||||
limit: int = 0
|
||||
limit: int = 0,
|
||||
**kwargs
|
||||
) -> SubmissionTypeKitTypeAssociation | List[SubmissionTypeKitTypeAssociation]:
|
||||
"""
|
||||
Lookup SubmissionTypeKitTypeAssociations of interest.
|
||||
@@ -1126,7 +1164,9 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
|
||||
query = query.join(KitType).filter(KitType.name == kit_type)
|
||||
case int():
|
||||
query = query.join(KitType).filter(KitType.id == kit_type)
|
||||
limit = query.count()
|
||||
if kit_type is not None and submission_type is not None:
|
||||
limit = 1
|
||||
# limit = query.count()
|
||||
return cls.execute_query(query=query, limit=limit)
|
||||
|
||||
def to_export_dict(self):
|
||||
@@ -1148,6 +1188,9 @@ class KitTypeReagentRoleAssociation(BaseClass):
|
||||
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
|
||||
"""
|
||||
|
||||
omni_removes = BaseClass.omni_removes + ["submission_type_id", "kits_id", "reagent_roles_id", "last_used"]
|
||||
omni_sort = ["submission_type", "kit_type", "reagent_role", "required", "uses"]
|
||||
|
||||
reagent_roles_id = Column(INTEGER, ForeignKey("_reagentrole.id"),
|
||||
primary_key=True) #: id of associated reagent type
|
||||
kits_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of associated reagent type
|
||||
@@ -1176,6 +1219,10 @@ class KitTypeReagentRoleAssociation(BaseClass):
|
||||
def __repr__(self) -> str:
|
||||
return f"<KitTypeReagentRoleAssociation({self.kit_type} & {self.reagent_role})>"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return f"{self.kit_type.name} -> {self.reagent_role.name}"
|
||||
|
||||
@validates('required')
|
||||
def validate_required(self, key, value):
|
||||
"""
|
||||
@@ -1219,7 +1266,8 @@ class KitTypeReagentRoleAssociation(BaseClass):
|
||||
def query(cls,
|
||||
kit_type: KitType | str | None = None,
|
||||
reagent_role: ReagentRole | str | None = None,
|
||||
limit: int = 0
|
||||
limit: int = 0,
|
||||
**kwargs
|
||||
) -> KitTypeReagentRoleAssociation | List[KitTypeReagentRoleAssociation]:
|
||||
"""
|
||||
Lookup junction of ReagentType and KitType
|
||||
@@ -1280,6 +1328,12 @@ class KitTypeReagentRoleAssociation(BaseClass):
|
||||
for rel_reagent in relevant_reagents:
|
||||
yield rel_reagent
|
||||
|
||||
@property
|
||||
def omnigui_dict(self) -> dict:
|
||||
dicto = super().omnigui_dict
|
||||
dicto['required']['instance_attr'] = bool(dicto['required']['instance_attr'])
|
||||
return dicto
|
||||
|
||||
|
||||
class SubmissionReagentAssociation(BaseClass):
|
||||
"""
|
||||
@@ -1420,7 +1474,7 @@ class Equipment(BaseClass, LogMixin):
|
||||
|
||||
def get_processes(self, submission_type: str | SubmissionType | None = None,
|
||||
extraction_kit: str | KitType | None = None,
|
||||
equipment_role: str | EquipmentRole | None=None) -> List[str]:
|
||||
equipment_role: str | EquipmentRole | None = None) -> List[str]:
|
||||
"""
|
||||
Get all processes associated with this Equipment for a given SubmissionType
|
||||
|
||||
@@ -1498,7 +1552,8 @@ class Equipment(BaseClass, LogMixin):
|
||||
PydEquipment: pydantic equipment object
|
||||
"""
|
||||
from backend.validators.pydant import PydEquipment
|
||||
processes = self.get_processes(submission_type=submission_type, extraction_kit=extraction_kit, equipment_role=role)
|
||||
processes = self.get_processes(submission_type=submission_type, extraction_kit=extraction_kit,
|
||||
equipment_role=role)
|
||||
return PydEquipment(processes=processes, role=role,
|
||||
**self.to_dict(processes=False))
|
||||
|
||||
@@ -1718,7 +1773,8 @@ class SubmissionEquipmentAssociation(BaseClass):
|
||||
|
||||
@classmethod
|
||||
@setup_lookup
|
||||
def query(cls, equipment_id: int|None=None, submission_id: int|None=None, role: str | None = None, limit: int = 0, **kwargs) \
|
||||
def query(cls, equipment_id: int | None = None, submission_id: int | None = None, role: str | None = None,
|
||||
limit: int = 0, **kwargs) \
|
||||
-> Any | List[Any]:
|
||||
query: Query = cls.__database_session__.query(cls)
|
||||
query = query.filter(cls.equipment_id == equipment_id)
|
||||
@@ -1819,9 +1875,10 @@ class Process(BaseClass):
|
||||
name: str | None = None,
|
||||
id: int | None = None,
|
||||
submission_type: str | SubmissionType | None = None,
|
||||
extraction_kit : str | KitType | None = None,
|
||||
extraction_kit: str | KitType | None = None,
|
||||
equipment_role: str | KitType | None = None,
|
||||
limit: int = 0) -> Process | List[Process]:
|
||||
limit: int = 0,
|
||||
**kwargs) -> Process | List[Process]:
|
||||
"""
|
||||
Lookup Processes
|
||||
|
||||
@@ -1876,6 +1933,8 @@ class Process(BaseClass):
|
||||
def save(self):
|
||||
super().save()
|
||||
|
||||
# @classmethod
|
||||
|
||||
|
||||
class TipRole(BaseClass):
|
||||
"""
|
||||
@@ -2019,7 +2078,6 @@ class SubmissionTipsAssociation(BaseClass):
|
||||
instance = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=role)
|
||||
return instance
|
||||
|
||||
|
||||
def to_pydantic(self):
|
||||
from backend.validators import PydTips
|
||||
return PydTips(name=self.tips.name, lot=self.tips.lot, role=self.role_name)
|
||||
|
||||
@@ -28,6 +28,8 @@ class Organization(BaseClass):
|
||||
Base of organization
|
||||
"""
|
||||
|
||||
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
name = Column(String(64)) #: organization name
|
||||
submissions = relationship("BasicSubmission",
|
||||
@@ -124,8 +126,6 @@ class Contact(BaseClass):
|
||||
Base of Contact
|
||||
"""
|
||||
|
||||
searchables = []
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
name = Column(String(64)) #: contact name
|
||||
email = Column(String(64)) #: contact email
|
||||
@@ -137,6 +137,10 @@ class Contact(BaseClass):
|
||||
def __repr__(self) -> str:
|
||||
return f"<Contact({self.name})>"
|
||||
|
||||
@classproperty
|
||||
def searchables(cls):
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
@setup_lookup
|
||||
def query(cls,
|
||||
|
||||
@@ -1960,6 +1960,7 @@ class WastewaterArtic(BasicSubmission):
|
||||
input_dict['rsl_number'] = cls.en_adapter(input_str=input_dict['submitter_id'])
|
||||
# NOTE: Check for extraction negative control (Robotics)
|
||||
if re.search(rf"^{year}-(RSL)", input_dict['submitter_id']):
|
||||
logger.debug(f"Found {year}-(RSL), so we are going to run PBS adapter:")
|
||||
input_dict['rsl_number'] = cls.pbs_adapter(input_str=input_dict['submitter_id'])
|
||||
return input_dict
|
||||
|
||||
@@ -2019,7 +2020,9 @@ class WastewaterArtic(BasicSubmission):
|
||||
"""
|
||||
# NOTE: Remove letters.
|
||||
processed = input_str.replace("RSL", "")
|
||||
# NOTE: Remove brackets at end
|
||||
processed = re.sub(r"\(.*\)$", "", processed).strip()
|
||||
# NOTE: Remove any non-R letters at end.
|
||||
processed = re.sub(r"[A-QS-Z]+\d*", "", processed)
|
||||
# NOTE: Remove trailing '-' if any
|
||||
processed = processed.strip("-")
|
||||
@@ -2037,6 +2040,8 @@ class WastewaterArtic(BasicSubmission):
|
||||
if repeat_num is None and "R" in plate_num:
|
||||
repeat_num = "1"
|
||||
plate_num = re.sub(r"R", rf"R{repeat_num}", plate_num)
|
||||
# NOTE: Remove any redundant -digits
|
||||
processed = re.sub(r"-\d$", "", processed)
|
||||
day = re.search(r"\d{2}$", processed).group()
|
||||
processed = rreplace(processed, day, "")
|
||||
month = re.search(r"\d{2}$", processed).group()
|
||||
@@ -2237,8 +2242,6 @@ class BasicSample(BaseClass, LogMixin):
|
||||
Base of basic sample which polymorphs into BCSample and WWSample
|
||||
"""
|
||||
|
||||
searchables = [dict(label="Submitter ID", field="submitter_id")]
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
submitter_id = Column(String(64), nullable=False, unique=True) #: identification from submitter
|
||||
sample_type = Column(String(32)) #: mode_sub_type of sample
|
||||
@@ -2287,6 +2290,10 @@ class BasicSample(BaseClass, LogMixin):
|
||||
except AttributeError:
|
||||
return f"<Sample({self.submitter_id})"
|
||||
|
||||
@classproperty
|
||||
def searchables(cls):
|
||||
return [dict(label="Submitter ID", field="submitter_id")]
|
||||
|
||||
@classproperty
|
||||
def timestamps(cls) -> List[str]:
|
||||
"""
|
||||
@@ -2657,7 +2664,7 @@ class WastewaterSample(BasicSample):
|
||||
Returns:
|
||||
List[str]: List of fields.
|
||||
"""
|
||||
searchables = super().searchables
|
||||
searchables = deepcopy(super().searchables)
|
||||
for item in ["ww_processing_num", "ww_full_sample_id", "rsl_number"]:
|
||||
label = item.strip("ww_").replace("_", " ").replace("rsl", "RSL").title()
|
||||
searchables.append(dict(label=label, field=item))
|
||||
|
||||
Reference in New Issue
Block a user