Midway through disaster of changing table names.

This commit is contained in:
Landon Wark
2024-01-19 15:17:07 -06:00
parent d66d861262
commit 319f72cab2
31 changed files with 1040 additions and 1457 deletions

View File

@@ -18,9 +18,13 @@ class BaseClass(Base):
Base (DeclarativeMeta): Declarative base for metadata.
"""
__abstract__ = True
__table_args__ = {'extend_existing': True}
@declared_attr
def __tablename__(cls):
return f"_{cls.__name__.lower()}"
@declared_attr
def __database_session__(cls):
if not 'pytest' in sys.modules:
@@ -44,6 +48,15 @@ class BaseClass(Base):
else:
from test_settings import ctx
return ctx.backup_path
def save(self):
logger.debug(f"Saving {self}")
try:
self.__database_session__.add(self)
self.__database_session__.commit()
except Exception as e:
logger.critical(f"Problem saving object: {e}")
self.__database_session__.rollback()
from .controls import *
# import order must go: orgs, kit, subs due to circular import issues

View File

@@ -18,7 +18,7 @@ class ControlType(BaseClass):
"""
Base class of a control archetype.
"""
__tablename__ = '_control_types'
# __tablename__ = '_control_types'
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(255), unique=True) #: controltype name (e.g. MCS)
@@ -75,7 +75,7 @@ class Control(BaseClass):
Base class of a control sample.
"""
__tablename__ = '_control_samples'
# __tablename__ = '_control_samples'
id = Column(INTEGER, primary_key=True) #: primary key
parent_id = Column(String, ForeignKey("_control_types.id", name="fk_control_parent_id")) #: primary key of control type
@@ -264,7 +264,4 @@ class Control(BaseClass):
case _:
pass
return query_return(query=query, limit=limit)
def save(self):
self.__database_session__.add(self)
self.__database_session__.commit()

View File

