Code cleanup, dependency update, various bug fixes

This commit is contained in:
lwark
2024-05-24 13:02:46 -05:00
parent 2814be8980
commit e047d1a9ee
24 changed files with 403 additions and 561 deletions

View File

@@ -102,7 +102,7 @@ class BaseClass(Base):
@classmethod
def query(cls, **kwargs) -> Any | List[Any]:
"""
Default query function for models
Default query function for models. Overridden in most models.
Returns:
Any | List[Any]: Result of query execution.
@@ -128,7 +128,7 @@ class BaseClass(Base):
query: Query = cls.__database_session__.query(model)
# logger.debug(f"Grabbing singles using {model.get_default_info}")
singles = model.get_default_info('singles')
logger.debug(f"Querying: {model}, with kwargs: {kwargs}")
logger.info(f"Querying: {model}, with kwargs: {kwargs}")
for k, v in kwargs.items():
# logger.debug(f"Using key: {k} with value: {v}")
try:

View File

@@ -63,16 +63,16 @@ class ControlType(BaseClass):
Returns:
List[str]: list of subtypes available
"""
# Get first instance since all should have same subtypes
# Get mode of instance
# NOTE: Get first instance since all should have same subtypes
# NOTE: Get mode of instance
jsoner = getattr(self.instances[0], mode)
# logger.debug(f"JSON out: {jsoner.keys()}")
try:
# Pick genera (all should have same subtypes)
# NOTE: Pick genera (all should have same subtypes)
genera = list(jsoner.keys())[0]
except IndexError:
return []
# remove items that don't have relevant data
# NOTE: remove items that don't have relevant data
subtypes = [item for item in jsoner[genera] if "_hashes" not in item and "_ratio" not in item]
return subtypes
@@ -135,7 +135,6 @@ class Control(BaseClass):
"""
# logger.debug("loading json string into dict")
try:
# kraken = json.loads(self.kraken)
kraken = self.kraken
except TypeError:
kraken = {}
@@ -178,7 +177,7 @@ class Control(BaseClass):
data = self.__getattribute__(mode)
except TypeError:
data = {}
logger.debug(f"Length of data: {len(data)}")
# logger.debug(f"Length of data: {len(data)}")
# logger.debug("dict keys are genera of bacteria, e.g. 'Streptococcus'")
for genus in data:
_dict = dict(
@@ -236,7 +235,7 @@ class Control(BaseClass):
models.Control|List[models.Control]: Control object of interest.
"""
query: Query = cls.__database_session__.query(cls)
# by control type
# NOTE: by control type
match control_type:
case ControlType():
# logger.debug(f"Looking up control by control type: {control_type}")
@@ -246,7 +245,7 @@ class Control(BaseClass):
query = query.join(ControlType).filter(ControlType.name == control_type)
case _:
pass
# by date range
# NOTE: by date range
if start_date is not None and end_date is None:
logger.warning(f"Start date with no end date, using today.")
end_date = date.today()

View File

