Moments before disaster

This commit is contained in:
lwark
2025-08-14 09:52:01 -05:00
parent 6f58030e75
commit 87efb18518
6 changed files with 519 additions and 455 deletions

View File

@@ -2,11 +2,7 @@
Contains all models for sqlalchemy Contains all models for sqlalchemy
""" """
from __future__ import annotations from __future__ import annotations
import sys, logging, json import sys, logging, json
from collections import OrderedDict
import sqlalchemy.exc
from dateutil.parser import parse from dateutil.parser import parse
from pandas import DataFrame from pandas import DataFrame
from pydantic import BaseModel from pydantic import BaseModel
@@ -21,7 +17,6 @@ from pathlib import Path
from sqlalchemy.orm.relationships import _RelationshipDeclared from sqlalchemy.orm.relationships import _RelationshipDeclared
from tools import report_result, list_sort_dict from tools import report_result, list_sort_dict
# NOTE: Load testing environment # NOTE: Load testing environment
if 'pytest' in sys.modules: if 'pytest' in sys.modules:
sys.path.append(Path(__file__).parents[4].absolute().joinpath("tests").__str__()) sys.path.append(Path(__file__).parents[4].absolute().joinpath("tests").__str__())
@@ -41,9 +36,9 @@ class BaseClass(Base):
__table_args__ = {'extend_existing': True} #: NOTE Will only add new columns __table_args__ = {'extend_existing': True} #: NOTE Will only add new columns
singles = ['id'] singles = ['id']
omni_removes = ["id", 'run', "omnigui_class_dict", "omnigui_instance_dict"] # omni_removes = ["id", 'run', "omnigui_class_dict", "omnigui_instance_dict"]
omni_sort = ["name"] # omni_sort = ["name"]
omni_inheritable = [] # omni_inheritable = []
searchables = [] searchables = []
_misc_info = Column(JSON) _misc_info = Column(JSON)
@@ -242,12 +237,13 @@ class BaseClass(Base):
@classmethod @classmethod
def query_or_create(cls, **kwargs) -> Tuple[Any, bool]: def query_or_create(cls, **kwargs) -> Tuple[Any, bool]:
new = False new = False
allowed = [k for k, v in cls.__dict__.items() if isinstance(v, InstrumentedAttribute) or isinstance(v, hybrid_property)] allowed = [k for k, v in cls.__dict__.items() if
isinstance(v, InstrumentedAttribute) or isinstance(v, hybrid_property)]
# and not isinstance(v.property, _RelationshipDeclared)] # and not isinstance(v.property, _RelationshipDeclared)]
sanitized_kwargs = {k: v for k, v in kwargs.items() if k in allowed} sanitized_kwargs = {k: v for k, v in kwargs.items() if k in allowed}
outside_kwargs = {k: v for k, v in kwargs.items() if k not in allowed} outside_kwargs = {k: v for k, v in kwargs.items() if k not in allowed}
# logger.debug(f"Sanitized kwargs: {sanitized_kwargs}") logger.debug(f"Sanitized kwargs: {sanitized_kwargs}")
instance = cls.query(**sanitized_kwargs) instance = cls.query(limit=1, **sanitized_kwargs)
if not instance or isinstance(instance, list): if not instance or isinstance(instance, list):
instance = cls() instance = cls()
new = True new = True
@@ -280,7 +276,8 @@ class BaseClass(Base):
return cls.execute_query(**kwargs) return cls.execute_query(**kwargs)
@classmethod @classmethod
def execute_query(cls, query: Query = None, model=None, limit: int = 0, offset:int|None=None, **kwargs) -> Any | List[Any]: def execute_query(cls, query: Query = None, model=None, limit: int = 0, offset: int | None = None,
**kwargs) -> Any | List[Any]:
""" """
Execute sqlalchemy query with relevant defaults. Execute sqlalchemy query with relevant defaults.
@@ -610,12 +607,12 @@ class BaseClass(Base):
output_date = datetime.combine(output_date, addition_time).strftime("%Y-%m-%d %H:%M:%S") output_date = datetime.combine(output_date, addition_time).strftime("%Y-%m-%d %H:%M:%S")
return output_date return output_date
def details_dict(self, **kwargs): def details_dict(self, **kwargs) -> dict:
relevant = {k: v for k, v in self.__class__.__dict__.items() if relevant = {k: v for k, v in self.__class__.__dict__.items() if
isinstance(v, InstrumentedAttribute) or isinstance(v, AssociationProxy)} isinstance(v, InstrumentedAttribute) or isinstance(v, AssociationProxy)}
# output = OrderedDict() # output = OrderedDict()
output = dict(excluded = ["excluded", "misc_info", "_misc_info", "id"]) output = dict(excluded=["excluded", "misc_info", "_misc_info", "id"])
for k, v in relevant.items(): for k, v in relevant.items():
try: try:
check = v.foreign_keys check = v.foreign_keys
@@ -666,7 +663,7 @@ class BaseClass(Base):
output[k] = value output[k] = value
return output return output
def to_pydantic(self, pyd_model_name:str|None=None, **kwargs): def to_pydantic(self, pyd_model_name: str | None = None, **kwargs):
from backend.validators import pydant from backend.validators import pydant
if not pyd_model_name: if not pyd_model_name:
pyd_model_name = f"Pyd{self.__class__.__name__}" pyd_model_name = f"Pyd{self.__class__.__name__}"
@@ -685,7 +682,7 @@ class BaseClass(Base):
if dlg.exec(): if dlg.exec():
pass pass
def export(self, obj, output_filepath: str|Path|None=None): def export(self, obj, output_filepath: str | Path | None = None):
# if not hasattr(self, "template_file"): # if not hasattr(self, "template_file"):
# logger.error(f"Export not implemented for {self.__class__.__name__}") # logger.error(f"Export not implemented for {self.__class__.__name__}")
# return # return

View File