@@ -12,7 +12,6 @@ from typing import List
from pandas import ExcelFile
from pathlib import Path
from . import Base, BaseClass, Organization
from tools import Settings
logger = logging.getLogger(f'submissions.{__name__}')
@@ -32,11 +31,19 @@ equipmentroles_equipment = Table(
extend_existing=True
)
equipment_processes = Table(
"_equipment_processes",
Base.metadata,
Column("process_id", INTEGER, ForeignKey("_process.id")),
Column("equipment_id", INTEGER, ForeignKey("_equipment.id")),
extend_existing=True
)
equipmentroles_processes = Table(
"_equipmentroles_processes",
Base.metadata,
Column("process_id", INTEGER, ForeignKey("_process.id")),
Column("equipmentroles_id", INTEGER, ForeignKey("_equipment_roles.id")),
Column("equipmentrole_id", INTEGER, ForeignKey("_equipment_roles.id")),
extend_existing=True
)
@@ -48,16 +55,24 @@ submissiontypes_processes = Table(
extend_existing=True
)
kittypes_processes = Table(
"_kittypes_processes",
Base.metadata,
Column("process_id", INTEGER, ForeignKey("_process.id")),
Column("kit_id", INTEGER, ForeignKey("_kits.id")),
extend_existing=True
)
class KitType(BaseClass):
"""
Base of kits used in submission processing
"""
__tablename__ = "_kits"
# __table_args__ = {'extend_existing': True}
# __tablename__ = "_kits"
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
processes = relationship("Process", back_populates="kit_types", secondary=kittypes_processes)
kit_reagenttype_associations = relationship(
"KitTypeReagentTypeAssociation",
@@ -87,7 +102,7 @@ class KitType(BaseClass):
Args:
required (bool, optional): If true only return required types. Defaults to False.
submission_type (str | None, optional): Submission type to narrow results. Defaults to None.
submission_type (str | Submissiontype | None, optional): Submission type to narrow results. Defaults to None.
Returns:
list: List of reagent types
@@ -109,12 +124,13 @@ class KitType(BaseClass):
Creates map of locations in excel workbook for a SubmissionType
Args:
use (str): Submissiontype.name
use (str | SubmissionType): Submissiontype.name
Returns:
dict: Dictionary containing information locations.
"""
map = {}
# Account for submission_type variable type.
match submission_type:
case str():
assocs = [item for item in self.kit_reagenttype_associations if item.submission_type.name==submission_type]
@@ -125,7 +141,6 @@ class KitType(BaseClass):
case _:
raise ValueError(f"Wrong variable type: {type(submission_type)} used!")
# Get all KitTypeReagentTypeAssociation for SubmissionType
# assocs = [item for item in self.kit_reagenttype_associations if item.submission_type==submission_type]
for assoc in assocs:
try:
map[assoc.reagent_type.name] = assoc.uses
@@ -133,7 +148,6 @@ class KitType(BaseClass):
continue
# Get SubmissionType info map
try:
# st_assoc = [item for item in self.used_for if use == item.name][0]
map['info'] = st_assoc.info_map
except IndexError as e:
map['info'] = {}
@@ -152,7 +166,7 @@ class KitType(BaseClass):
Args:
name (str, optional): Name of desired kit (returns single instance). Defaults to None.
used_for (str | models.Submissiontype | None, optional): Submission type the kit is used for. Defaults to None.
used_for (str | Submissiontype | None, optional): Submission type the kit is used for. Defaults to None.
id (int | None, optional): Kit id in the database. Defaults to None.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
@@ -190,17 +204,13 @@ class KitType(BaseClass):
@check_authorization
def save(self, ctx:Settings):
"""
Add this instance to database and commit
"""
self.__database_session__.add(self)
self.__database_session__.commit()
super().save()
class ReagentType(BaseClass):
"""
Base of reagent type abstract
"""
__tablename__ = "_reagent_types"
# __tablename__ = "_reagent_types"
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: name of reagent type
@@ -281,130 +291,16 @@ class ReagentType(BaseClass):
def to_pydantic(self):
from backend.validators.pydant import PydReagent
return PydReagent(lot=None, type=self.name, name=self.name, expiry=date.today())
# class KitTypeReagentTypeAssociation(BaseClass):
# """
# table containing reagenttype/kittype associations
# DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
# """
# __tablename__ = "_reagenttypes_kittypes"
# reagent_types_id = Column(INTEGER, ForeignKey("_reagent_types.id"), primary_key=True) #: id of associated reagent type
# kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) #: id of associated reagent type
# submission_type_id = (Column(INTEGER), ForeignKey("_submission_types.id"), primary_key=True)
# uses = Column(JSON) #: map to location on excel sheets of different submission types
# required = Column(INTEGER) #: whether the reagent type is required for the kit (Boolean 1 or 0)
# last_used = Column(String(32)) #: last used lot number of this type of reagent
# kit_type = relationship(KitType, back_populates="kit_reagenttype_associations") #: relationship to associated kit
# # reference to the "ReagentType" object
# reagent_type = relationship(ReagentType, back_populates="reagenttype_kit_associations") #: relationship to associated reagent type
# submission_type = relationship(SubmissionType, back_populates="submissiontype_kit_rt_associations")
# def __init__(self, kit_type=None, reagent_type=None, uses=None, required=1):
# # logger.debug(f"Parameters: Kit={kit_type}, RT={reagent_type}, Uses={uses}, Required={required}")
# self.kit_type = kit_type
# self.reagent_type = reagent_type
# self.uses = uses
# self.required = required
# def __repr__(self) -> str:
# return f"<KitTypeReagentTypeAssociation({self.kit_type} & {self.reagent_type})>"
# @validates('required')
# def validate_age(self, key, value):
# """
# Ensures only 1 & 0 used in 'required'
# 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
# @validates('reagenttype')
# def validate_reagenttype(self, key, value):
# """
# Ensures reagenttype is an actual ReagentType
# Args:
# key (str)): name of attribute
# value (_type_): value of attribute
# Raises:
# ValueError: raised if reagenttype is not a ReagentType
# Returns:
# _type_: ReagentType
# """
# if not isinstance(value, ReagentType):
# raise ValueError(f'{value} is not a reagenttype')
# return value
# @classmethod
# @setup_lookup
# def query(cls,
# kit_type:KitType|str|None=None,
# reagent_type:ReagentType|str|None=None,
# limit:int=0
# ) -> KitTypeReagentTypeAssociation|List[KitTypeReagentTypeAssociation]:
# """
# Lookup junction of ReagentType and KitType
# Args:
# kit_type (models.KitType | str | None): KitType of interest.
# reagent_type (models.ReagentType | str | None): ReagentType of interest.
# limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
# Returns:
# models.KitTypeReagentTypeAssociation|List[models.KitTypeReagentTypeAssociation]: Junction of interest.
# """
# query: Query = cls.__database_session__.query(cls)
# match kit_type:
# case KitType():
# query = query.filter(cls.kit_type==kit_type)
# case str():
# query = query.join(KitType).filter(KitType.name==kit_type)
# case _:
# pass
# match reagent_type:
# case ReagentType():
# query = query.filter(cls.reagent_type==reagent_type)
# case str():
# query = query.join(ReagentType).filter(ReagentType.name==reagent_type)
# case _:
# pass
# if kit_type != None and reagent_type != None:
# limit = 1
# return query_return(query=query, limit=limit)
# def save(self) -> Report:
# """
# Adds this instance to the database and commits.
# Returns:
# Report: Result of save action
# """
# report = Report()
# self.__database_session__.add(self)
# self.__database_session__.commit()
# return report
@check_authorization
def save(self, ctx:Settings):
super().save()
class Reagent(BaseClass):
"""
Concrete reagent instance
"""
__tablename__ = "_reagents"
# __tablename__ = "_reagents"
id = Column(INTEGER, primary_key=True) #: primary key
type = relationship("ReagentType", back_populates="instances", secondary=reagenttypes_reagents) #: joined parent reagent type
@@ -412,7 +308,6 @@ class Reagent(BaseClass):
name = Column(String(64)) #: reagent name
lot = Column(String(64)) #: lot number of reagent
expiry = Column(TIMESTAMP) #: expiry date - extended by eol_ext of parent programmatically
# submissions = relationship("BasicSubmission", back_populates="reagents", uselist=True) #: submissions this reagent is used in
reagent_submission_associations = relationship(
"SubmissionReagentAssociation",
@@ -497,6 +392,7 @@ class Reagent(BaseClass):
def query(cls,
reagent_type:str|ReagentType|None=None,
lot_number:str|None=None,
name:str|None=None,
limit:int=0
) -> Reagent|List[Reagent]:
"""
@@ -505,6 +401,7 @@ class Reagent(BaseClass):
Args:
reagent_type (str | models.ReagentType | None, optional): Reagent type. Defaults to None.
lot_number (str | None, optional): Reagent lot number. Defaults to None.
name (str | None, optional): Reagent name. Defaults to None.
limit (int, optional): limit of results returned. Defaults to 0.
Returns:
@@ -521,6 +418,12 @@ class Reagent(BaseClass):
query = query.filter(cls.type.contains(reagent_type))
case _:
pass
match name:
case str():
logger.debug(f"Looking up reagent by name: {name}")
query = query.filter(cls.name==name)
case _:
pass
match lot_number:
case str():
logger.debug(f"Looking up reagent by lot number: {lot_number}")
@@ -530,19 +433,12 @@ class Reagent(BaseClass):
case _:
pass
return query_return(query=query, limit=limit)
def save(self):
"""
Add this instance to the database and commit
"""
self.__database_session__.add(self)
self.__database_session__.commit()
class Discount(BaseClass):
"""
Relationship table for client labs for certain kits.
"""
__tablename__ = "_discounts"
# __tablename__ = "_discounts"
id = Column(INTEGER, primary_key=True) #: primary key
kit = relationship("KitType") #: joined parent reagent type
@@ -604,17 +500,20 @@ class Discount(BaseClass):
pass
return query.all()
@check_authorization
def save(self, ctx:Settings):
super().save()
class SubmissionType(BaseClass):
"""
Abstract of types of submissions.
"""
__tablename__ = "_submission_types"
# __tablename__ = "_submission_types"
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(128), unique=True) #: name of submission type
info_map = Column(JSON) #: Where basic information is found in the excel workbook corresponding to this type.
instances = relationship("BasicSubmission", backref="submission_type") #: Concrete instances of this type.
# regex = Column(String(512))
template_file = Column(BLOB) #: Blank form for this type stored as binary.
processes = relationship("Process", back_populates="submission_types", secondary=submissiontypes_processes)
@@ -664,20 +563,25 @@ class SubmissionType(BaseClass):
output = []
for item in self.submissiontype_equipmentrole_associations:
map = item.uses
map['role'] = item.equipment_role.name
if map == None:
map = {}
try:
map['role'] = item.equipment_role.name
except TypeError:
pass
output.append(map)
return output
# return [item.uses for item in self.submissiontype_equipmentrole_associations]
def get_equipment(self) -> List['PydEquipmentRole']:
return [item.to_pydantic(submission_type=self) for item in self.equipment]
def get_equipment(self, extraction_kit:str|KitType|None=None) -> List['PydEquipmentRole']:
return [item.to_pydantic(submission_type=self, extraction_kit=extraction_kit) for item in self.equipment]
def get_processes_for_role(self, equipment_role:str|EquipmentRole):
def get_processes_for_role(self, equipment_role:str|EquipmentRole, kit:str|KitType|None=None):
match equipment_role:
case str():
relevant = [item.get_all_processes() for item in self.submissiontype_equipmentrole_associations if item.equipment_role.name==equipment_role]
relevant = [item.get_all_processes(kit) for item in self.submissiontype_equipmentrole_associations if item.equipment_role.name==equipment_role]
case EquipmentRole():
relevant = [item.get_all_processes() for item in self.submissiontype_equipmentrole_associations if item.equipment_role==equipment_role]
relevant = [item.get_all_processes(kit) for item in self.submissiontype_equipmentrole_associations if item.equipment_role==equipment_role]
case _:
raise TypeError(f"Type {type(equipment_role)} is not allowed")
return list(set([item for items in relevant for item in items if item != None ]))
@@ -728,7 +632,7 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
"""
Abstract of relationship between kits and their submission type.
"""
__tablename__ = "_submissiontypes_kittypes"
# __tablename__ = "_submissiontypes_kittypes"
submission_types_id = Column(INTEGER, ForeignKey("_submission_types.id"), primary_key=True) #: id of joined submission type
kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) #: id of joined kit
@@ -801,7 +705,7 @@ class KitTypeReagentTypeAssociation(BaseClass):
table containing reagenttype/kittype associations
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
"""
__tablename__ = "_reagenttypes_kittypes"
# __tablename__ = "_reagenttypes_kittypes"
reagent_types_id = Column(INTEGER, ForeignKey("_reagent_types.id"), primary_key=True) #: id of associated reagent type
kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) #: id of associated reagent type
@@ -902,22 +806,9 @@ class KitTypeReagentTypeAssociation(BaseClass):
limit = 1
return query_return(query=query, limit=limit)
def save(self) -> Report:
"""
Adds this instance to the database and commits.
Returns:
Report: Result of save action
"""
report = Report()
self.__database_session__.add(self)
self.__database_session__.commit()
return report
class SubmissionReagentAssociation(BaseClass):
__tablename__ = "_reagents_submissions"
# __tablename__ = "_reagents_submissions"
reagent_id = Column(INTEGER, ForeignKey("_reagents.id"), primary_key=True) #: id of associated sample
submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True)
@@ -961,8 +852,6 @@ class SubmissionReagentAssociation(BaseClass):
# logger.debug(f"Filtering query with reagent: {reagent}")
reagent = Reagent.query(lot_number=reagent)
query = query.filter(cls.reagent==reagent)
# logger.debug([item.reagent.lot for item in query.all()])
# query = query.join(Reagent).filter(Reagent.lot==reagent)
case _:
pass
# logger.debug(f"Result of query after reagent: {query.all()}")
@@ -976,7 +865,6 @@ class SubmissionReagentAssociation(BaseClass):
case _:
pass
# logger.debug(f"Result of query after submission: {query.all()}")
# limit = query.count()
return query_return(query=query, limit=limit)
def to_sub_dict(self, extraction_kit):
@@ -989,13 +877,14 @@ class Equipment(BaseClass):
# Currently abstract until ready to implement
# __abstract__ = True
__tablename__ = "_equipment"
# __tablename__ = "_equipment"
id = Column(INTEGER, primary_key=True)
name = Column(String(64))
nickname = Column(String(64))
asset_number = Column(String(16))
roles = relationship("EquipmentRole", back_populates="instances", secondary=equipmentroles_equipment)
processes = relationship("Process", back_populates="equipment", secondary=equipment_processes)
equipment_submission_associations = relationship(
"SubmissionEquipmentAssociation",
@@ -1008,10 +897,30 @@ class Equipment(BaseClass):
def __repr__(self):
return f"<Equipment({self.name})>"
def get_processes(self, submission_type:SubmissionType):
processes = [assoc.process for assoc in self.equipment_submission_associations if assoc.submission.submission_type_name==submission_type.name]
def to_dict(self, processes:bool=False):
if not processes:
return {k:v for k,v in self.__dict__.items() if k != 'processes'}
else:
return {k:v for k,v in self.__dict__.items()}
def get_processes(self, submission_type:SubmissionType, extraction_kit:str|KitType|None=None):
processes = [process for process in self.processes if submission_type in process.submission_types]
match extraction_kit:
case str():
processes = [process for process in processes if extraction_kit in [kit.name for kit in process.kit_types]]
case KitType():
processes = [process for process in processes if extraction_kit in process.kit_types]
case _:
pass
processes = [process.name for process in processes]
# try:
assert all([isinstance(process, str) for process in processes])
# except AssertionError as e:
# logger.error(processes)
# raise e
if len(processes) == 0:
processes = ['']
# logger.debug(f"Processes: {processes}")
return processes
@classmethod
@@ -1043,10 +952,10 @@ class Equipment(BaseClass):
pass
return query_return(query=query, limit=limit)
def to_pydantic(self, submission_type:SubmissionType):
def to_pydantic(self, submission_type:SubmissionType, extraction_kit:str|KitType|None=None):
from backend.validators.pydant import PydEquipment
# return PydEquipment(process=self.get_processes(submission_type=submission_type), role=None, **self.__dict__)
return PydEquipment(process=None, role=None, **self.__dict__)
return PydEquipment(processes=self.get_processes(submission_type=submission_type, extraction_kit=extraction_kit), role=None, **self.to_dict(processes=False))
# return PydEquipment(process=None, role=None, **self.__dict__)
def save(self):
self.__database_session__.add(self)
@@ -1064,12 +973,12 @@ class Equipment(BaseClass):
class EquipmentRole(BaseClass):
__tablename__ = "_equipment_roles"
# __tablename__ = "_equipment_roles"
id = Column(INTEGER, primary_key=True)
name = Column(String(32))
instances = relationship("Equipment", back_populates="roles", secondary=equipmentroles_equipment)
processes = relationship("Process", back_populates="equipment_roles", secondary=equipmentroles_processes)
processes = relationship("Process", back_populates='equipment_roles', secondary=equipmentroles_processes)
equipmentrole_submissiontype_associations = relationship(
"SubmissionTypeEquipmentRoleAssociation",
@@ -1081,12 +990,24 @@ class EquipmentRole(BaseClass):
def __repr__(self):
return f"<EquipmentRole({self.name})>"
def to_dict(self):
output = {}
for key, value in self.__dict__.items():
match key:
case "processes":
pass
case _:
value = value
output[key] = value
return output
def to_pydantic(self, submission_type:SubmissionType):
def to_pydantic(self, submission_type:SubmissionType, extraction_kit:str|KitType|None=None):
from backend.validators.pydant import PydEquipmentRole
equipment = [item.to_pydantic(submission_type=submission_type) for item in self.instances]
pyd_dict = self.__dict__
pyd_dict['processes'] = self.get_processes(submission_type=submission_type)
equipment = [item.to_pydantic(submission_type=submission_type, extraction_kit=extraction_kit) for item in self.instances]
# processes = [item.name for item in self.processes]
pyd_dict = self.to_dict()
pyd_dict['processes'] = self.get_processes(submission_type=submission_type, extraction_kit=extraction_kit)
return PydEquipmentRole(equipment=equipment, **pyd_dict)
@classmethod
@@ -1107,31 +1028,36 @@ class EquipmentRole(BaseClass):
pass
return query_return(query=query, limit=limit)
def get_processes(self, submission_type:str|SubmissionType|None) -> List[Process]:
def get_processes(self, submission_type:str|SubmissionType|None, extraction_kit:str|KitType|None=None) -> List[Process]:
if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type)
# assert all([isinstance(process, Process) for process in self.processes])
# logger.debug(self.processes)
if submission_type != None:
output = [process.name for process in self.processes if submission_type in process.submission_types]
# for process in self.processes:
# logger.debug(f"Process: {type(process)}: {process}")
processes = [process for process in self.processes if submission_type in process.submission_types]
else:
output = [process.name for process in self.processes]
processes = self.processes
match extraction_kit:
case str():
processes = [item for item in processes if extraction_kit in [kit.name for kit in item.kit_type]]
case KitType():
processes = [item for item in processes if extraction_kit in [kit for kit in item.kit_type]]
case _:
pass
output = [item.name for item in processes]
if len(output) == 0:
return ['']
else:
return output
def save(self):
try:
self.__database_session__.add(self)
self.__database_session__.commit()
except:
self.__database_session__.rollback()
class SubmissionEquipmentAssociation(BaseClass):
# Currently abstract until ready to implement
# __abstract__ = True
__tablename__ = "_equipment_submissions"
# __tablename__ = "_equipment_submissions"
equipment_id = Column(INTEGER, ForeignKey("_equipment.id"), primary_key=True) #: id of associated equipment
submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True) #: id of associated submission
@@ -1144,14 +1070,14 @@ class SubmissionEquipmentAssociation(BaseClass):
submission = relationship("BasicSubmission", back_populates="submission_equipment_associations") #: associated submission
equipment = relationship(Equipment, back_populates="equipment_submission_associations") #: associated submission
equipment = relationship(Equipment, back_populates="equipment_submission_associations") #: associated equipment
def __init__(self, submission, equipment):
self.submission = submission
self.equipment = equipment
def to_sub_dict(self) -> dict:
output = dict(name=self.equipment.name, asset_number=self.equipment.asset_number, comment=self.comments, process=self.process.name, role=self.role, nickname=self.equipment.nickname)
output = dict(name=self.equipment.name, asset_number=self.equipment.asset_number, comment=self.comments, processes=[self.process.name], role=self.role, nickname=self.equipment.nickname)
return output
def save(self):
@@ -1162,7 +1088,7 @@ class SubmissionTypeEquipmentRoleAssociation(BaseClass):
# __abstract__ = True
__tablename__ = "_submissiontype_equipmentrole"
# __tablename__ = "_submissiontype_equipmentrole"
equipmentrole_id = Column(INTEGER, ForeignKey("_equipment_roles.id"), primary_key=True) #: id of associated equipment
submissiontype_id = Column(INTEGER, ForeignKey("_submission_types.id"), primary_key=True) #: id of associated submission
@@ -1192,25 +1118,38 @@ class SubmissionTypeEquipmentRoleAssociation(BaseClass):
raise ValueError(f'Invalid required value {value}. Must be 0 or 1.')
return value
def get_all_processes(self):
def get_all_processes(self, extraction_kit:KitType|str|None=None):
processes = [equipment.get_processes(self.submission_type) for equipment in self.equipment_role.instances]
# flatten list
processes = [item for items in processes for item in items if item != None ]
match extraction_kit:
case str():
processes = [item for item in processes if extraction_kit in [kit.name for kit in item.kit_type]]
case KitType():
processes = [item for item in processes if extraction_kit in [kit for kit in item.kit_type]]
case _:
pass
return processes
@check_authorization
def save(self, ctx:Settings):
self.__database_session__.add(self)
self.__database_session__.commit()
# self.__database_session__.add(self)
# self.__database_session__.commit()
super().save()
class Process(BaseClass):
__tablename__ = "_process"
"""
A Process is a method used by a piece of equipment.
"""
# __tablename__ = "_process"
id = Column(INTEGER, primary_key=True)
name = Column(String(64))
submission_types = relationship("SubmissionType", back_populates='processes', secondary=submissiontypes_processes)
equipment = relationship("Equipment", back_populates='processes', secondary=equipment_processes)
equipment_roles = relationship("EquipmentRole", back_populates='processes', secondary=equipmentroles_processes)
submissions = relationship("SubmissionEquipmentAssociation", backref='process')
kit_types = relationship("KitType", back_populates='processes', secondary=kittypes_processes)
def __repr__(self):
return f"<Process({self.name})"
@@ -1227,124 +1166,3 @@ class Process(BaseClass):
pass
return query_return(query=query, limit=limit)
def save(self):
try:
self.__database_session__.add(self)
self.__database_session__.commit()
except:
self.__database_session__.rollback()
# class KitTypeReagentTypeAssociation(BaseClass):
# """
# table containing reagenttype/kittype associations
# DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
# """
# __tablename__ = "_reagenttypes_kittypes"
# reagent_types_id = Column(INTEGER, ForeignKey("_reagent_types.id"), primary_key=True) #: id of associated reagent type
# kits_id = Column(INTEGER, ForeignKey("_kits.id"), primary_key=True) #: id of associated reagent type
# uses = Column(JSON) #: map to location on excel sheets of different submission types
# required = Column(INTEGER) #: whether the reagent type is required for the kit (Boolean 1 or 0)
# last_used = Column(String(32)) #: last used lot number of this type of reagent
# kit_type = relationship(KitType, back_populates="kit_reagenttype_associations") #: relationship to associated kit
# # reference to the "ReagentType" object
# reagent_type = relationship(ReagentType, back_populates="reagenttype_kit_associations") #: relationship to associated reagent type
# def __init__(self, kit_type=None, reagent_type=None, uses=None, required=1):
# # logger.debug(f"Parameters: Kit={kit_type}, RT={reagent_type}, Uses={uses}, Required={required}")
# self.kit_type = kit_type
# self.reagent_type = reagent_type
# self.uses = uses
# self.required = required
# def __repr__(self) -> str:
# return f"<KitTypeReagentTypeAssociation({self.kit_type} & {self.reagent_type})>"
# @validates('required')
# def validate_age(self, key, value):
# """
# Ensures only 1 & 0 used in 'required'
# 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
# @validates('reagenttype')
# def validate_reagenttype(self, key, value):
# """
# Ensures reagenttype is an actual ReagentType
# Args:
# key (str)): name of attribute
# value (_type_): value of attribute
# Raises:
# ValueError: raised if reagenttype is not a ReagentType
# Returns:
# _type_: ReagentType
# """
# if not isinstance(value, ReagentType):
# raise ValueError(f'{value} is not a reagenttype')
# return value
# @classmethod
# @setup_lookup
# def query(cls,
# kit_type:KitType|str|None=None,
# reagent_type:ReagentType|str|None=None,
# limit:int=0
# ) -> KitTypeReagentTypeAssociation|List[KitTypeReagentTypeAssociation]:
# """
# Lookup junction of ReagentType and KitType
# Args:
# kit_type (models.KitType | str | None): KitType of interest.
# reagent_type (models.ReagentType | str | None): ReagentType of interest.
# limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
# Returns:
# models.KitTypeReagentTypeAssociation|List[models.KitTypeReagentTypeAssociation]: Junction of interest.
# """
# query: Query = cls.__database_session__.query(cls)
# match kit_type:
# case KitType():
# query = query.filter(cls.kit_type==kit_type)
# case str():
# query = query.join(KitType).filter(KitType.name==kit_type)
# case _:
# pass
# match reagent_type:
# case ReagentType():
# query = query.filter(cls.reagent_type==reagent_type)
# case str():
# query = query.join(ReagentType).filter(ReagentType.name==reagent_type)
# case _:
# pass
# if kit_type != None and reagent_type != None:
# limit = 1
# return query_return(query=query, limit=limit)
# def save(self) -> Report:
# """
# Adds this instance to the database and commits.
# Returns:
# Report: Result of save action
# """
# report = Report()
# self.__database_session__.add(self)
# self.__database_session__.commit()
# return report