@@ -120,8 +120,8 @@ class KitType(BaseClass):
submission_type (str | Submissiontype | None, optional): Submission type to narrow results. Defaults to None.
Returns:
list: List of reagent types
"""
List[ReagentType]: List of reagents linked to this kit.
"""
match submission_type:
case SubmissionType():
# logger.debug(f"Getting reagents by SubmissionType {submission_type}")
@@ -152,17 +152,15 @@ class KitType(BaseClass):
dict: Dictionary containing information locations.
"""
info_map = {}
# Account for submission_type variable type.
# NOTE: Account for submission_type variable type.
match submission_type:
case str():
# logger.debug(f"Constructing xl map with str {submission_type}")
assocs = [item for item in self.kit_reagenttype_associations if
item.submission_type.name == submission_type]
# st_assoc = [item for item in self.used_for if submission_type == item.name][0]
case SubmissionType():
# logger.debug(f"Constructing xl map with SubmissionType {submission_type}")
assocs = [item for item in self.kit_reagenttype_associations if item.submission_type == submission_type]
# st_assoc = submission_type
case _:
raise ValueError(f"Wrong variable type: {type(submission_type)} used!")
# logger.debug("Get all KitTypeReagentTypeAssociation for SubmissionType")
@@ -371,10 +369,10 @@ class Reagent(BaseClass):
dict: representation of the reagent's attributes
"""
if extraction_kit is not None:
# Get the intersection of this reagent's ReagentType and all ReagentTypes in KitType
# NOTE: Get the intersection of this reagent's ReagentType and all ReagentTypes in KitType
try:
reagent_role = list(set(self.type).intersection(extraction_kit.reagent_types))[0]
# Most will be able to fall back to first ReagentType in itself because most will only have 1.
# NOTE: Most will be able to fall back to first ReagentType in itself because most will only have 1.
except:
reagent_role = self.type[0]
else:
@@ -383,7 +381,7 @@ class Reagent(BaseClass):
rtype = reagent_role.name.replace("_", " ")
except AttributeError:
rtype = "Unknown"
# Calculate expiry with EOL from ReagentType
# NOTE: Calculate expiry with EOL from ReagentType
try:
place_holder = self.expiry + reagent_role.eol_ext
except (TypeError, AttributeError) as e:
@@ -467,7 +465,7 @@ class Reagent(BaseClass):
match name:
case str():
# logger.debug(f"Looking up reagent by name str: {name}")
# Not limited due to multiple reagents having same name.
# NOTE: Not limited due to multiple reagents having same name.
query = query.filter(cls.name == name)
case _:
pass
@@ -475,7 +473,7 @@ class Reagent(BaseClass):
case str():
# logger.debug(f"Looking up reagent by lot number str: {lot_number}")
query = query.filter(cls.lot == lot_number)
# In this case limit number returned.
# NOTE: In this case limit number returned.
limit = 1
case _:
pass
@@ -516,10 +514,6 @@ class Discount(BaseClass):
organization (models.Organization | str | int): Organization receiving discount.
kit_type (models.KitType | str | int): Kit discount received on.
Raises:
ValueError: Invalid Organization
ValueError: Invalid kit.
Returns:
models.Discount|List[models.Discount]: Discount(s) of interest.
"""
@@ -535,7 +529,6 @@ class Discount(BaseClass):
# logger.debug(f"Looking up discount with organization id: {organization}")
query = query.join(Organization).filter(Organization.id == organization)
case _:
# raise ValueError(f"Invalid value for organization: {organization}")
pass
match kit_type:
case KitType():
@@ -548,7 +541,6 @@ class Discount(BaseClass):
# logger.debug(f"Looking up discount with kit type id: {kit_type}")
query = query.join(KitType).filter(KitType.id == kit_type)
case _:
# raise ValueError(f"Invalid value for kit type: {kit_type}")
pass
return cls.execute_query(query=query)
@@ -634,11 +626,18 @@ class SubmissionType(BaseClass):
self.save()
def construct_info_map(self, mode: Literal['read', 'write']) -> dict:
"""
Make of map of where all fields are located in excel sheet
Args:
mode (Literal["read", "write"]): Which mode to get locations for
Returns:
dict: Map of locations
"""
info = self.info_map
# logger.debug(f"Info map: {info}")
output = {}
# for k,v in info.items():
# info[k]['write'] += info[k]['read']
match mode:
case "read":
output = {k: v[mode] for k, v in info.items() if v[mode]}
@@ -647,7 +646,13 @@ class SubmissionType(BaseClass):
output = {k: v for k, v in output.items() if all([isinstance(item, dict) for item in v])}
return output
def construct_sample_map(self):
def construct_sample_map(self) -> dict:
"""
Returns sample map
Returns:
dict: sample location map
"""
return self.sample_map
def construct_equipment_map(self) -> dict:
@@ -655,7 +660,7 @@ class SubmissionType(BaseClass):
Constructs map of equipment to excel cells.
Returns:
List[dict]: List of equipment locations in excel sheet
dict: Map equipment locations in excel sheet
"""
output = {}
# logger.debug("Iterating through equipment roles")
@@ -671,7 +676,7 @@ class SubmissionType(BaseClass):
Returns PydEquipmentRole of all equipment associated with this SubmissionType
Returns:
List['PydEquipmentRole']: List of equipment roles
List[PydEquipmentRole]: List of equipment roles
"""
return [item.to_pydantic(submission_type=self, extraction_kit=extraction_kit) for item in self.equipment]
@@ -702,7 +707,13 @@ class SubmissionType(BaseClass):
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]))
def get_submission_class(self):
def get_submission_class(self) -> "BasicSubmission":
"""
Gets submission class associated with this submission type.
Returns:
BasicSubmission: Submission class
"""
from .submissions import BasicSubmission
return BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.name)
@@ -1063,7 +1074,7 @@ class Equipment(BaseClass):
processes (bool, optional): Whether to include processes. Defaults to False.
Returns:
dict: _description_
dict: Dictionary representation of this equipment
"""
if not processes:
return {k: v for k, v in self.__dict__.items() if k != 'processes'}
@@ -1152,7 +1163,7 @@ class Equipment(BaseClass):
extraction_kit (str | KitType | None, optional): Relevant KitType. Defaults to None.
Returns:
PydEquipment: _description_
PydEquipment: pydantic equipment object
"""
from backend.validators.pydant import PydEquipment
return PydEquipment(
@@ -1179,7 +1190,6 @@ class Equipment(BaseClass):
class EquipmentRole(BaseClass):
"""
Abstract roles for equipment
"""
id = Column(INTEGER, primary_key=True) #: Role id, primary key
@@ -1331,7 +1341,7 @@ class SubmissionEquipmentAssociation(BaseClass):
equipment = relationship(Equipment, back_populates="equipment_submission_associations") #: associated equipment
def __repr__(self):
return f"<SubmissionEquipmentAssociation({self.submission.rsl_plate_num}&{self.equipment.name})>"
return f"<SubmissionEquipmentAssociation({self.submission.rsl_plate_num} & {self.equipment.name})>"
def __init__(self, submission, equipment, role: str = "None"):
self.submission = submission

View File

@@ -107,6 +107,12 @@ class BasicSubmission(BaseClass):
@classmethod
def jsons(cls) -> List[str]:
"""
Get list of JSON db columns
Returns:
List[str]: List of column names
"""
output = [item.name for item in cls.__table__.columns if isinstance(item.type, JSON)]
if issubclass(cls, BasicSubmission) and not cls.__name__ == "BasicSubmission":
output += BasicSubmission.jsons()
@@ -114,6 +120,12 @@ class BasicSubmission(BaseClass):
@classmethod
def timestamps(cls) -> List[str]:
"""
Get list of TIMESTAMP columns
Returns:
List[str]: List of column names
"""
output = [item.name for item in cls.__table__.columns if isinstance(item.type, TIMESTAMP)]
if issubclass(cls, BasicSubmission) and not cls.__name__ == "BasicSubmission":
output += BasicSubmission.timestamps()
@@ -122,7 +134,7 @@ class BasicSubmission(BaseClass):
# TODO: Beef up this to include info_map from DB
@classmethod
def get_default_info(cls, *args):
# Create defaults for all submission_types
# NOTE: Create defaults for all submission_types
parent_defs = super().get_default_info()
recover = ['filepath', 'samples', 'csv', 'comment', 'equipment']
dicto = dict(
@@ -132,9 +144,7 @@ class BasicSubmission(BaseClass):
# NOTE: Fields not placed in ui form
form_ignore=['reagents', 'ctx', 'id', 'cost', 'extraction_info', 'signed_by', 'comment'] + recover,
# NOTE: Fields not placed in ui form to be moved to pydantic
form_recover=recover,
# parser_ignore=['samples', 'signed_by'] + [item for item in cls.jsons() if item != "comment"],
# excel_ignore=[],
form_recover=recover
)
# logger.debug(dicto['singles'])
# NOTE: Singles tells the query which fields to set limit to 1
@@ -151,7 +161,6 @@ class BasicSubmission(BaseClass):
st = cls.get_submission_type()
if st is None:
logger.error("No default info for BasicSubmission.")
# return output
else:
output['submission_type'] = st.name
for k, v in st.defaults.items():
@@ -169,16 +178,37 @@ class BasicSubmission(BaseClass):
return output
@classmethod
def get_submission_type(cls):
def get_submission_type(cls) -> SubmissionType:
"""
Gets the SubmissionType associated with this class
Returns:
SubmissionType: SubmissionType with name equal to this polymorphic identity
"""
name = cls.__mapper_args__['polymorphic_identity']
return SubmissionType.query(name=name)
@classmethod
def construct_info_map(cls, mode:Literal['read', 'write']):
def construct_info_map(cls, mode:Literal["read", "write"]) -> dict:
"""
Method to call submission type's construct info map.
Args:
mode (Literal["read", "write"]): Which map to construct.
Returns:
dict: Map of info locations.
"""
return cls.get_submission_type().construct_info_map(mode=mode)
@classmethod
def construct_sample_map(cls):
def construct_sample_map(cls) -> dict:
"""
Method to call submission type's construct_sample_map
Returns:
dict: sample location map
"""
return cls.get_submission_type().construct_sample_map()
def to_dict(self, full_data: bool = False, backup: bool = False, report: bool = False) -> dict:
@@ -192,7 +222,7 @@ class BasicSubmission(BaseClass):
Returns:
dict: dictionary used in submissions summary and details
"""
# get lab from nested organization object
# NOTE: get lab from nested organization object
# logger.debug(f"Converting {self.rsl_plate_num} to dict...")
try:
sub_lab = self.submitting_lab.name
@@ -202,12 +232,12 @@ class BasicSubmission(BaseClass):
sub_lab = sub_lab.replace("_", " ").title()
except AttributeError:
pass
# get extraction kit name from nested kit object
# NOTE: get extraction kit name from nested kit object
try:
ext_kit = self.extraction_kit.name
except AttributeError:
ext_kit = None
# load scraped extraction info
# NOTE: load scraped extraction info
try:
ext_info = self.extraction_info
except TypeError:
@@ -324,7 +354,7 @@ class BasicSubmission(BaseClass):
def make_plate_map(self, plate_rows: int = 8, plate_columns=12) -> str:
"""
Constructs an html based plate map.
Constructs an html based plate map for submission details.
Args:
sample_list (list): List of submission samples
@@ -386,7 +416,7 @@ class BasicSubmission(BaseClass):
subs = [item.to_dict() for item in cls.query(submission_type=submission_type, limit=limit, chronologic=chronologic)]
# logger.debug(f"Got {len(subs)} submissions.")
df = pd.DataFrame.from_records(subs)
# Exclude sub information
# NOTE: Exclude sub information
for item in ['controls', 'extraction_info', 'pcr_info', 'comment', 'comments', 'samples', 'reagents',
'equipment', 'gel_info', 'gel_image', 'dna_core_submission_number', 'gel_controls']:
try:
@@ -414,9 +444,6 @@ class BasicSubmission(BaseClass):
# logger.debug(f"Looking up organization: {value}")
field_value = Organization.query(name=value)
# logger.debug(f"Got {field_value} for organization {value}")
# case "submitter_plate_num":
# # logger.debug(f"Submitter plate id: {value}")
# field_value = value
case "samples":
for sample in value:
# logger.debug(f"Parsing {sample} to sql.")
@@ -436,17 +463,6 @@ class BasicSubmission(BaseClass):
field_value = value
case "ctx" | "csv" | "filepath" | "equipment":
return
# case "comment":
# if value == "" or value == None or value == 'null':
# field_value = None
# else:
# field_value = dict(name=getuser(), text=value, time=datetime.now())
# # if self.comment is None:
# # self.comment = [field_value]
# # else:
# # self.comment.append(field_value)
# self.update_json(field=key, value=field_value)
# return
case item if item in self.jsons():
logger.debug(f"Setting JSON attribute.")
existing = self.__getattribute__(key)
@@ -1852,13 +1868,6 @@ class WastewaterArtic(BasicSubmission):
set_plate = None
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}"
for item in self.source_plates:
old_plate = WastewaterAssociation.query(submission=item['plate'], sample=assoc.sample, limit=1)
if old_plate is not None:
@@ -1879,6 +1888,12 @@ class WastewaterArtic(BasicSubmission):
events['Gel Box'] = self.gel_box
return events
def set_attribute(self, key: str, value):
super().set_attribute(key=key, value=value)
if key == 'gel_info':
if len(self.gel_info) > 3:
self.gel_info = self.gel_info[-3:]
def gel_box(self, obj):
"""
Creates widget to perform gel viewing operations