@@ -44,13 +44,13 @@ equipmentrole_equipment = Table(
extend_existing=True extend_existing=True
) )
equipment_process = Table( # equipment_process = Table(
"_equipment_process", # "_equipment_process",
Base.metadata, # Base.metadata,
Column("process_id", INTEGER, ForeignKey("_process.id")), # Column("process_id", INTEGER, ForeignKey("_process.id")),
Column("equipment_id", INTEGER, ForeignKey("_equipment.id")), # Column("equipment_id", INTEGER, ForeignKey("_equipment.id")),
extend_existing=True # extend_existing=True
) # )
equipmentrole_process = Table( equipmentrole_process = Table(
"_equipmentrole_process", "_equipmentrole_process",
@@ -68,15 +68,15 @@ equipmentrole_process = Table(
# extend_existing=True # extend_existing=True
# ) # )
tiprole_tips = Table( # tiprole_tips = Table(
"_tiprole_tips", # "_tiprole_tips",
Base.metadata, # Base.metadata,
Column("tiprole_id", INTEGER, ForeignKey("_tiprole.id")), # Column("tiprole_id", INTEGER, ForeignKey("_tiprole.id")),
Column("tips_id", INTEGER, ForeignKey("_tips.id")), # Column("tips_id", INTEGER, ForeignKey("_tips.id")),
extend_existing=True # extend_existing=True
) # )
process_tiprole = Table( process_tips = Table(
"_process_tiprole", "_process_tiprole",
Base.metadata, Base.metadata,
Column("process_id", INTEGER, ForeignKey("_process.id")), Column("process_id", INTEGER, ForeignKey("_process.id")),
@@ -84,13 +84,13 @@ process_tiprole = Table(
extend_existing=True extend_existing=True
) )
equipment_tips = Table( # equipment_tips = Table(
"_equipment_tips", # "_equipment_tips",
Base.metadata, # Base.metadata,
Column("equipment_id", INTEGER, ForeignKey("_equipment.id")), # Column("equipment_id", INTEGER, ForeignKey("_equipment.id")),
Column("tips_id", INTEGER, ForeignKey("_tips.id")), # Column("tips_id", INTEGER, ForeignKey("_tips.id")),
extend_existing=True # extend_existing=True
) # )
# kittype_procedure = Table( # kittype_procedure = Table(
# "_kittype_procedure", # "_kittype_procedure",
@@ -100,13 +100,13 @@ equipment_tips = Table(
# extend_existing=True # extend_existing=True
# ) # )
proceduretype_process = Table( # proceduretype_process = Table(
"_proceduretype_process", # "_proceduretype_process",
Base.metadata, # Base.metadata,
Column("process_id", INTEGER, ForeignKey("_process.id")), # Column("process_id", INTEGER, ForeignKey("_process.id")),
Column("proceduretype_id", INTEGER, ForeignKey("_proceduretype.id")), # Column("proceduretype_id", INTEGER, ForeignKey("_proceduretype.id")),
extend_existing=True # extend_existing=True
) # )
submissiontype_proceduretype = Table( submissiontype_proceduretype = Table(
"_submissiontype_proceduretype", "_submissiontype_proceduretype",
@@ -823,7 +823,7 @@ class Reagent(BaseClass, LogMixin):
class ReagentLot(BaseClass): class ReagentLot(BaseClass):
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
lot = Column(String(64)) #: lot number of reagent lot = Column(String(64), unique=True) #: lot number of reagent
expiry = Column(TIMESTAMP) #: expiry date - extended by eol_ext of parent programmatically expiry = Column(TIMESTAMP) #: expiry date - extended by eol_ext of parent programmatically
reagent = relationship("Reagent") #: joined parent reagent type reagent = relationship("Reagent") #: joined parent reagent type
reagent_id = Column(INTEGER, ForeignKey("_reagent.id", ondelete='SET NULL', reagent_id = Column(INTEGER, ForeignKey("_reagent.id", ondelete='SET NULL',
@@ -843,6 +843,25 @@ class ReagentLot(BaseClass):
def name(self): def name(self):
return self.lot return self.lot
@classmethod
def query(cls,
lot: str | None = None,
name: str | None = None,
limit: int = 1,
**kwargs) -> ReagentLot | List[ReagentLot]:
query: Query = cls.__database_session__.query(cls)
match lot:
case str():
query = query.filter(cls.lot == lot)
case _:
pass
match name:
case str():
query = query.join(Reagent).filter(Reagent.name == name)
case _:
pass
return cls.execute_query(query=query, limit=limit)
def __repr__(self): def __repr__(self):
return f"<Lot({self.lot}-{self.expiry}>" return f"<Lot({self.lot}-{self.expiry}>"
@@ -1447,7 +1466,7 @@ class Procedure(BaseClass):
) #: Relation to ProcedureReagentAssociation ) #: Relation to ProcedureReagentAssociation
reagentlot = association_proxy("procedurereagentlotassociation", reagentlot = association_proxy("procedurereagentlotassociation",
"reagentlot", creator=lambda reg: ProcedureReagentLotAssociation( "reagentlot", creator=lambda reg: ProcedureReagentLotAssociation(
reagent=reg)) #: Association proxy to RunReagentAssociation.reagent reagent=reg)) #: Association proxy to RunReagentAssociation.reagent
procedureequipmentassociation = relationship( procedureequipmentassociation = relationship(
@@ -1459,13 +1478,13 @@ class Procedure(BaseClass):
equipment = association_proxy("procedureequipmentassociation", equipment = association_proxy("procedureequipmentassociation",
"equipment") #: Association proxy to RunEquipmentAssociation.equipment "equipment") #: Association proxy to RunEquipmentAssociation.equipment
proceduretipsassociation = relationship( # proceduretipsassociation = relationship(
"ProcedureTipsAssociation", # "ProcedureTipsAssociation",
back_populates="procedure", # back_populates="procedure",
cascade="all, delete-orphan") # cascade="all, delete-orphan")
#
tips = association_proxy("proceduretipsassociation", # tips = association_proxy("proceduretipsassociation",
"tips") # "tips")
@validates('repeat') @validates('repeat')
def validate_repeat(self, key, value): def validate_repeat(self, key, value):
@@ -1477,7 +1496,8 @@ class Procedure(BaseClass):
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, id: int | None = None, name: str | None = None, start_date: date | datetime | str | int | None = None, def query(cls, id: int | None = None, name: str | None = None,
start_date: date | datetime | str | int | None = None,
end_date: date | datetime | str | int | None = None, limit: int = 0, **kwargs) -> Procedure | List[ end_date: date | datetime | str | int | None = None, limit: int = 0, **kwargs) -> Procedure | List[
Procedure]: Procedure]:
query: Query = cls.__database_session__.query(cls) query: Query = cls.__database_session__.query(cls)
@@ -1610,10 +1630,7 @@ class Procedure(BaseClass):
output['tips'] = [tips.details_dict() for tips in output['proceduretipsassociation']] output['tips'] = [tips.details_dict() for tips in output['proceduretipsassociation']]
output['repeat'] = bool(output['repeat']) output['repeat'] = bool(output['repeat'])
output['run'] = self.run.name output['run'] = self.run.name
output['excluded'] += ['id', "results", "proceduresampleassociation", "sample", output['excluded'] += self.get_default_info("details_ignore")
"procedurereagentlotassociation",
"procedureequipmentassociation", "proceduretipsassociation", "reagent", "equipment",
"tips", "control", "kittype"]
output['sample_count'] = len(active_samples) output['sample_count'] = len(active_samples)
output['clientlab'] = self.run.clientsubmission.clientlab.name output['clientlab'] = self.run.clientsubmission.clientlab.name
output['cost'] = 0.00 output['cost'] = 0.00
@@ -1670,11 +1687,17 @@ class Procedure(BaseClass):
def get_default_info(cls, *args) -> dict | list | str: def get_default_info(cls, *args) -> dict | list | str:
dicto = super().get_default_info() dicto = super().get_default_info()
recover = ['filepath', 'sample', 'csv', 'comment', 'equipment'] recover = ['filepath', 'sample', 'csv', 'comment', 'equipment']
# ['id', "results", "proceduresampleassociation", "sample",
# "procedurereagentlotassociation",
# "procedureequipmentassociation", "proceduretipsassociation", "reagent", "equipment",
# "tips", "control", "kittype"]
dicto.update(dict( dicto.update(dict(
details_ignore=['excluded', 'reagents', 'sample', details_ignore=['excluded', 'reagents', 'sample', 'extraction_info', 'comment', 'barcode',
'extraction_info', 'comment', 'barcode', 'platemap', 'export_map', 'equipment', 'tips', 'custom', 'reagentlot', 'reagent_lot',
'platemap', 'export_map', 'equipment', 'tips', 'custom', 'reagentlot', "results", "proceduresampleassociation", "sample",
'procedurereagentassociation'], "procedurereagentlotassociation",
"procedureequipmentassociation", "proceduretipsassociation", "reagent", "equipment",
"tips", "control"],
# NOTE: Fields not placed in ui form # NOTE: Fields not placed in ui form
form_ignore=['reagents', 'ctx', 'id', 'cost', 'extraction_info', 'signed_by', 'comment', 'namer', form_ignore=['reagents', 'ctx', 'id', 'cost', 'extraction_info', 'signed_by', 'comment', 'namer',
'submission_object', "tips", 'contact_phone', 'custom', 'cost_centre', 'completed_date', 'submission_object', "tips", 'contact_phone', 'custom', 'cost_centre', 'completed_date',
@@ -1683,11 +1706,15 @@ class Procedure(BaseClass):
form_recover=recover form_recover=recover
)) ))
if args: if args:
output = {k: v for k, v in dicto.items() if k in args} if len(args) > 1:
output = {k: v for k, v in dicto.items() if k in args}
else:
output = dicto[args[0]]
else: else:
output = {k: v for k, v in dicto.items()} output = {k: v for k, v in dicto.items()}
return output return output
# class ProcedureTypeKitTypeAssociation(BaseClass): # class ProcedureTypeKitTypeAssociation(BaseClass):
# """ # """
# Abstract of relationship between kits and their procedure type. # Abstract of relationship between kits and their procedure type.
@@ -1824,9 +1851,9 @@ class ProcedureTypeReagentRoleAssociation(BaseClass):
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html 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_removes = BaseClass.omni_removes + ["submission_type_id", "kits_id", "reagent_roles_id", "last_used"]
omni_sort = ["proceduretype", "kittype", "reagentrole", "required", "uses"] # omni_sort = ["proceduretype", "kittype", "reagentrole", "required", "uses"]
omni_inheritable = ["proceduretype", "kittype"] # omni_inheritable = ["proceduretype", "kittype"]
reagentrole_id = Column(INTEGER, ForeignKey("_reagentrole.id"), reagentrole_id = Column(INTEGER, ForeignKey("_reagentrole.id"),
primary_key=True) #: id of associated reagent type primary_key=True) #: id of associated reagent type
@@ -2076,7 +2103,7 @@ class ProcedureReagentLotAssociation(BaseClass):
str: Representation of this RunReagentAssociation str: Representation of this RunReagentAssociation
""" """
try: try:
return f"<ProcedureReagentAssociation({self.procedure.procedure.rsl_plate_number} & {self.reagent.lot})>" return f"<ProcedureReagentLotAssociation({self.procedure.name} & {self.reagent.lot})>"
except AttributeError: except AttributeError:
try: try:
logger.error(f"Reagent {self.reagent.lot} procedure association {self.reagent_id} has no procedure!") logger.error(f"Reagent {self.reagent.lot} procedure association {self.reagent_id} has no procedure!")
@@ -2584,189 +2611,12 @@ class EquipmentRole(BaseClass):
return output return output
class ProcedureEquipmentAssociation(BaseClass):
"""
Abstract association between BasicRun and Equipment
"""
equipment_id = Column(INTEGER, ForeignKey("_equipment.id"), primary_key=True) #: id of associated equipment
procedure_id = Column(INTEGER, ForeignKey("_procedure.id"), primary_key=True) #: id of associated procedure
equipmentrole = Column(String(64), primary_key=True) #: name of the reagentrole the equipment fills
processversion_id = Column(INTEGER, ForeignKey("_processversion.id", ondelete="SET NULL",
name="SEA_Process_id")) #: Foreign key of process id
start_time = Column(TIMESTAMP) #: start time of equipment use
end_time = Column(TIMESTAMP) #: end time of equipment use
comments = Column(String(1024)) #: comments about equipment
procedure = relationship(Procedure,
back_populates="procedureequipmentassociation") #: associated procedure
equipment = relationship(Equipment, back_populates="equipmentprocedureassociation") #: associated equipment
tips_id = Column(INTEGER, ForeignKey("_tips.id", ondelete="SET NULL",
name="SEA_Process_id"))
def __repr__(self) -> str:
try:
return f"<ProcedureEquipmentAssociation({self.name})>"
except AttributeError:
return "<ProcedureEquipmentAssociation(Unknown)>"
def __init__(self, procedure=None, equipment=None, procedure_id: int | None = None, equipment_id: int | None = None,
equipmentrole: str = "None"):
if not procedure:
if procedure_id:
procedure = Procedure.query(id=procedure_id)
else:
logger.error("Creation error")
self.procedure = procedure
if not equipment:
if equipment_id:
equipment = Equipment.query(id=equipment_id)
else:
logger.error("Creation error")
self.equipment = equipment
if isinstance(equipmentrole, list):
equipmentrole = equipmentrole[0]
if isinstance(equipmentrole, EquipmentRole):
equipmentrole = equipmentrole.name
self.equipmentrole = equipmentrole
@property
def name(self):
return f"{self.procedure.name} & {self.equipment.name}"
@property
def process(self):
return ProcessVersion.query(id=self.processversion_id)
@property
def tips(self):
try:
return Tips.query(id=self.tips_id, limit=1)
except AttributeError:
return None
def to_sub_dict(self) -> dict:
"""
This RunEquipmentAssociation as a dictionary
Returns:
dict: This RunEquipmentAssociation as a dictionary
"""
try:
process = self.process.name
except AttributeError:
process = "No process found"
output = dict(name=self.equipment.name, asset_number=self.equipment.asset_number, comment=self.comments,
processes=[process], role=self.equipmentrole, nickname=self.equipment.nickname)
return output
def to_pydantic(self) -> "PydEquipment":
"""
Returns a pydantic model based on this object.
Returns:
PydEquipment: pydantic equipment model
"""
from backend.validators import PydEquipment
return PydEquipment(**self.details_dict())
@classmethod
@setup_lookup
def query(cls,
equipment: int | Equipment | None = None,
procedure: int | Procedure | None = None,
equipmentrole: str | None = None,
limit: int = 0, **kwargs) \
-> Any | List[Any]:
query: Query = cls.__database_session__.query(cls)
match equipment:
case int():
query = query.filter(cls.equipment_id == equipment)
case Equipment():
query = query.filter(cls.equipment == equipment)
case _:
pass
match procedure:
case int():
query = query.filter(cls.procedure_id == procedure)
case Procedure():
query = query.filter(cls.procedure == procedure)
case _:
pass
if equipmentrole is not None:
query = query.filter(cls.equipmentrole == equipmentrole)
return cls.execute_query(query=query, limit=limit, **kwargs)
def details_dict(self, **kwargs):
output = super().details_dict()
# NOTE: Figure out how to merge the misc_info if doing .update instead.
relevant = {k: v for k, v in output.items() if k not in ['equipment']}
output = output['equipment'].details_dict()
misc = output['misc_info']
output.update(relevant)
output['misc_info'] = misc
output['equipment_role'] = self.equipmentrole
output['process'] = self.process.details_dict()
try:
output['tips'] = self.tips.details_dict()
except AttributeError:
output['tips'] = None
return output
class ProcedureTypeEquipmentRoleAssociation(BaseClass):
"""
Abstract association between SubmissionType and EquipmentRole
"""
equipmentrole_id = Column(INTEGER, ForeignKey("_equipmentrole.id"), primary_key=True) #: id of associated equipment
proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id"),
primary_key=True) #: id of associated procedure
# kittype_id = Column(INTEGER, ForeignKey("_kittype.id"),
# primary_key=True)
uses = Column(JSON) #: locations of equipment on the procedure type excel sheet.
static = Column(INTEGER,
default=1) #: if 1 this piece of equipment will always be used, otherwise it will need to be selected from list?
proceduretype = relationship(ProcedureType,
back_populates="proceduretypeequipmentroleassociation") #: associated procedure
equipmentrole = relationship(EquipmentRole,
back_populates="equipmentroleproceduretypeassociation") #: associated equipment
# kittype = relationship(KitType, back_populates="proceduretypeequipmentroleassociation")
@validates('static')
def validate_static(self, key, value):
"""
Ensures only 1 & 0 used in 'static'
Args:
key (str): name of attribute
value (_type_): value of attribute
Raises:
ValueError: Raised if bad value given
Returns:
_type_: value
"""
if not 0 <= value < 2:
raise ValueError(f'Invalid required value {value}. Must be 0 or 1.')
return value
@check_authorization
def save(self):
super().save()
class Process(BaseClass): class Process(BaseClass):
""" """
A Process is a method used by a piece of equipment. A Process is a method used by a piece of equipment.
""" """
level = 2
id = Column(INTEGER, primary_key=True) #: Process id, primary key id = Column(INTEGER, primary_key=True) #: Process id, primary key
name = Column(String(64), unique=True) #: Process name name = Column(String(64), unique=True) #: Process name
@@ -2885,20 +2735,20 @@ class Process(BaseClass):
def details_dict(self, **kwargs): def details_dict(self, **kwargs):
output = super().details_dict(**kwargs) output = super().details_dict(**kwargs)
output['processversion'] = [item.details_dict() for item in self.processversion] output['processversion'] = [item.details_dict() for item in self.processversion]
logger.debug(f"Process output dict: {pformat(output)}") # logger.debug(f"Process output dict: {pformat(output)}")
return output return output
def to_pydantic(self): # def to_pydantic(self):
from backend.validators.pydant import PydProcess # from backend.validators.pydant import PydProcess
output = {} # output = {}
for k, v in self.details_dict().items(): # for k, v in self.details_dict().items():
if isinstance(v, list): # if isinstance(v, list):
output[k] = [item.name if issubclass(item.__class__, BaseClass) else item for item in v] # output[k] = [item.name if issubclass(item.__class__, BaseClass) else item for item in v]
elif issubclass(v.__class__, BaseClass): # elif issubclass(v.__class__, BaseClass):
output[k] = v.name # output[k] = v.name
else: # else:
output[k] = v # output[k] = v
return PydProcess(**output) # return PydProcess(**output)
class ProcessVersion(BaseClass): class ProcessVersion(BaseClass):
@@ -2923,26 +2773,51 @@ class ProcessVersion(BaseClass):
output['project'] = "" output['project'] = ""
return output return output
def set_attribute(self, key, value):
setattr(self, key, value)
class TipRole(BaseClass): @classmethod
def query(cls,
version: str | None = None,
name: str | None = None,
limit: int = 0,
**kwargs) -> ReagentLot | List[ReagentLot]:
query: Query = cls.__database_session__.query(cls)
match name:
case str():
query = query.join(Process).filter(Process.name == name)
case _:
pass
match version:
case str():
query = query.filter(cls.version == version)
case _:
pass
return cls.execute_query(query=query, limit=limit)
class Tips(BaseClass):
""" """
An abstract reagentrole that a tip fills during a process An abstract reagentrole that a tip fills during a process
""" """
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: name of reagent type name = Column(String(64)) #: name of reagent type
tips = relationship("Tips", back_populates="tiprole", tipslot = relationship("TipsLot", back_populates="tips",
secondary=tiprole_tips) #: concrete control of this reagent type secondary=tiprole_tips) #: concrete control of this reagent type
process = relationship("Process", back_populates="tiprole", secondary=process_tiprole) manufacturer = Column(String(64))
capacity = Column(INTEGER)
ref = Column(String(64)) #: tip reference number
process = relationship("Process", back_populates="tips", secondary=process_tiprole)
tiproleproceduretypeassociation = relationship( # tiproleproceduretypeassociation = relationship(
"ProcedureTypeTipRoleAssociation", # "ProcedureTypeTipRoleAssociation",
back_populates="tiprole", # back_populates="tiprole",
cascade="all, delete-orphan" # cascade="all, delete-orphan"
) #: associated procedure # ) #: associated procedure
#
proceduretype = association_proxy("tiproleproceduretypeassociation", "proceduretype", # proceduretype = association_proxy("tiproleproceduretypeassociation", "proceduretype",
creator=lambda proceduretype: ProcedureTypeTipRoleAssociation( # creator=lambda proceduretype: ProcedureTypeTipRoleAssociation(
proceduretype=proceduretype)) # proceduretype=proceduretype))
# @classmethod # @classmethod
# def query_or_create(cls, **kwargs) -> Tuple[TipRole, bool]: # def query_or_create(cls, **kwargs) -> Tuple[TipRole, bool]:
@@ -2990,35 +2865,37 @@ class TipRole(BaseClass):
) )
class Tips(BaseClass, LogMixin): class TipsLot(BaseClass, LogMixin):
""" """
A concrete instance of tips. A concrete instance of tips.
""" """
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
tiprole = relationship("TipRole", back_populates="tips", tips = relationship("Tips", back_populates="tipslot",
secondary=tiprole_tips) #: joined parent reagent type secondary=tiprole_tips) #: joined parent reagent type
tiprole_id = Column(INTEGER, ForeignKey("_tiprole.id", ondelete='SET NULL', tips_id = Column(INTEGER, ForeignKey("_tips.id", ondelete='SET NULL',
name="fk_tip_role_id")) #: id of parent reagent type name="fk_tips_id")) #: id of parent reagent type
manufacturer = Column(String(64)) lot = Column(String(64), unique=True)
capacity = Column(INTEGER) expiry = Column(TIMESTAMP)
ref = Column(String(64)) #: tip reference number
# lot = Column(String(64)) #: lot number of tips
equipment = relationship("Equipment", back_populates="tips",
secondary=equipment_tips) #: associated procedure
tipsprocedureassociation = relationship(
"ProcedureTipsAssociation",
back_populates="tips",
cascade="all, delete-orphan"
) #: associated procedure
procedure = association_proxy("tipsprocedureassociation", 'procedure') # lot = Column(String(64)) #: lot number of tips
# equipment = relationship("Equipment", back_populates="tips",
# secondary=equipment_tips) #: associated procedure
# tipsprocedureassociation = relationship(
# "ProcedureTipsAssociation",
# back_populates="tips",
# cascade="all, delete-orphan"
# ) #: associated procedure
#
# procedure = association_proxy("tipsprocedureassociation", 'procedure')
procedureequipmentassociation =
@property @property
def size(self) -> str: def size(self) -> str:
return f"{self.capacity}ul" return f"{self.capacity}ul"
@property @property
def name (self) -> str: def name(self) -> str:
return f"{self.manufacturer}-{self.size}-{self.ref}" return f"{self.manufacturer}-{self.size}-{self.ref}"
# @classmethod # @classmethod
@@ -3121,6 +2998,187 @@ class Tips(BaseClass, LogMixin):
return output return output
class ProcedureEquipmentAssociation(BaseClass):
"""
Abstract association between BasicRun and Equipment
"""
equipment_id = Column(INTEGER, ForeignKey("_equipment.id"), primary_key=True) #: id of associated equipment
procedure_id = Column(INTEGER, ForeignKey("_procedure.id"), primary_key=True) #: id of associated procedure
equipmentrole = Column(String(64), primary_key=True) #: name of the reagentrole the equipment fills
processversion_id = Column(INTEGER, ForeignKey("_processversion.id", ondelete="SET NULL",
name="SEA_Process_id")) #: Foreign key of process id
start_time = Column(TIMESTAMP) #: start time of equipment use
end_time = Column(TIMESTAMP) #: end time of equipment use
comments = Column(String(1024)) #: comments about equipment
procedure = relationship(Procedure,
back_populates="procedureequipmentassociation") #: associated procedure
equipment = relationship(Equipment, back_populates="equipmentprocedureassociation") #: associated equipment
processversion = relationship(ProcessVersion)
tips_id = Column(INTEGER, ForeignKey("_tips.id", ondelete="SET NULL",
name="SEA_Process_id"))
tips = relationship(Tips)
def __repr__(self) -> str:
try:
return f"<ProcedureEquipmentAssociation({self.name})>"
except AttributeError:
return "<ProcedureEquipmentAssociation(Unknown)>"
def __init__(self, procedure=None, equipment=None, procedure_id: int | None = None, equipment_id: int | None = None,
equipmentrole: str = "None"):
if not procedure:
if procedure_id:
procedure = Procedure.query(id=procedure_id)
else:
logger.error("Creation error")
self.procedure = procedure
if not equipment:
if equipment_id:
equipment = Equipment.query(id=equipment_id)
else:
logger.error("Creation error")
self.equipment = equipment
if isinstance(equipmentrole, list):
equipmentrole = equipmentrole[0]
if isinstance(equipmentrole, EquipmentRole):
equipmentrole = equipmentrole.name
self.equipmentrole = equipmentrole
@property
def name(self):
return f"{self.procedure.name} & {self.equipment.name}"
@property
def process(self):
return ProcessVersion.query(id=self.processversion_id)
@property
def tips(self):
try:
return Tips.query(id=self.tips_id, limit=1)
except AttributeError:
return None
def to_sub_dict(self) -> dict:
"""
This RunEquipmentAssociation as a dictionary
Returns:
dict: This RunEquipmentAssociation as a dictionary
"""
try:
process = self.process.name
except AttributeError:
process = "No process found"
output = dict(name=self.equipment.name, asset_number=self.equipment.asset_number, comment=self.comments,
processes=[process], role=self.equipmentrole, nickname=self.equipment.nickname)
return output
def to_pydantic(self) -> "PydEquipment":
"""
Returns a pydantic model based on this object.
Returns:
PydEquipment: pydantic equipment model
"""
from backend.validators import PydEquipment
return PydEquipment(**self.details_dict())
@classmethod
@setup_lookup
def query(cls,
equipment: int | Equipment | None = None,
procedure: int | Procedure | None = None,
equipmentrole: str | None = None,
limit: int = 0, **kwargs) \
-> Any | List[Any]:
query: Query = cls.__database_session__.query(cls)
match equipment:
case int():
query = query.filter(cls.equipment_id == equipment)
case Equipment():
query = query.filter(cls.equipment == equipment)
case _:
pass
match procedure:
case int():
query = query.filter(cls.procedure_id == procedure)
case Procedure():
query = query.filter(cls.procedure == procedure)
case _:
pass
if equipmentrole is not None:
query = query.filter(cls.equipmentrole == equipmentrole)
return cls.execute_query(query=query, limit=limit, **kwargs)
def details_dict(self, **kwargs):
output = super().details_dict()
# NOTE: Figure out how to merge the misc_info if doing .update instead.
relevant = {k: v for k, v in output.items() if k not in ['equipment']}
output = output['equipment'].details_dict()
misc = output['misc_info']
output.update(relevant)
output['misc_info'] = misc
output['equipment_role'] = self.equipmentrole
output['processversion'] = self.processversion.details_dict()
try:
output['tips'] = self.tips.details_dict()
except AttributeError:
output['tips'] = None
return output
class ProcedureTypeEquipmentRoleAssociation(BaseClass):
"""
Abstract association between SubmissionType and EquipmentRole
"""
equipmentrole_id = Column(INTEGER, ForeignKey("_equipmentrole.id"), primary_key=True) #: id of associated equipment
proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id"),
primary_key=True) #: id of associated procedure
# kittype_id = Column(INTEGER, ForeignKey("_kittype.id"),
# primary_key=True)
uses = Column(JSON) #: locations of equipment on the procedure type excel sheet.
static = Column(INTEGER,
default=1) #: if 1 this piece of equipment will always be used, otherwise it will need to be selected from list?
proceduretype = relationship(ProcedureType,
back_populates="proceduretypeequipmentroleassociation") #: associated procedure
equipmentrole = relationship(EquipmentRole,
back_populates="equipmentroleproceduretypeassociation") #: associated equipment
@validates('static')
def validate_static(self, key, value):
"""
Ensures only 1 & 0 used in 'static'
Args:
key (str): name of attribute
value (_type_): value of attribute
Raises:
ValueError: Raised if bad value given
Returns:
_type_: value
"""
if not 0 <= value < 2:
raise ValueError(f'Invalid required value {value}. Must be 0 or 1.')
return value
@check_authorization
def save(self):
super().save()
class ProcedureTypeTipRoleAssociation(BaseClass): class ProcedureTypeTipRoleAssociation(BaseClass):
""" """
Abstract association between SubmissionType and TipRole Abstract association between SubmissionType and TipRole
@@ -3144,71 +3202,71 @@ class ProcedureTypeTipRoleAssociation(BaseClass):
pass pass
class ProcedureTipsAssociation(BaseClass): # class ProcedureTipsAssociation(BaseClass):
""" # """
Association between a concrete procedure instance and concrete tips # Association between a concrete procedure instance and concrete tips
""" # """
tips_id = Column(INTEGER, ForeignKey("_tips.id"), primary_key=True) #: id of associated equipment # tips_id = Column(INTEGER, ForeignKey("_tips.id"), primary_key=True) #: id of associated equipment
procedure_id = Column(INTEGER, ForeignKey("_procedure.id"), primary_key=True) #: id of associated procedure # procedure_id = Column(INTEGER, ForeignKey("_procedure.id"), primary_key=True) #: id of associated procedure
procedure = relationship("Procedure", # procedure = relationship("Procedure",
back_populates="proceduretipsassociation") #: associated procedure # back_populates="proceduretipsassociation") #: associated procedure
tips = relationship(Tips, # tips = relationship(Tips,
back_populates="tipsprocedureassociation") #: associated equipment # back_populates="tipsprocedureassociation") #: associated equipment
tiprole = Column(String(32), primary_key=True) #, ForeignKey("_tiprole.name")) # tiprole = Column(String(32), primary_key=True) #, ForeignKey("_tiprole.name"))
#
def to_sub_dict(self) -> dict: # def to_sub_dict(self) -> dict:
""" # """
This item as a dictionary # This item as a dictionary
#
Returns: # Returns:
dict: Values of this object # dict: Values of this object
""" # """
return dict(role=self.role_name, name=self.tips.name, lot=self.tips.lot) # return dict(role=self.role_name, name=self.tips.name, lot=self.tips.lot)
#
@classmethod # @classmethod
@setup_lookup # @setup_lookup
def query(cls, tips: int | Tips, tiprole: str, procedure: int | Procedure | None = None, limit: int = 0, **kwargs) \ # def query(cls, tips: int | Tips, tiprole: str, procedure: int | Procedure | None = None, limit: int = 0, **kwargs) \
-> Any | List[Any]: # -> Any | List[Any]:
query: Query = cls.__database_session__.query(cls) # query: Query = cls.__database_session__.query(cls)
match tips: # match tips:
case int(): # case int():
query = query.filter(cls.tips_id == tips) # query = query.filter(cls.tips_id == tips)
case Tips(): # case Tips():
query = query.filter(cls.tips == tips) # query = query.filter(cls.tips == tips)
case _: # case _:
pass # pass
match procedure: # match procedure:
case int(): # case int():
query = query.filter(cls.procedure_id == procedure) # query = query.filter(cls.procedure_id == procedure)
case Procedure(): # case Procedure():
query = query.filter(cls.procedure == procedure) # query = query.filter(cls.procedure == procedure)
case _: # case _:
pass # pass
query = query.filter(cls.tiprole == tiprole) # query = query.filter(cls.tiprole == tiprole)
return cls.execute_query(query=query, limit=limit, **kwargs) # return cls.execute_query(query=query, limit=limit, **kwargs)
#
# TODO: fold this into the BaseClass.query_or_create ? # # TODO: fold this into the BaseClass.query_or_create ?
# @classmethod # # @classmethod
# def query_or_create(cls, tips, procedure, role: str, **kwargs): # # def query_or_create(cls, tips, procedure, role: str, **kwargs):
# kwargs['limit'] = 1 # # kwargs['limit'] = 1
# instance = cls.query(tips_id=tips.id, role_name=role, procedure_id=procedure.id, **kwargs) # # instance = cls.query(tips_id=tips.id, role_name=role, procedure_id=procedure.id, **kwargs)
# if instance is None: # # if instance is None:
# instance = cls(procedure=procedure, tips=tips, role_name=role) # # instance = cls(procedure=procedure, tips=tips, role_name=role)
# return instance # # return instance
#
def to_pydantic(self): # def to_pydantic(self):
from backend.validators import PydTips # from backend.validators import PydTips
return PydTips(name=self.tips.name, lot=self.tips.lot, role=self.role_name) # return PydTips(name=self.tips.name, lot=self.tips.lot, role=self.role_name)
#
def details_dict(self, **kwargs): # def details_dict(self, **kwargs):
output = super().details_dict() # output = super().details_dict()
# NOTE: Figure out how to merge the misc_info if doing .update instead. # # NOTE: Figure out how to merge the misc_info if doing .update instead.
relevant = {k: v for k, v in output.items() if k not in ['tips']} # relevant = {k: v for k, v in output.items() if k not in ['tips']}
output = output['tips'].details_dict() # output = output['tips'].details_dict()
misc = output['misc_info'] # misc = output['misc_info']
output.update(relevant) # output.update(relevant)
output['misc_info'] = misc # output['misc_info'] = misc
return output # return output
class Results(BaseClass): class Results(BaseClass):

View File

@@ -1248,6 +1248,7 @@ class Run(BaseClass, LogMixin):
dlg = ProcedureCreation(parent=obj, procedure=procedure_type.construct_dummy_procedure(run=self)) dlg = ProcedureCreation(parent=obj, procedure=procedure_type.construct_dummy_procedure(run=self))
if dlg.exec(): if dlg.exec():
sql, _ = dlg.return_sql(new=True) sql, _ = dlg.return_sql(new=True)
# sys.exit(pformat(sql.__dict__))
sql.save() sql.save()
obj.set_data() obj.set_data()
@@ -1460,11 +1461,11 @@ class Run(BaseClass, LogMixin):
return list(sorted(padded_list, key=itemgetter('submission_rank'))) return list(sorted(padded_list, key=itemgetter('submission_rank')))
class SampleType(BaseClass): # class SampleType(BaseClass):
id = Column(INTEGER, primary_key=True) #: primary key # id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64), nullable=False, unique=True) #: identification from submitter # name = Column(String(64), nullable=False, unique=True) #: identification from submitter
#
sample = relationship("Sample", back_populates="sampletype", uselist=True) # sample = relationship("Sample", back_populates="sampletype", uselist=True)
# NOTE: Sample Classes # NOTE: Sample Classes
@@ -1476,9 +1477,9 @@ class Sample(BaseClass, LogMixin):
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
sample_id = Column(String(64), nullable=False, unique=True) #: identification from submitter sample_id = Column(String(64), nullable=False, unique=True) #: identification from submitter
sampletype_id = Column(INTEGER, ForeignKey("_sampletype.id", ondelete="SET NULL", # sampletype_id = Column(INTEGER, ForeignKey("_sampletype.id", ondelete="SET NULL",
name="fk_SAMP_sampletype_id")) # name="fk_SAMP_sampletype_id"))
sampletype = relationship("SampleType", back_populates="sample") # sampletype = relationship("SampleType", back_populates="sample")
# misc_info = Column(JSON) # misc_info = Column(JSON)
control = relationship("Control", back_populates="sample", uselist=False) control = relationship("Control", back_populates="sample", uselist=False)
@@ -1512,10 +1513,7 @@ class Sample(BaseClass, LogMixin):
return self.sample_id return self.sample_id
def __repr__(self) -> str: def __repr__(self) -> str:
try: return f"<Sample({self.sample_id})>"
return f"<{self.sampletype.name.replace('_', ' ').title().replace(' ', '')}({self.sample_id})>"
except AttributeError:
return f"<Sample({self.sample_id})>"
@classproperty @classproperty
def searchables(cls): def searchables(cls):
@@ -1531,13 +1529,13 @@ class Sample(BaseClass, LogMixin):
Returns: Returns:
dict: submitter id and sample type and linked procedure if full data dict: submitter id and sample type and linked procedure if full data
""" """
try: # try:
sample_type = self.sampletype.name # sample_type = self.sampletype.name
except AttributeError: # except AttributeError:
sample_type = "NA" # sample_type = "NA"
sample = dict( sample = dict(
sample_id=self.sample_id, sample_id=self.sample_id
sampletype=sample_type # sampletype=sample_type
) )
if full_data: if full_data:
sample['clientsubmission'] = sorted([item.to_sub_dict() for item in self.sampleclientsubmissionassociation], sample['clientsubmission'] = sorted([item.to_sub_dict() for item in self.sampleclientsubmissionassociation],
@@ -1565,7 +1563,7 @@ class Sample(BaseClass, LogMixin):
@setup_lookup @setup_lookup
def query(cls, def query(cls,
sample_id: str | None = None, sample_id: str | None = None,
sampletype: str | SampleType | None = None, # sampletype: str | SampleType | None = None,
limit: int = 0, limit: int = 0,
**kwargs **kwargs
) -> Sample | List[Sample]: ) -> Sample | List[Sample]:
@@ -1574,20 +1572,19 @@ class Sample(BaseClass, LogMixin):
Args: Args:
sample_id (str | None, optional): Name of the sample (limits results to 1). Defaults to None. sample_id (str | None, optional): Name of the sample (limits results to 1). Defaults to None.
sampletype (str | None, optional): Sample type. Defaults to None.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0. limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
Returns: Returns:
models.Sample|List[models.Sample]: Sample(s) of interest. models.Sample|List[models.Sample]: Sample(s) of interest.
""" """
query = cls.__database_session__.query(cls) query = cls.__database_session__.query(cls)
match sampletype: # match sampletype:
case str(): # case str():
query = query.join(SampleType).filter(SampleType.name == sampletype) # query = query.join(SampleType).filter(SampleType.name == sampletype)
case SampleType(): # case SampleType():
query = query.filter(cls.sampletype == sampletype) # query = query.filter(cls.sampletype == sampletype)
case _: # case _:
pass # pass
match sample_id: match sample_id:
case str(): case str():
query = query.filter(cls.sample_id == sample_id) query = query.filter(cls.sample_id == sample_id)
@@ -1596,37 +1593,37 @@ class Sample(BaseClass, LogMixin):
pass pass
return cls.execute_query(query=query, limit=limit, **kwargs) return cls.execute_query(query=query, limit=limit, **kwargs)
@classmethod # @classmethod
def fuzzy_search(cls, # def fuzzy_search(cls,
sampletype: str | Sample | None = None, # sampletype: str | Sample | None = None,
**kwargs # **kwargs
) -> List[Sample]: # ) -> List[Sample]:
""" # """
Allows for fuzzy search of sample. # Allows for fuzzy search of sample.
#
Args: # Args:
sampletype (str | BasicSample | None, optional): Type of sample. Defaults to None. # sampletype (str | BasicSample | None, optional): Type of sample. Defaults to None.
#
Returns: # Returns:
List[Sample]: List of sample that match kwarg search parameters. # List[Sample]: List of sample that match kwarg search parameters.
""" # """
query: Query = cls.__database_session__.query(cls) # query: Query = cls.__database_session__.query(cls)
match sampletype: # match sampletype:
case str(): # case str():
query = query.join(SampleType).filter(SampleType.name == sampletype) # query = query.join(SampleType).filter(SampleType.name == sampletype)
case SampleType(): # case SampleType():
query = query.filter(cls.sampletype == sampletype) # query = query.filter(cls.sampletype == sampletype)
case _: # case _:
pass # pass
for k, v in kwargs.items(): # for k, v in kwargs.items():
search = f"%{v}%" # search = f"%{v}%"
try: # try:
attr = getattr(cls, k) # attr = getattr(cls, k)
# NOTE: the secret sauce is in attr.like # # NOTE: the secret sauce is in attr.like
query = query.filter(attr.like(search)) # query = query.filter(attr.like(search))
except (ArgumentError, AttributeError) as e: # except (ArgumentError, AttributeError) as e:
logger.error(f"Attribute {k} unavailable due to:\n\t{e}\nSkipping.") # logger.error(f"Attribute {k} unavailable due to:\n\t{e}\nSkipping.")
return query.limit(50).all() # return query.limit(50).all()
def delete(self): def delete(self):
raise AttributeError(f"Delete not implemented for {self.__class__}") raise AttributeError(f"Delete not implemented for {self.__class__}")

View File

@@ -35,6 +35,7 @@ class PydBaseClass(BaseModel, extra='allow', validate_assignment=True):
@model_validator(mode="before") @model_validator(mode="before")
@classmethod @classmethod
def prevalidate(cls, data): def prevalidate(cls, data):
# logger.debug(f"Initial data for {cls.__name__}: {pformat(data)}")
sql_fields = [k for k, v in cls._sql_object.__dict__.items() if isinstance(v, InstrumentedAttribute)] sql_fields = [k for k, v in cls._sql_object.__dict__.items() if isinstance(v, InstrumentedAttribute)]
output = {} output = {}
try: try:
@@ -72,7 +73,7 @@ class PydBaseClass(BaseModel, extra='allow', validate_assignment=True):
def __init__(self, **data): def __init__(self, **data):
# NOTE: Grab the sql model for validation purposes. # NOTE: Grab the sql model for validation purposes.
# self.__class__._sql_object = getattr(models, self.__class__.__name__.replace("Pyd", "")) # self.__class__._sql_object = getattr(models, self.__class__.__name__.replace("Pyd", ""))
logger.debug(f"Initial data: {data}")
super().__init__(**data) super().__init__(**data)
def filter_field(self, key: str) -> Any: def filter_field(self, key: str) -> Any:
@@ -284,7 +285,7 @@ class PydReagent(PydBaseClass):
class PydSample(PydBaseClass): class PydSample(PydBaseClass):
sample_id: str sample_id: str
sampletype: str | None = Field(default=None) # sampletype: str | None = Field(default=None)
submission_rank: int | List[int] | None = Field(default=0, validate_default=True) submission_rank: int | List[int] | None = Field(default=0, validate_default=True)
enabled: bool = Field(default=True) enabled: bool = Field(default=True)
row: int = Field(default=0) row: int = Field(default=0)
@@ -327,7 +328,7 @@ class PydSample(PydBaseClass):
def improved_dict(self, dictionaries: bool = True) -> dict: def improved_dict(self, dictionaries: bool = True) -> dict:
output = super().improved_dict(dictionaries=dictionaries) output = super().improved_dict(dictionaries=dictionaries)
output['name'] = self.sample_id output['name'] = self.sample_id
del output['sampletype'] # del output['sampletype']
return output return output
def to_sql(self): def to_sql(self):
@@ -399,22 +400,24 @@ class PydEquipment(PydBaseClass):
@field_validator('process', mode='before') @field_validator('process', mode='before')
@classmethod @classmethod
def make_empty_list(cls, value): def make_empty_list(cls, value, values):
# if isinstance(value, dict): # if isinstance(value, dict):
# value = value['processes'] # value = value['processes']
if isinstance(value, GeneratorType): if isinstance(value, GeneratorType):
value = [item for item in value] value = [item for item in value]
value = convert_nans_to_nones(value) value = convert_nans_to_nones(value)
if not value: if not value:
value = [''] value = []
# logger.debug(value) logger.debug(value)
try: try:
# value = [item.strip() for item in value] # value = [item.strip() for item in value]
d = next((process for process in value), None) d: Process = next((process for process in value if values.data['name'] in [item.name for item in process.equipment]), None)
logger.debug(f"Next process: {d.detail_dict()}") # logger.debug(f"Next process: {d.details_dict()}")
value = PydProcess(d.details_dict()) value = d.to_pydantic()
# value = PydProcess(**d.details_dict())
# value = next((process.to_pydantic() for process in value)) # value = next((process.to_pydantic() for process in value))
except AttributeError: except AttributeError as e:
logger.error(f"Process Validation error due to {e}")
pass pass
return value return value
@@ -1179,6 +1182,7 @@ class PydProcess(PydBaseClass, extra="allow"):
@field_validator("proceduretype", "equipment", "equipmentrole", "tiprole", mode="before") @field_validator("proceduretype", "equipment", "equipmentrole", "tiprole", mode="before")
@classmethod @classmethod
def enforce_list(cls, value): def enforce_list(cls, value):
# logger.debug(f"Validating field: {value}")
if not isinstance(value, list): if not isinstance(value, list):
value = [value] value = [value]
output = [] output = []
@@ -1192,32 +1196,36 @@ class PydProcess(PydBaseClass, extra="allow"):
@report_result @report_result
def to_sql(self): def to_sql(self):
report = Report() report = Report()
instance = Process.query(name=self.name) logger.debug(f"Query process: {self.name}, version = {self.version}")
# NOTE: can't use query_or_create due to name not being part of ProcessVersion
instance = ProcessVersion.query(name=self.name, version=self.version, limit=1)
logger.debug(f"Got instance: {instance}")
if not instance: if not instance:
instance = Process() instance = ProcessVersion()
fields = [item for item in self.model_fields] # fields = [item for item in self.model_fields]
for field in fields: # for field in fields:
logger.debug(f"Field: {field}") # logger.debug(f"Field: {field}")
try: # try:
field_type = getattr(instance.__class__, field).property # field_type = getattr(instance.__class__, field).property
except AttributeError: # except AttributeError:
logger.error(f"No attribute: {field} in {instance.__class__}") # logger.error(f"No attribute: {field} in {instance.__class__}")
continue # continue
match field_type: # match field_type:
case _RelationshipDeclared(): # case _RelationshipDeclared():
logger.debug(f"{field} is a relationship with {field_type.entity.class_}") # logger.debug(f"{field} is a relationship with {field_type.entity.class_}")
query_str = getattr(self, field) # query_str = getattr(self, field)
if isinstance(query_str, list): # if isinstance(query_str, list):
query_str = query_str[0] # query_str = query_str[0]
if query_str in ["", " ", None]: # if query_str in ["", " ", None]:
continue # continue
logger.debug(f"Querying {field_type.entity.class_} with name {query_str}") # logger.debug(f"Querying {field_type.entity.class_} with name {query_str}")
field_value = field_type.entity.class_.query(name=query_str) # field_value = field_type.entity.class_.query(name=query_str)
logger.debug(f"{field} query result: {field_value}") # logger.debug(f"{field} query result: {field_value}")
case ColumnProperty(): # case ColumnProperty():
logger.debug(f"{field} is a property.") # logger.debug(f"{field} is a property.")
field_value = getattr(self, field) # field_value = getattr(self, field)
instance.set_attribute(key=field, value=field_value) # # instance.set_attribute(key=field, value=field_value)
# setattr(instance, field, field_value)
return instance, report return instance, report
@@ -1469,7 +1477,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
idx = 0 idx = 0
insertable = PydReagent(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry) insertable = PydReagent(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry)
self.reagent.insert(idx, insertable) self.reagent.insert(idx, insertable)
logger.debug(self.reagent) # logger.debug(self.reagent)
@classmethod @classmethod
def update_new_reagents(cls, reagent: PydReagent): def update_new_reagents(cls, reagent: PydReagent):
@@ -1506,9 +1514,9 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
sql.proceduretype = self.proceduretype sql.proceduretype = self.proceduretype
# Note: convert any new reagents to sql and save # Note: convert any new reagents to sql and save
# for reagentrole, reagents in self.reagentrole.items(): # for reagentrole, reagents in self.reagentrole.items():
for reagent in self.reagent: # for reagent in self.reagent:
if not reagent.lot or reagent.name == "--New--": # if not reagent.lot or reagent.name == "--New--":
continue # continue
# self.update_new_reagents(reagent) # self.update_new_reagents(reagent)
# NOTE: reset reagent associations. # NOTE: reset reagent associations.
# sql.procedurereagentassociation = [] # sql.procedurereagentassociation = []
@@ -1532,7 +1540,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
r.delete() r.delete()
else: else:
removable.delete() removable.delete()
# logger.debug(f"Adding {reagent} to {sql}") logger.debug(f"Adding {reagent} to {sql}")
reagent_assoc = ProcedureReagentLotAssociation(reagentlot=reagent, procedure=sql, reagentrole=reagentrole) reagent_assoc = ProcedureReagentLotAssociation(reagentlot=reagent, procedure=sql, reagentrole=reagentrole)
try: try:
start_index = max([item.id for item in ProcedureSampleAssociation.query()]) + 1 start_index = max([item.id for item in ProcedureSampleAssociation.query()]) + 1
@@ -1556,14 +1564,18 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
proc_assoc = ProcedureSampleAssociation(new_id=assoc_id_range[iii], procedure=sql, sample=sample_sql, proc_assoc = ProcedureSampleAssociation(new_id=assoc_id_range[iii], procedure=sql, sample=sample_sql,
row=sample.row, column=sample.column, row=sample.row, column=sample.column,
procedure_rank=sample.procedure_rank) procedure_rank=sample.procedure_rank)
sys.exit(pformat(self.equipment)) # sys.exit(pformat(self.equipment))
for equipment in self.equipment: for equipment in self.equipment:
equip = Equipment.query(name=equipment.name) logger.debug(f"Equipment: {equipment}")
# equip = Equipment.query(name=equipment.name)
equip, _ = equipment.to_sql()
logger.debug(f"Process: {equipment.process}")
if equip not in sql.equipment: if equip not in sql.equipment:
equip_assoc = ProcedureEquipmentAssociation(equipment=equip, procedure=sql, equip_assoc = ProcedureEquipmentAssociation(equipment=equip, procedure=sql,
equipmentrole=equip.equipmentrole[0]) equipmentrole=equip.equipmentrole[0])
process = equipment.process.to_sql() process = equipment.process.to_sql()
equip_assoc.process = process equip_assoc.processversion = process
# sys.exit(pformat([item.__dict__ for item in sql.procedureequipmentassociation]))
return sql, None return sql, None

View File

@@ -134,7 +134,7 @@ class ProcedureCreation(QDialog):
tips = next((tps for tps in equipment.tips if tps.name == tips), None) tips = next((tps for tps in equipment.tips if tps.name == tips), None)
if tips: if tips:
eoi.tips = tips.to_pydantic() eoi.tips = tips.to_pydantic()
self.procedure.equipment.append(eoi) self.procedure.equipment.append(eoi)
logger.debug(f"Updated equipment: {self.procedure.equipment}") logger.debug(f"Updated equipment: {self.procedure.equipment}")
@pyqtSlot(str, str) @pyqtSlot(str, str)

View File

@@ -110,7 +110,7 @@ class SubmissionDetails(QDialog):
@pyqtSlot(str) @pyqtSlot(str)
def process_details(self, process: str | Process): def process_details(self, process: str | Process):
logger.debug(f"Equipment details") logger.debug(f"Process details")
if isinstance(process, str): if isinstance(process, str):
process = Process.query(name=process) process = Process.query(name=process)
base_dict = process.to_sub_dict(full_data=True) base_dict = process.to_sub_dict(full_data=True)