Improvements to submission querying.

This commit is contained in:
lwark
2024-04-26 15:25:24 -05:00
parent b619d751b8
commit 5378c79933
7 changed files with 164 additions and 136 deletions

View File

@@ -1,3 +1,7 @@
- [ ] Make reporting better.
- [ ] Build master query method?
- Obviously there will need to be extensions, but I feel the attr method I have in Submissions could work.
- [x] Fix Artic RSLNamer
- [x] Put "Not applicable" reagents in to_dict() method. - [x] Put "Not applicable" reagents in to_dict() method.
- Currently in to_pydantic(). - Currently in to_pydantic().
- [x] Critical: Convert Json lits to dicts so I can have them update properly without using crashy Sqlalchemy-json - [x] Critical: Convert Json lits to dicts so I can have them update properly without using crashy Sqlalchemy-json

View File

@@ -2,8 +2,10 @@
Contains all models for sqlalchemy Contains all models for sqlalchemy
''' '''
import sys import sys
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from typing import Any, List
from pathlib import Path
# Load testing environment # Load testing environment
if 'pytest' in sys.modules: if 'pytest' in sys.modules:
from pathlib import Path from pathlib import Path
@@ -11,28 +13,32 @@ if 'pytest' in sys.modules:
Base: DeclarativeMeta = declarative_base() Base: DeclarativeMeta = declarative_base()
class BaseClass(Base): class BaseClass(Base):
""" """
Abstract class to pass ctx values to all SQLAlchemy objects. Abstract class to pass ctx values to all SQLAlchemy objects.
Args:
Base (DeclarativeMeta): Declarative base for metadata.
""" """
__abstract__ = True __abstract__ = True #: Will not be added to DB
__table_args__ = {'extend_existing': True} __table_args__ = {'extend_existing': True} #: Will only add new columns
@declared_attr @declared_attr
def __tablename__(cls): def __tablename__(cls) -> str:
""" """
Set tablename to lowercase class name Sets table name to lower case class name.
Returns:
str: lower case class name
""" """
return f"_{cls.__name__.lower()}" return f"_{cls.__name__.lower()}"
@declared_attr @declared_attr
def __database_session__(cls): def __database_session__(cls) -> Session:
""" """
Pull db session from ctx Pull db session from ctx to be used in operations
Returns:
Session: DB session from ctx settings.
""" """
if not 'pytest' in sys.modules: if not 'pytest' in sys.modules:
from tools import ctx from tools import ctx
@@ -41,9 +47,12 @@ class BaseClass(Base):
return ctx.database_session return ctx.database_session
@declared_attr @declared_attr
def __directory_path__(cls): def __directory_path__(cls) -> Path:
""" """
Pull submission directory from ctx Pull directory path from ctx to be used in operations.
Returns:
Path: Location of the Submissions directory in Settings object
""" """
if not 'pytest' in sys.modules: if not 'pytest' in sys.modules:
from tools import ctx from tools import ctx
@@ -52,27 +61,31 @@ class BaseClass(Base):
return ctx.directory_path return ctx.directory_path
@declared_attr @declared_attr
def __backup_path__(cls): def __backup_path__(cls) -> Path:
""" """
Pull backup directory from ctx Pull backup directory path from ctx to be used in operations.
Returns:
Path: Location of the Submissions backup directory in Settings object
""" """
if not 'pytest' in sys.modules: if not 'pytest' in sys.modules:
from tools import ctx from tools import ctx
else: else:
from test_settings import ctx from test_settings import ctx
return ctx.backup_path return ctx.backup_path
def query_return(query:Query, limit:int=0): @classmethod
def execute_query(cls, query: Query, limit: int = 0) -> Any | List[Any]:
""" """
Execute sqlalchemy query. Execute sqlalchemy query.
Args: Args:
query (Query): Query object query (Query): input query object
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0. limit (int): Maximum number of results. (0 = all)
Returns: Returns:
_type_: Query result. Any | List[Any]: Single result if limit = 1 or List if other.
""" """
with query.session.no_autoflush: with query.session.no_autoflush:
match limit: match limit:
case 0: case 0:
@@ -81,11 +94,11 @@ class BaseClass(Base):
return query.first() return query.first()
case _: case _:
return query.limit(limit).all() return query.limit(limit).all()
def save(self): def save(self):
""" """
Add the object to the database and commit Add the object to the database and commit
""" """
# logger.debug(f"Saving object: {pformat(self.__dict__)}") # logger.debug(f"Saving object: {pformat(self.__dict__)}")
try: try:
self.__database_session__.add(self) self.__database_session__.add(self)
@@ -94,6 +107,7 @@ class BaseClass(Base):
logger.critical(f"Problem saving object: {e}") logger.critical(f"Problem saving object: {e}")
self.__database_session__.rollback() self.__database_session__.rollback()
from .controls import * from .controls import *
# import order must go: orgs, kit, subs due to circular import issues # import order must go: orgs, kit, subs due to circular import issues
from .organizations import * from .organizations import *