View File

@@ -25,7 +25,7 @@ class Organization(BaseClass):
"""
Base of organization
"""
__tablename__ = "_organizations"
# __tablename__ = "_organizations"
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: organization name
@@ -73,14 +73,13 @@ class Organization(BaseClass):
Args:
ctx (Settings): Settings object passed down from GUI. Necessary to check authorization
"""
ctx.database_session.add(self)
ctx.database_session.commit()
super().save()
class Contact(BaseClass):
"""
Base of Contact
"""
__tablename__ = "_contacts"
# __tablename__ = "_contacts"
id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: contact name

View File

@@ -13,23 +13,24 @@ from json.decoder import JSONDecodeError
from sqlalchemy.ext.associationproxy import association_proxy
import pandas as pd
from openpyxl import Workbook
from . import BaseClass
from . import BaseClass, Equipment
from tools import check_not_nan, row_map, query_return, setup_lookup, jinja_template_loading
from datetime import datetime, date, time
from typing import List
from typing import List, Any
from dateutil.parser import parse
from dateutil.parser._parser import ParserError
from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError, StatementError
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
from pathlib import Path
logger = logging.getLogger(f"submissions.{__name__}")
class BasicSubmission(BaseClass):
"""
Concrete of basic submission which polymorphs into BacterialCulture and Wastewater
"""
__tablename__ = "_submissions"
# __tablename__ = "_submissions"
id = Column(INTEGER, primary_key=True) #: primary key
rsl_plate_num = Column(String(32), unique=True, nullable=False) #: RSL name (e.g. RSL-22-0012)
@@ -96,7 +97,7 @@ class BasicSubmission(BaseClass):
"""
return f"{self.rsl_plate_num} - {self.submitter_plate_num}"
def to_dict(self, full_data:bool=False) -> dict:
def to_dict(self, full_data:bool=False, backup:bool=False) -> dict:
"""
Constructs dictionary used in submissions summary
@@ -137,7 +138,7 @@ class BasicSubmission(BaseClass):
logger.error(f"We got an error retrieving reagents: {e}")
reagents = None
# samples = [item.sample.to_sub_dict(submission_rsl=self.rsl_plate_num) for item in self.submission_sample_associations]
samples = [item.to_sub_dict() for item in self.submission_sample_associations]
samples = self.adjust_to_dict_samples(backup=backup)
try:
equipment = [item.to_sub_dict() for item in self.submission_equipment_associations]
if len(equipment) == 0:
@@ -255,18 +256,7 @@ class BasicSubmission(BaseClass):
Returns:
list: list of htipick dictionaries for each sample
"""
output_list = []
for assoc in self.submission_sample_associations:
samp = assoc.sample.to_hitpick(submission_rsl=self.rsl_plate_num)
if samp != None:
if plate_number != None:
samp['plate_number'] = plate_number
samp['row'] = assoc.row
samp['column'] = assoc.column
samp['plate_name'] = self.rsl_plate_num
output_list.append(samp)
else:
continue
output_list = [assoc.to_hitpick() for assoc in self.submission_sample_associations]
return output_list
@classmethod
@@ -548,7 +538,7 @@ class BasicSubmission(BaseClass):
result = assoc.save()
return result
def to_pydantic(self):
def to_pydantic(self, backup:bool=False):
"""
Converts this instance into a PydSubmission
@@ -556,7 +546,7 @@ class BasicSubmission(BaseClass):
PydSubmission: converted object.
"""
from backend.validators import PydSubmission, PydSample, PydReagent, PydEquipment
dicto = self.to_dict(full_data=True)
dicto = self.to_dict(full_data=True, backup=backup)
logger.debug(f"Backup dictionary: {pformat(dicto)}")
# dicto['filepath'] = Path(tempfile.TemporaryFile().name)
new_dict = {}
@@ -567,7 +557,11 @@ class BasicSubmission(BaseClass):
case "samples":
new_dict[key] = [PydSample(**sample) for sample in dicto['samples']]
case "equipment":
new_dict[key] = [PydEquipment(**equipment) for equipment in dicto['equipment']]
# logger.debug(f"\n\nEquipment: {dicto['equipment']}\n\n")
try:
new_dict[key] = [PydEquipment(**equipment) for equipment in dicto['equipment']]
except TypeError as e:
logger.error(f"Possible no equipment error: {e}")
case "Plate Number":
new_dict['rsl_plate_num'] = dict(value=value, missing=True)
case "Submitter Plate Number":
@@ -582,25 +576,6 @@ class BasicSubmission(BaseClass):
# sys.exit()
return PydSubmission(**new_dict)
def backup(self, fname:Path, full_backup:bool=True):
"""
Exports xlsx and yml info files for this instance.
Args:
fname (Path): Filename of xlsx file.
"""
if full_backup:
backup = self.to_dict(full_data=True)
try:
with open(self.__backup_path__.joinpath(fname.with_suffix(".yml")), "w") as f:
yaml.dump(backup, f)
except KeyError as e:
logger.error(f"Problem saving yml backup file: {e}")
pyd = self.to_pydantic()
wb = pyd.autofill_excel()
wb = pyd.autofill_samples(wb)
wb.save(filename=fname.with_suffix(".xlsx"))
def save(self, original:bool=True):
"""
Adds this instance to database and commits.
@@ -610,24 +585,7 @@ class BasicSubmission(BaseClass):
"""
if original:
self.uploaded_by = getuser()
self.__database_session__.add(self)
self.__database_session__.commit()
def delete(self):
"""
Performs backup and deletes this instance from database.
Raises:
e: Raised in something goes wrong.
"""
fname = self.__backup_path__.joinpath(f"{self.rsl_plate_num}-backup({date.today().strftime('%Y%m%d')})")
self.backup(fname=fname)
self.__database_session__.delete(self)
try:
self.__database_session__.commit()
except (SQLIntegrityError, SQLOperationalError, AlcIntegrityError, AlcOperationalError) as e:
self.__database_session__.rollback()
raise e
super().save()
@classmethod
@setup_lookup
@@ -782,15 +740,121 @@ class BasicSubmission(BaseClass):
def get_used_equipment(self) -> List[str]:
return [item.role for item in self.submission_equipment_associations]
@classmethod
def adjust_autofill_samples(cls, samples:List[Any]) -> List[Any]:
logger.info(f"Hello from {cls.__mapper_args__['polymorphic_identity']} sampler")
return samples
def adjust_to_dict_samples(self, backup:bool=False):
logger.debug(f"Hello from {self.__class__.__name__} dictionary sample adjuster.")
return [item.to_sub_dict() for item in self.submission_sample_associations]
# Custom context events for the ui
def custom_context_events(self):
names = ["Delete", "Details", "Add Comment", "Add Equipment", "Export"]
funcs = [self.delete, self.show_details, self.add_comment, self.add_equipment, self.backup]
dicto = {item[0]:item[1] for item in zip(names, funcs)}
return dicto
def delete(self, obj=None):
"""
Performs backup and deletes this instance from database.
Raises:
e: Raised in something goes wrong.
"""
logger.debug("Hello from delete")
fname = self.__backup_path__.joinpath(f"{self.rsl_plate_num}-backup({date.today().strftime('%Y%m%d')})")
self.backup(fname=fname, full_backup=True)
self.__database_session__.delete(self)
try:
self.__database_session__.commit()
except (SQLIntegrityError, SQLOperationalError, AlcIntegrityError, AlcOperationalError) as e:
self.__database_session__.rollback()
raise e
def show_details(self, obj):
logger.debug("Hello from details")
from frontend.widgets.submission_details import SubmissionDetails
dlg = SubmissionDetails(parent=obj, sub=self)
if dlg.exec():
pass
def add_comment(self, obj):
from frontend.widgets.submission_details import SubmissionComment
dlg = SubmissionComment(parent=obj, submission=self)
if dlg.exec():
comment = dlg.parse_form()
try:
# For some reason .append results in new comment being ignored, so have to concatenate lists.
self.comment = self.comment + comment
except (AttributeError, TypeError) as e:
logger.error(f"Hit error ({e}) creating comment")
self.comment = comment
logger.debug(self.comment)
self.save(original=False)
# logger.debug(f"Save result: {result}")
def add_equipment(self, obj):
# submission_type = submission.submission_type_name
from frontend.widgets.equipment_usage import EquipmentUsage
dlg = EquipmentUsage(parent=obj, submission_type=self.submission_type_name, submission=self)
if dlg.exec():
equipment = dlg.parse_form()
logger.debug(f"We've got equipment: {equipment}")
for equip in equipment:
# e = Equipment.query(name=equip.name)
# assoc = SubmissionEquipmentAssociation(submission=submission, equipment=e)
# process = Process.query(name=equip.processes)
# assoc.process = process
# assoc.role = equip.role
_, assoc = equip.toSQL(submission=self)
# submission.submission_equipment_associations.append(assoc)
logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}")
# submission.save()
assoc.save()
else:
pass
def backup(self, obj=None, fname:Path|None=None, full_backup:bool=False):
"""
Exports xlsx and yml info files for this instance.
Args:
fname (Path): Filename of xlsx file.
"""
logger.debug("Hello from backup.")
if fname == None:
from frontend.widgets.functions import select_save_file
from backend.validators import RSLNamer
abbreviation = self.get_abbreviation()
file_data = dict(rsl_plate_num=self.rsl_plate_num, submission_type=self.submission_type_name, submitted_date=self.submitted_date, abbreviation=abbreviation)
fname = select_save_file(default_name=RSLNamer.construct_new_plate_name(data=file_data), extension="xlsx", obj=obj)
if full_backup:
backup = self.to_dict(full_data=True)
try:
with open(self.__backup_path__.joinpath(fname.with_suffix(".yml")), "w") as f:
yaml.dump(backup, f)
except KeyError as e:
logger.error(f"Problem saving yml backup file: {e}")
pyd = self.to_pydantic(backup=True)
wb = pyd.autofill_excel()
wb = pyd.autofill_samples(wb)
wb = pyd.autofill_equipment(wb)
wb.save(filename=fname.with_suffix(".xlsx"))
# Below are the custom submission types
class BacterialCulture(BasicSubmission):
"""
derivative submission type from BasicSubmission
"""
# id = Column(INTEGER, ForeignKey('basicsubmission.id'), primary_key=True)
id = Column(INTEGER, ForeignKey('_basicsubmission.id'), primary_key=True)
controls = relationship("Control", back_populates="submission", uselist=True) #: A control sample added to submission
__mapper_args__ = {"polymorphic_identity": "Bacterial Culture", "polymorphic_load": "inline"}
__mapper_args__ = dict(polymorphic_identity="Bacterial Culture",
polymorphic_load="inline",
inherit_condition=(id == BasicSubmission.id))
def to_dict(self, full_data:bool=False) -> dict:
"""
@@ -804,6 +868,10 @@ class BacterialCulture(BasicSubmission):
output['controls'] = [item.to_sub_dict() for item in self.controls]
return output
@classmethod
def get_abbreviation(cls):
return "BC"
@classmethod
def custom_platemap(cls, xl: pd.ExcelFile, plate_map: pd.DataFrame) -> pd.DataFrame:
"""
@@ -853,7 +921,7 @@ class BacterialCulture(BasicSubmission):
Extends parent
"""
from backend.validators import RSLNamer
data['abbreviation'] = "BC"
data['abbreviation'] = cls.get_abbreviation()
outstr = super().enforce_name(instr=instr, data=data)
# def construct(data:dict|None=None) -> str:
# """
@@ -932,10 +1000,12 @@ class Wastewater(BasicSubmission):
"""
derivative submission type from BasicSubmission
"""
# id = Column(INTEGER, ForeignKey('basicsubmission.id'), primary_key=True)
id = Column(INTEGER, ForeignKey('_basicsubmission.id'), primary_key=True)
ext_technician = Column(String(64))
pcr_technician = Column(String(64))
__mapper_args__ = {"polymorphic_identity": "Wastewater", "polymorphic_load": "inline"}
__mapper_args__ = __mapper_args__ = dict(polymorphic_identity="Wastewater",
polymorphic_load="inline",
inherit_condition=(id == BasicSubmission.id))
def to_dict(self, full_data:bool=False) -> dict:
"""
@@ -952,6 +1022,10 @@ class Wastewater(BasicSubmission):
output['Technician'] = f"Enr: {self.technician}, Ext: {self.ext_technician}, PCR: {self.pcr_technician}"
return output
@classmethod
def get_abbreviation(cls):
return "WW"
@classmethod
def parse_info(cls, input_dict:dict, xl:pd.ExcelFile|None=None) -> dict:
"""
@@ -1012,7 +1086,7 @@ class Wastewater(BasicSubmission):
Extends parent
"""
from backend.validators import RSLNamer
data['abbreviation'] = "WW"
data['abbreviation'] = cls.get_abbreviation()
outstr = super().enforce_name(instr=instr, data=data)
# def construct(data:dict|None=None):
# if "submitted_date" in data.keys():
@@ -1066,13 +1140,21 @@ class Wastewater(BasicSubmission):
# return "(?P<Wastewater>RSL(?:-|_)?WW(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789\s]|$)R?\d?)?)"
return "(?P<Wastewater>RSL(?:-|_)?WW(?:-|_)?20\d{2}-?\d{2}-?\d{2}(?:(_|-)?\d?([^_0123456789\s]|$)?R?\d?)?)"
@classmethod
def adjust_autofill_samples(cls, samples: List[Any]) -> List[Any]:
samples = super().adjust_autofill_samples(samples)
return [item for item in samples if not item.submitter_id.startswith("EN")]
class WastewaterArtic(BasicSubmission):
"""
derivative submission type for artic wastewater
"""
# id = Column(INTEGER, ForeignKey('basicsubmission.id'), primary_key=True)
__mapper_args__ = {"polymorphic_identity": "Wastewater Artic", "polymorphic_load": "inline"}
id = Column(INTEGER, ForeignKey('_basicsubmission.id'), primary_key=True)
__mapper_args__ = dict(polymorphic_identity="Wastewater Artic",
polymorphic_load="inline",
inherit_condition=(id == BasicSubmission.id))
artic_technician = Column(String(64))
dna_core_submission_number = Column(String(64))
def calculate_base_cost(self):
"""
@@ -1093,6 +1175,10 @@ class WastewaterArtic(BasicSubmission):
except Exception as e:
logger.error(f"Calculation error: {e}")
@classmethod
def get_abbreviation(cls):
return "AR"
@classmethod
def parse_samples(cls, input_dict: dict) -> dict:
"""
@@ -1109,15 +1195,45 @@ class WastewaterArtic(BasicSubmission):
# Because generate_sample_object needs the submitter_id and the artic has the "({origin well})"
# at the end, this has to be done here. No moving to sqlalchemy object :(
input_dict['submitter_id'] = re.sub(r"\s\(.+\)\s?$", "", str(input_dict['submitter_id'])).strip()
if "ENC" in input_dict['submitter_id']:
input_dict['submitter_id'] = cls.en_adapter(input_str=input_dict['submitter_id'])
return input_dict
@classmethod
def en_adapter(cls, input_str) -> str:
processed = re.sub(r"[A-Z]", "", input_str)
try:
en_num = re.search(r"\-\d{1}$", processed).group()
processed = processed.replace(en_num, "", -1)
except AttributeError:
en_num = "1"
en_num = en_num.strip("-")
logger.debug(f"Processed after en-num: {processed}")
try:
plate_num = re.search(r"\-\d{1}$", processed).group()
processed = processed.replace(plate_num, "", -1)
except AttributeError:
plate_num = "1"
plate_num = plate_num.strip("-")
logger.debug(f"Processed after plate-num: {processed}")
day = re.search(r"\d{2}$", processed).group()
processed = processed.replace(day, "", -1)
logger.debug(f"Processed after day: {processed}")
month = re.search(r"\d{2}$", processed).group()
processed = processed.replace(month, "", -1)
processed = processed.replace("--", "")
logger.debug(f"Processed after month: {processed}")
year = re.search(r'^(?:\d{2})?\d{2}', processed).group()
year = f"20{year}"
return f"EN{year}{month}{day}-{en_num}"
@classmethod
def enforce_name(cls, instr:str, data:dict|None=None) -> str:
"""
Extends parent
"""
from backend.validators import RSLNamer
data['abbreviation'] = "AR"
data['abbreviation'] = cls.get_abbreviation()
outstr = super().enforce_name(instr=instr, data=data)
# def construct(data:dict|None=None):
# today = datetime.now()
@@ -1240,6 +1356,36 @@ class WastewaterArtic(BasicSubmission):
worksheet.cell(row=iii, column=jjj, value=value)
return input_excel
def adjust_to_dict_samples(self, backup:bool=False):
logger.debug(f"Hello from {self.__class__.__name__} dictionary sample adjuster.")
if backup:
output = []
for assoc in self.submission_sample_associations:
dicto = assoc.to_sub_dict()
old_sub = assoc.sample.get_previous_ww_submission(current_artic_submission=self)
try:
dicto['plate_name'] = old_sub.rsl_plate_num
except AttributeError:
dicto['plate_name'] = ""
old_assoc = WastewaterAssociation.query(submission=old_sub, sample=assoc.sample, limit=1)
dicto['well'] = f"{row_map[old_assoc.row]}{old_assoc.column}"
output.append(dicto)
else:
output = super().adjust_to_dict_samples(backup=False)
return output
def custom_context_events(self):
events = super().custom_context_events()
events['Gel Box'] = self.gel_box
return events
def gel_box(self, obj):
from frontend.widgets.gel_checker import GelBox
dlg = GelBox(parent=obj)
if dlg.exec():
output = dlg.parse_form()
print(output)
# Sample Classes
class BasicSample(BaseClass):
@@ -1247,7 +1393,7 @@ class BasicSample(BaseClass):
Base of basic sample which polymorphs into BCSample and WWSample
"""
__tablename__ = "_samples"
# __tablename__ = "_samples"
id = Column(INTEGER, primary_key=True) #: primary key
submitter_id = Column(String(64), nullable=False, unique=True) #: identification from submitter
@@ -1295,6 +1441,18 @@ class BasicSample(BaseClass):
def __repr__(self) -> str:
return f"<{self.sample_type.replace('_', ' ').title().replace(' ', '')}({self.submitter_id})>"
def to_sub_dict(self, submission_rsl:str) -> dict:
"""
gui friendly dictionary, extends parent method.
Returns:
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
"""
sample = {}
sample['submitter_id'] = self.submitter_id
sample['sample_type'] = self.sample_type
return sample
def set_attribute(self, name:str, value):
"""
Custom attribute setter
@@ -1307,59 +1465,6 @@ class BasicSample(BaseClass):
setattr(self, name, value)
except AttributeError:
logger.error(f"Attribute {name} not found")
def to_sub_dict(self, submission_rsl:str|BasicSubmission) -> dict:
"""
Returns a dictionary of locations.
Args:
submission_rsl (str): Submission RSL number.
Returns:
dict: 'well' and sample submitter_id as 'name'
"""
match submission_rsl:
case BasicSubmission():
assoc = [item for item in self.sample_submission_associations if item.submission==submission_rsl][0]
case str():
assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
sample = {}
try:
sample['well'] = f"{row_map[assoc.row]}{assoc.column}"
except KeyError as e:
logger.error(f"Unable to find row {assoc.row} in row_map.")
sample['well'] = None
sample['name'] = self.submitter_id
sample['submitter_id'] = self.submitter_id
sample['sample_type'] = self.sample_type
if isinstance(assoc.row, list):
sample['row'] = assoc.row[0]
else:
sample['row'] = assoc.row
if isinstance(assoc.column, list):
sample['column'] = assoc.column[0]
else:
sample['column'] = assoc.column
return sample
def to_hitpick(self, submission_rsl:str|None=None) -> dict|None:
"""
Outputs a dictionary usable for html plate maps.
Returns:
dict: dictionary of sample id, row and column in elution plate
"""
# Since there is no PCR, negliable result is necessary.
# assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
fields = self.to_sub_dict(submission_rsl=submission_rsl)
env = jinja_template_loading()
template = env.get_template("tooltip.html")
tooltip_text = template.render(fields=fields)
# tooltip_text = f"""
# Sample name: {self.submitter_id}<br>
# Well: {row_map[assoc.row]}{assoc.column}
# """
return dict(name=self.submitter_id[:10], positive=False, tooltip=tooltip_text)
@classmethod
def find_subclasses(cls, attrs:dict|None=None, sample_type:str|None=None) -> BasicSample:
@@ -1507,11 +1612,6 @@ class BasicSample(BaseClass):
logger.debug(f"Creating instance: {instance}")
return instance
def save(self):
# raise AttributeError(f"Save not implemented for {self.__class__}")
self.__database_session__.add(self)
self.__database_session__.commit()
def delete(self):
raise AttributeError(f"Delete not implemented for {self.__class__}")
@@ -1521,7 +1621,7 @@ class WastewaterSample(BasicSample):
"""
Derivative wastewater sample
"""
# id = Column(INTEGER, ForeignKey('basicsample.id'), primary_key=True)
id = Column(INTEGER, ForeignKey('_basicsample.id'), primary_key=True)
ww_processing_num = Column(String(64)) #: wastewater processing number
ww_full_sample_id = Column(String(64)) #: full id given by entrics
rsl_number = Column(String(64)) #: rsl plate identification number
@@ -1529,46 +1629,21 @@ class WastewaterSample(BasicSample):
received_date = Column(TIMESTAMP) #: Date sample received
notes = Column(String(2000)) #: notes from submission form
sample_location = Column(String(8)) #: location on 24 well plate
__mapper_args__ = {"polymorphic_identity": "Wastewater Sample", "polymorphic_load": "inline"}
__mapper_args__ = dict(polymorphic_identity="Wastewater Sample",
polymorphic_load="inline",
inherit_condition=(id == BasicSample.id))
def to_hitpick(self, submission_rsl:str) -> dict|None:
def to_sub_dict(self, submission_rsl:str) -> dict:
"""
Outputs a dictionary usable for html plate maps. Extends parent method.
Args:
submission_rsl (str): rsl_plate_num of the submission
gui friendly dictionary, extends parent method.
Returns:
dict|None: dict: dictionary of sample id, row and column in elution plate
"""
sample = super().to_hitpick(submission_rsl=submission_rsl)
assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
# if either n1 or n2 is positive, include this sample
try:
sample['positive'] = any(["positive" in item for item in [assoc.n1_status, assoc.n2_status]])
except (TypeError, AttributeError) as e:
logger.error(f"Couldn't check positives for {self.rsl_number}. Looks like there isn't PCR data.")
try:
sample['tooltip'] += f"<br>- ct N1: {'{:.2f}'.format(assoc.ct_n1)} ({assoc.n1_status})<br>- ct N2: {'{:.2f}'.format(assoc.ct_n2)} ({assoc.n2_status})"
except (TypeError, AttributeError) as e:
logger.error(f"Couldn't set tooltip for {self.rsl_number}. Looks like there isn't PCR data.")
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
"""
sample = super().to_sub_dict(submission_rsl=submission_rsl)
sample['ww_processing_num'] = self.ww_processing_num
return sample
def get_recent_ww_submission(self) -> Wastewater:
"""
Gets most recent associated wastewater submission
Returns:
Wastewater: Most recent wastewater submission
"""
results = [sub for sub in self.submissions if isinstance(sub, Wastewater)]
if len(results) > 1:
results = results.sort(key=lambda sub: sub.submitted_date)
try:
return results[0]
except IndexError:
return None
@classmethod
def parse_sample(cls, input_dict: dict) -> dict:
output_dict = super().parse_sample(input_dict)
@@ -1591,35 +1666,28 @@ class WastewaterSample(BasicSample):
case _:
del output_dict['collection_date']
return output_dict
def to_sub_dict(self, submission_rsl: str | BasicSubmission) -> dict:
sample = super().to_sub_dict(submission_rsl)
if self.ww_processing_num != None:
sample['ww_processing_num'] = self.ww_processing_num
else:
sample['ww_processing_num'] = self.submitter_id
def get_previous_ww_submission(self, current_artic_submission:WastewaterArtic):
# assocs = [assoc for assoc in self.sample_submission_associations if assoc.submission.submission_type_name=="Wastewater"]
subs = self.submissions[:self.submissions.index(current_artic_submission)]
subs = [sub for sub in subs if sub.submission_type_name=="Wastewater"]
logger.debug(f"Submissions up to current artic submission: {subs}")
try:
assoc = [item for item in self.sample_submission_associations if item.submission.submission_type_name=="Wastewater"][-1]
except:
assoc = None
if assoc != None:
try:
sample['ct'] = f"{assoc.ct_n1:.2f}, {assoc.ct_n2:.2f}"
except TypeError:
sample['ct'] = "None, None"
sample['source_plate'] = assoc.submission.rsl_plate_num
sample['source_well'] = f"{row_map[assoc.row]}{assoc.column}"
return sample
return subs[-1]
except IndexError:
return None
class BacterialCultureSample(BasicSample):
"""
base of bacterial culture sample
"""
# id = Column(INTEGER, ForeignKey('basicsample.id'), primary_key=True)
id = Column(INTEGER, ForeignKey('_basicsample.id'), primary_key=True)
organism = Column(String(64)) #: bacterial specimen
concentration = Column(String(16)) #: sample concentration
control = relationship("Control", back_populates="sample", uselist=False)
__mapper_args__ = {"polymorphic_identity": "Bacterial Culture Sample", "polymorphic_load": "inline"}
__mapper_args__ = dict(polymorphic_identity="Bacterial Culture Sample",
polymorphic_load="inline",
inherit_condition=(id == BasicSample.id))
def to_sub_dict(self, submission_rsl:str) -> dict:
"""
@@ -1632,15 +1700,18 @@ class BacterialCultureSample(BasicSample):
sample['name'] = self.submitter_id
sample['organism'] = self.organism
sample['concentration'] = self.concentration
return sample
def to_hitpick(self, submission_rsl: str | None = None) -> dict | None:
sample = super().to_hitpick(submission_rsl)
if self.control != None:
sample['colour'] = [0,128,0]
sample['tooltip'] += f"<br>- Control: {self.control.controltype.name} - {self.control.controltype.targets}"
sample['tooltip'] = f"Control: {self.control.controltype.name} - {self.control.controltype.targets}"
return sample
# def to_hitpick(self, submission_rsl: str | None = None) -> dict | None:
# sample = super().to_hitpick(submission_rsl)
# if self.control != None:
# sample['colour'] = [0,128,0]
# sample['tooltip'] += f"<br>- Control: {self.control.controltype.name} - {self.control.controltype.targets}"
# return sample
# Submission to Sample Associations
class SubmissionSampleAssociation(BaseClass):
@@ -1649,7 +1720,7 @@ class SubmissionSampleAssociation(BaseClass):
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
"""
__tablename__ = "_submission_sample"
# __tablename__ = "_submission_sample"
sample_id = Column(INTEGER, ForeignKey("_samples.id"), nullable=False) #: id of associated sample
submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True) #: id of associated submission
@@ -1688,7 +1759,12 @@ class SubmissionSampleAssociation(BaseClass):
Returns:
dict: Updated dictionary with row, column and well updated
"""
# Get sample info
sample = self.sample.to_sub_dict(submission_rsl=self.submission)
# sample = {}
sample['name'] = self.sample.submitter_id
# sample['submitter_id'] = self.sample.submitter_id
# sample['sample_type'] = self.sample.sample_type
sample['row'] = self.row
sample['column'] = self.column
try:
@@ -1696,6 +1772,33 @@ class SubmissionSampleAssociation(BaseClass):
except KeyError as e:
logger.error(f"Unable to find row {self.row} in row_map.")
sample['well'] = None
sample['plate_name'] = self.submission.rsl_plate_num
sample['positive'] = False
return sample
def to_hitpick(self) -> dict|None:
"""
Outputs a dictionary usable for html plate maps.
Returns:
dict: dictionary of sample id, row and column in elution plate
"""
# Since there is no PCR, negliable result is necessary.
# assoc = [item for item in self.sample_submission_associations if item.submission.rsl_plate_num==submission_rsl][0]
sample = self.to_sub_dict()
env = jinja_template_loading()
template = env.get_template("tooltip.html")
tooltip_text = template.render(fields=sample)
try:
tooltip_text += sample['tooltip']
except KeyError:
pass
# tooltip_text = f"""
# Sample name: {self.submitter_id}<br>
# Well: {row_map[fields['row']]}{fields['column']}
# """
sample.update(dict(name=self.sample.submitter_id[:10], tooltip=tooltip_text))
return sample
@classmethod
@@ -1831,14 +1934,6 @@ class SubmissionSampleAssociation(BaseClass):
instance = used_cls(submission=submission, sample=sample, **kwargs)
return instance
def save(self):
"""
Adds this instance to the database and commits.
"""
self.__database_session__.add(self)
self.__database_session__.commit()
return None
def delete(self):
raise AttributeError(f"Delete not implemented for {self.__class__}")
@@ -1846,10 +1941,32 @@ class WastewaterAssociation(SubmissionSampleAssociation):
"""
Derivative custom Wastewater/Submission Association... fancy.
"""
sample_id = Column(INTEGER, ForeignKey('_submissionsampleassociation.sample_id'), primary_key=True)
submission_id = Column(INTEGER, ForeignKey('_submissionsampleassociation.submission_id'), primary_key=True)
ct_n1 = Column(FLOAT(2)) #: AKA ct for N1
ct_n2 = Column(FLOAT(2)) #: AKA ct for N2
n1_status = Column(String(32)) #: positive or negative for N1
n2_status = Column(String(32)) #: positive or negative for N2
pcr_results = Column(JSON) #: imported PCR status from QuantStudio
__mapper_args__ = {"polymorphic_identity": "Wastewater Association", "polymorphic_load": "inline"}
# __mapper_args__ = {"polymorphic_identity": "Wastewater Association", "polymorphic_load": "inline"}
__mapper_args__ = dict(polymorphic_identity="Wastewater Association",
polymorphic_load="inline",
inherit_condition=(sample_id == SubmissionSampleAssociation.sample_id))
def to_sub_dict(self) -> dict:
sample = super().to_sub_dict()
sample['ct'] = f"({self.ct_n1}, {self.ct_n2})"
try:
sample['positive'] = any(["positive" in item for item in [self.n1_status, self.n2_status]])
except (TypeError, AttributeError) as e:
logger.error(f"Couldn't check positives for {self.sample.rsl_number}. Looks like there isn't PCR data.")
return sample
def to_hitpick(self) -> dict | None:
sample = super().to_hitpick()
try:
sample['tooltip'] += f"<br>- ct N1: {'{:.2f}'.format(self.ct_n1)} ({self.n1_status})<br>- ct N2: {'{:.2f}'.format(self.ct_n2)} ({self.n2_status})"
except (TypeError, AttributeError) as e:
logger.error(f"Couldn't set tooltip for {self.sample.rsl_number}. Looks like there isn't PCR data.")
return sample