View File

@@ -1,59 +1,59 @@
''' """
All control related models. All control related models.
''' """
from __future__ import annotations from __future__ import annotations
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey
from sqlalchemy.orm import relationship, Query from sqlalchemy.orm import relationship, Query
import logging, re, sys import logging, re
from operator import itemgetter from operator import itemgetter
from . import BaseClass from . import BaseClass
from tools import setup_lookup from tools import setup_lookup
from datetime import date, datetime from datetime import date, datetime
from typing import List from typing import List
from dateutil.parser import parse from dateutil.parser import parse
from re import Pattern
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
class ControlType(BaseClass): class ControlType(BaseClass):
""" """
Base class of a control archetype. Base class of a control archetype.
""" """
id = Column(INTEGER, primary_key=True) #: primary key
id = Column(INTEGER, primary_key=True) #: primary key name = Column(String(255), unique=True) #: controltype name (e.g. MCS)
name = Column(String(255), unique=True) #: controltype name (e.g. MCS) targets = Column(JSON) #: organisms checked for
targets = Column(JSON) #: organisms checked for instances = relationship("Control", back_populates="controltype") #: control samples created of this type.
instances = relationship("Control", back_populates="controltype") #: control samples created of this type.
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<ControlType({self.name})>" return f"<ControlType({self.name})>"
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, def query(cls,
name:str=None, name: str = None,
limit:int=0 limit: int = 0
) -> ControlType|List[ControlType]: ) -> ControlType | List[ControlType]:
""" """
Lookup control archetypes in the database Lookup control archetypes in the database
Args: Args:
name (str, optional): Control type name (limits results to 1). Defaults to None. name (str, optional): Name of the desired controltype. Defaults to None.
limit (int, optional): Maximum number of results to return. Defaults to 0. limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
Returns: Returns:
models.ControlType|List[models.ControlType]: ControlType(s) of interest. ControlType | List[ControlType]: Single result if the limit = 1, else a list.
""" """
query = cls.__database_session__.query(cls) query = cls.__database_session__.query(cls)
match name: match name:
case str(): case str():
query = query.filter(cls.name==name) query = query.filter(cls.name == name)
limit = 1 limit = 1
case _: case _:
pass pass
return cls.query_return(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
def get_subtypes(self, mode:str) -> List[str]: def get_subtypes(self, mode: str) -> List[str]:
""" """
Get subtypes associated with this controltype Get subtypes associated with this controltype
@@ -62,56 +62,68 @@ class ControlType(BaseClass):
Returns: Returns:
List[str]: list of subtypes available List[str]: list of subtypes available
""" """
# Get first instance since all should have same subtypes # Get first instance since all should have same subtypes
# outs = self.instances[0]
# Get mode of instance # Get mode of instance
# jsoner = json.loads(getattr(outs, mode))
jsoner = getattr(self.instances[0], mode) jsoner = getattr(self.instances[0], mode)
logger.debug(f"JSON out: {jsoner.keys()}") # logger.debug(f"JSON out: {jsoner.keys()}")
try: try:
# Pick genera (all should have same subtypes) # Pick genera (all should have same subtypes)
genera = list(jsoner.keys())[0] genera = list(jsoner.keys())[0]
except IndexError: except IndexError:
return [] return []
# 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] subtypes = [item for item in jsoner[genera] if "_hashes" not in item and "_ratio" not in item]
return subtypes return subtypes
@classmethod @classmethod
def get_positive_control_types(cls): def get_positive_control_types(cls) -> List[ControlType]:
"""
Gets list of Control types if they have targets
Returns:
List[ControlType]: Control types that have targets
"""
return [item for item in cls.query() if item.targets != []] return [item for item in cls.query() if item.targets != []]
@classmethod @classmethod
def build_positive_regex(cls): def build_positive_regex(cls) -> Pattern:
"""
Creates a re.Pattern that will look for positive control types
Returns:
Pattern: Constructed pattern
"""
strings = list(set([item.name.split("-")[0] for item in cls.get_positive_control_types()])) strings = list(set([item.name.split("-")[0] for item in cls.get_positive_control_types()]))
return re.compile(rf"(^{'|^'.join(strings)})-.*", flags=re.IGNORECASE) return re.compile(rf"(^{'|^'.join(strings)})-.*", flags=re.IGNORECASE)
class Control(BaseClass): class Control(BaseClass):
""" """
Base class of a control sample. Base class of a control sample.
""" """
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
parent_id = Column(String, ForeignKey("_controltype.id", name="fk_control_parent_id")) #: primary key of control type parent_id = Column(String,
controltype = relationship("ControlType", back_populates="instances", foreign_keys=[parent_id]) #: reference to parent control type ForeignKey("_controltype.id", name="fk_control_parent_id")) #: primary key of control type
name = Column(String(255), unique=True) #: Sample ID controltype = relationship("ControlType", back_populates="instances",
submitted_date = Column(TIMESTAMP) #: Date submitted to Robotics foreign_keys=[parent_id]) #: reference to parent control type
contains = Column(JSON) #: unstructured hashes in contains.tsv for each organism name = Column(String(255), unique=True) #: Sample ID
matches = Column(JSON) #: unstructured hashes in matches.tsv for each organism submitted_date = Column(TIMESTAMP) #: Date submitted to Robotics
kraken = Column(JSON) #: unstructured output from kraken_report contains = Column(JSON) #: unstructured hashes in contains.tsv for each organism
submission_id = Column(INTEGER, ForeignKey("_basicsubmission.id")) #: parent submission id matches = Column(JSON) #: unstructured hashes in matches.tsv for each organism
submission = relationship("BacterialCulture", back_populates="controls", foreign_keys=[submission_id]) #: parent submission kraken = Column(JSON) #: unstructured output from kraken_report
refseq_version = Column(String(16)) #: version of refseq used in fastq parsing submission_id = Column(INTEGER, ForeignKey("_basicsubmission.id")) #: parent submission id
kraken2_version = Column(String(16)) #: version of kraken2 used in fastq parsing submission = relationship("BacterialCulture", back_populates="controls",
kraken2_db_version = Column(String(32)) #: folder name of kraken2 db foreign_keys=[submission_id]) #: parent submission
sample = relationship("BacterialCultureSample", back_populates="control") #: This control's submission sample refseq_version = Column(String(16)) #: version of refseq used in fastq parsing
sample_id = Column(INTEGER, ForeignKey("_basicsample.id", ondelete="SET NULL", name="cont_BCS_id")) #: sample id key kraken2_version = Column(String(16)) #: version of kraken2 used in fastq parsing
kraken2_db_version = Column(String(32)) #: folder name of kraken2 db
sample = relationship("BacterialCultureSample", back_populates="control") #: This control's submission sample
sample_id = Column(INTEGER,
ForeignKey("_basicsample.id", ondelete="SET NULL", name="cont_BCS_id")) #: sample id key
def __repr__(self) -> str: def __repr__(self) -> str:
"""
Returns:
str: Representation of self
"""
return f"<Control({self.name})>" return f"<Control({self.name})>"
def to_sub_dict(self) -> dict: def to_sub_dict(self) -> dict:
@@ -120,7 +132,7 @@ class Control(BaseClass):
Returns: Returns:
dict: output dictionary containing: Name, Type, Targets, Top Kraken results dict: output dictionary containing: Name, Type, Targets, Top Kraken results
""" """
# logger.debug("loading json string into dict") # logger.debug("loading json string into dict")
try: try:
# kraken = json.loads(self.kraken) # kraken = json.loads(self.kraken)
@@ -133,7 +145,8 @@ class Control(BaseClass):
for item in kraken: for item in kraken:
# logger.debug("calculating kraken percent (overwrites what's already been scraped)") # logger.debug("calculating kraken percent (overwrites what's already been scraped)")
kraken_percent = kraken[item]['kraken_count'] / kraken_cnt_total kraken_percent = kraken[item]['kraken_count'] / kraken_cnt_total
new_kraken.append({'name': item, 'kraken_count':kraken[item]['kraken_count'], 'kraken_percent':"{0:.0%}".format(kraken_percent)}) new_kraken.append({'name': item, 'kraken_count': kraken[item]['kraken_count'],
'kraken_percent': "{0:.0%}".format(kraken_percent)})
new_kraken = sorted(new_kraken, key=itemgetter('kraken_count'), reverse=True) new_kraken = sorted(new_kraken, key=itemgetter('kraken_count'), reverse=True)
# logger.debug("setting targets") # logger.debug("setting targets")
if self.controltype.targets == []: if self.controltype.targets == []:
@@ -142,14 +155,14 @@ class Control(BaseClass):
targets = self.controltype.targets targets = self.controltype.targets
# logger.debug("constructing output dictionary") # logger.debug("constructing output dictionary")
output = { output = {
"name" : self.name, "name": self.name,
"type" : self.controltype.name, "type": self.controltype.name,
"targets" : ", ".join(targets), "targets": ", ".join(targets),
"kraken" : new_kraken[0:5] "kraken": new_kraken[0:5]
} }
return output return output
def convert_by_mode(self, mode:str) -> list[dict]: def convert_by_mode(self, mode: str) -> list[dict]:
""" """
split this instance into analysis types for controls graphs split this instance into analysis types for controls graphs
@@ -158,7 +171,7 @@ class Control(BaseClass):
Returns: Returns:
list[dict]: list of records list[dict]: list of records
""" """
output = [] output = []
# logger.debug("load json string for mode (i.e. contains, matches, kraken2)") # logger.debug("load json string for mode (i.e. contains, matches, kraken2)")
try: try:
@@ -191,7 +204,7 @@ class Control(BaseClass):
Returns: Returns:
List[str]: List of control mode names. List[str]: List of control mode names.
""" """
try: try:
# logger.debug("Creating a list of JSON columns in _controls table") # logger.debug("Creating a list of JSON columns in _controls table")
cols = [item.name for item in list(cls.__table__.columns) if isinstance(item.type, JSON)] cols = [item.name for item in list(cls.__table__.columns) if isinstance(item.type, JSON)]
@@ -202,13 +215,13 @@ class Control(BaseClass):
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, def query(cls,
control_type:ControlType|str|None=None, control_type: ControlType | str | None = None,
start_date:date|str|int|None=None, start_date: date | str | int | None = None,
end_date:date|str|int|None=None, end_date: date | str | int | None = None,
control_name:str|None=None, control_name: str | None = None,
limit:int=0 limit: int = 0
) -> Control|List[Control]: ) -> Control | List[Control]:
""" """
Lookup control objects in the database based on a number of parameters. Lookup control objects in the database based on a number of parameters.
@@ -221,16 +234,16 @@ class Control(BaseClass):
Returns: Returns:
models.Control|List[models.Control]: Control object of interest. models.Control|List[models.Control]: Control object of interest.
""" """
query: Query = cls.__database_session__.query(cls) query: Query = cls.__database_session__.query(cls)
# by control type # by control type
match control_type: match control_type:
case ControlType(): case ControlType():
# logger.debug(f"Looking up control by control type: {control_type}") # logger.debug(f"Looking up control by control type: {control_type}")
query = query.filter(cls.controltype==control_type) query = query.filter(cls.controltype == control_type)
case str(): case str():
# logger.debug(f"Looking up control by control type: {control_type}") # logger.debug(f"Looking up control by control type: {control_type}")
query = query.join(ControlType).filter(ControlType.name==control_type) query = query.join(ControlType).filter(ControlType.name == control_type)
case _: case _:
pass pass
# by date range # by date range
@@ -247,7 +260,8 @@ class Control(BaseClass):
start_date = start_date.strftime("%Y-%m-%d") start_date = start_date.strftime("%Y-%m-%d")
case int(): case int():
# logger.debug(f"Lookup control by ordinal start date {start_date}") # logger.debug(f"Lookup control by ordinal start date {start_date}")
start_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d") start_date = datetime.fromordinal(
datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d")
case _: case _:
# logger.debug(f"Lookup control with parsed start date {start_date}") # logger.debug(f"Lookup control with parsed start date {start_date}")
start_date = parse(start_date).strftime("%Y-%m-%d") start_date = parse(start_date).strftime("%Y-%m-%d")
@@ -257,7 +271,8 @@ class Control(BaseClass):
end_date = end_date.strftime("%Y-%m-%d") end_date = end_date.strftime("%Y-%m-%d")
case int(): case int():
# logger.debug(f"Lookup control by ordinal end date {end_date}") # logger.debug(f"Lookup control by ordinal end date {end_date}")
end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime("%Y-%m-%d") end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime(
"%Y-%m-%d")
case _: case _:
# logger.debug(f"Lookup control with parsed end date {end_date}") # logger.debug(f"Lookup control with parsed end date {end_date}")
end_date = parse(end_date).strftime("%Y-%m-%d") end_date = parse(end_date).strftime("%Y-%m-%d")
@@ -270,5 +285,4 @@ class Control(BaseClass):
limit = 1 limit = 1
case _: case _:
pass pass
return cls.query_return(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)

View File

@@ -214,7 +214,7 @@ class KitType(BaseClass):
limit = 1 limit = 1
case _: case _:
pass pass
return cls.query_return(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
@check_authorization @check_authorization
def save(self): def save(self):
@@ -303,7 +303,7 @@ class ReagentType(BaseClass):
limit = 1 limit = 1
case _: case _:
pass pass
return cls.query_return(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
def to_pydantic(self) -> "PydReagent": def to_pydantic(self) -> "PydReagent":
""" """
@@ -464,7 +464,7 @@ class Reagent(BaseClass):
limit = 1 limit = 1
case _: case _:
pass pass
return cls.query_return(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
class Discount(BaseClass): class Discount(BaseClass):
""" """
@@ -533,7 +533,7 @@ class Discount(BaseClass):
case _: case _:
# raise ValueError(f"Invalid value for kit type: {kit_type}") # raise ValueError(f"Invalid value for kit type: {kit_type}")
pass pass
return cls.query_return(query=query) return cls.execute_query(query=query)
@check_authorization @check_authorization
def save(self): def save(self):
@@ -702,7 +702,7 @@ class SubmissionType(BaseClass):
query = query.filter(cls.info_map.op('->')(key)!=None) query = query.filter(cls.info_map.op('->')(key)!=None)
case _: case _:
pass pass
return cls.query_return(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
@check_authorization @check_authorization
def save(self): def save(self):
@@ -781,7 +781,7 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
# logger.debug(f"Looking up {cls.__name__} by id {kit_type}") # logger.debug(f"Looking up {cls.__name__} by id {kit_type}")
query = query.join(KitType).filter(KitType.id==kit_type) query = query.join(KitType).filter(KitType.id==kit_type)
limit = query.count() limit = query.count()
return cls.query_return(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
class KitTypeReagentTypeAssociation(BaseClass): class KitTypeReagentTypeAssociation(BaseClass):
""" """
@@ -889,7 +889,7 @@ class KitTypeReagentTypeAssociation(BaseClass):
pass pass
if kit_type != None and reagent_type != None: if kit_type != None and reagent_type != None:
limit = 1 limit = 1
return cls.query_return(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
class SubmissionReagentAssociation(BaseClass): class SubmissionReagentAssociation(BaseClass):
""" """
@@ -956,7 +956,7 @@ class SubmissionReagentAssociation(BaseClass):
query = query.join(BasicSubmission).filter(BasicSubmission.id==submission) query = query.join(BasicSubmission).filter(BasicSubmission.id==submission)
case _: case _:
pass pass
return cls.query_return(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
def to_sub_dict(self, extraction_kit) -> dict: def to_sub_dict(self, extraction_kit) -> dict:
""" """
@@ -1083,7 +1083,7 @@ class Equipment(BaseClass):
limit = 1 limit = 1
case _: case _:
pass pass
return cls.query_return(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
def to_pydantic(self, submission_type:SubmissionType, extraction_kit:str|KitType|None=None) -> "PydEquipment": def to_pydantic(self, submission_type:SubmissionType, extraction_kit:str|KitType|None=None) -> "PydEquipment":
""" """
@@ -1206,7 +1206,7 @@ class EquipmentRole(BaseClass):
limit = 1 limit = 1
case _: case _:
pass pass
return cls.query_return(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
def get_processes(self, submission_type:str|SubmissionType|None, extraction_kit:str|KitType|None=None) -> List[Process]: def get_processes(self, submission_type:str|SubmissionType|None, extraction_kit:str|KitType|None=None) -> List[Process]:
""" """
@@ -1382,5 +1382,5 @@ class Process(BaseClass):
limit = 1 limit = 1
case _: case _:
pass pass
return cls.query_return(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)

View File

@@ -33,10 +33,6 @@ class Organization(BaseClass):
contacts = relationship("Contact", back_populates="organization", secondary=orgs_contacts) #: contacts involved with this org contacts = relationship("Contact", back_populates="organization", secondary=orgs_contacts) #: contacts involved with this org
def __repr__(self) -> str: def __repr__(self) -> str:
"""
Returns:
str: Representation of this Organization
"""
return f"<Organization({self.name})>" return f"<Organization({self.name})>"
@classmethod @classmethod
@@ -70,7 +66,7 @@ class Organization(BaseClass):
limit = 1 limit = 1
case _: case _:
pass pass
return cls.query_return(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
@check_authorization @check_authorization
def save(self): def save(self):
@@ -137,5 +133,5 @@ class Contact(BaseClass):
limit = 1 limit = 1
case _: case _:
pass pass
return cls.query_return(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)

View File

@@ -47,7 +47,6 @@ class BasicSubmission(BaseClass):
submission_type_name = Column(String, ForeignKey("_submissiontype.name", ondelete="SET NULL", name="fk_BS_subtype_name")) #: name of joined submission type submission_type_name = Column(String, ForeignKey("_submissiontype.name", ondelete="SET NULL", name="fk_BS_subtype_name")) #: name of joined submission type
technician = Column(String(64)) #: initials of processing tech(s) technician = Column(String(64)) #: initials of processing tech(s)
# Move this into custom types? # Move this into custom types?
# reagents = relationship("Reagent", back_populates="submissions", secondary=reagents_submissions) #: relationship to reagents
reagents_id = Column(String, ForeignKey("_reagent.id", ondelete="SET NULL", name="fk_BS_reagents_id")) #: id of used reagents reagents_id = Column(String, ForeignKey("_reagent.id", ondelete="SET NULL", name="fk_BS_reagents_id")) #: id of used reagents
extraction_info = Column(JSON) #: unstructured output from the extraction table logger. extraction_info = Column(JSON) #: unstructured output from the extraction table logger.
run_cost = Column(FLOAT(2)) #: total cost of running the plate. Set from constant and mutable kit costs at time of creation. run_cost = Column(FLOAT(2)) #: total cost of running the plate. Set from constant and mutable kit costs at time of creation.
@@ -127,13 +126,13 @@ class BasicSubmission(BaseClass):
output = {} output = {}
for k,v in dicto.items(): for k,v in dicto.items():
if len(args) > 0 and k not in args: if len(args) > 0 and k not in args:
logger.debug(f"Don't want {k}") # logger.debug(f"Don't want {k}")
continue continue
else: else:
output[k] = v output[k] = v
for k,v in st.defaults.items(): for k,v in st.defaults.items():
if len(args) > 0 and k not in args: if len(args) > 0 and k not in args:
logger.debug(f"Don't want {k}") # logger.debug(f"Don't want {k}")
continue continue
else: else:
match v: match v:
@@ -410,7 +409,7 @@ class BasicSubmission(BaseClass):
case item if item in self.jsons(): case item if item in self.jsons():
logger.debug(f"Setting JSON attribute.") logger.debug(f"Setting JSON attribute.")
existing = self.__getattribute__(key) existing = self.__getattribute__(key)
if value == "" or value is None or value == 'null': if value is None or value in ['', 'null']:
logger.error(f"No value given, not setting.") logger.error(f"No value given, not setting.")
return return
if existing is None: if existing is None:
@@ -422,7 +421,8 @@ class BasicSubmission(BaseClass):
if isinstance(value, list): if isinstance(value, list):
existing += value existing += value
else: else:
existing.append(value) if value is not None:
existing.append(value)
self.__setattr__(key, existing) self.__setattr__(key, existing)
flag_modified(self, key) flag_modified(self, key)
return return
@@ -890,7 +890,7 @@ class BasicSubmission(BaseClass):
# limit = 1 # limit = 1
if chronologic: if chronologic:
query.order_by(cls.submitted_date) query.order_by(cls.submitted_date)
return cls.query_return(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
@classmethod @classmethod
def query_or_create(cls, submission_type:str|SubmissionType|None=None, **kwargs) -> BasicSubmission: def query_or_create(cls, submission_type:str|SubmissionType|None=None, **kwargs) -> BasicSubmission:
@@ -1421,7 +1421,7 @@ class WastewaterArtic(BasicSubmission):
return input_dict return input_dict
@classmethod @classmethod
def enforce_name(cls, instr:str, data:dict|None={}) -> str: def enforce_name(cls, instr:str, data:dict={}) -> str:
""" """
Extends parent Extends parent
""" """
@@ -1430,16 +1430,12 @@ class WastewaterArtic(BasicSubmission):
instr = re.sub(r"Artic", "", instr, flags=re.IGNORECASE) instr = re.sub(r"Artic", "", instr, flags=re.IGNORECASE)
except (AttributeError, TypeError) as e: except (AttributeError, TypeError) as e:
logger.error(f"Problem using regex: {e}") logger.error(f"Problem using regex: {e}")
# try: # logger.debug(f"Before RSL addition: {instr}")
# check = instr.startswith("RSL") instr = instr.replace("-", "")
# except AttributeError: instr = re.sub(r"^(\d{6})", f"RSL-AR-\\1", instr)
# check = False # logger.debug(f"name coming out of Artic namer: {instr}")
# if not check:
# try:
# instr = "RSL" + instr
# except TypeError:
# instr = "RSL"
outstr = super().enforce_name(instr=instr, data=data) outstr = super().enforce_name(instr=instr, data=data)
return outstr return outstr
@classmethod @classmethod
@@ -1922,7 +1918,7 @@ class BasicSample(BaseClass):
query = query.filter(attr==v) query = query.filter(attr==v)
if len(kwargs) > 0: if len(kwargs) > 0:
limit = 1 limit = 1
return cls.query_return(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
@classmethod @classmethod
def query_or_create(cls, sample_type:str|None=None, **kwargs) -> BasicSample: def query_or_create(cls, sample_type:str|None=None, **kwargs) -> BasicSample:
@@ -2259,7 +2255,7 @@ class SubmissionSampleAssociation(BaseClass):
query = query.order_by(BasicSubmission.submitted_date.desc()) query = query.order_by(BasicSubmission.submitted_date.desc())
else: else:
query = query.order_by(BasicSubmission.submitted_date) query = query.order_by(BasicSubmission.submitted_date)
return cls.query_return(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
@classmethod @classmethod
def query_or_create(cls, def query_or_create(cls,

View File

@@ -482,12 +482,16 @@ def setup_lookup(func):
func (_type_): _description_ func (_type_): _description_
""" """
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
for k, v in locals().items(): sanitized_kwargs = {}
if k == "kwargs": for k, v in locals()['kwargs'].items():
continue
if isinstance(v, dict): if isinstance(v, dict):
raise ValueError("Cannot use dictionary in query. Make sure you parse it first.") try:
return func(*args, **kwargs) sanitized_kwargs[k] = v['value']
except KeyError:
raise ValueError("Could not sanitize dictionary in query. Make sure you parse it first.")
elif v is not None:
sanitized_kwargs[k] = v
return func(*args, **sanitized_kwargs)
return wrapper return wrapper
class Result(BaseModel): class Result(BaseModel):