Pre-major refactor

This commit is contained in:
Landon Wark
2023-11-03 10:49:37 -05:00
parent 22a23b7838
commit 5570d87b7c
19 changed files with 1078 additions and 1045 deletions

View File

@@ -1,4 +1,5 @@
'''
All database related operations.
'''
# from .functions import *
from .models import *
from .functions import *

View File

@@ -1,25 +1,36 @@
'''
Contains convenience functions for using database
'''
import sys
'''Contains or imports all database convenience functions'''
from tools import Settings
from .lookups import *
from sqlalchemy import event
from sqlalchemy.engine import Engine
from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
import logging
import pandas as pd
import json
from pathlib import Path
import yaml
from .. import models
from . import store_object
from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
from pprint import pformat
from .models import *
# from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError
# from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
import logging
from backend.validators import pydant
from backend.validators.pydant import *
logger = logging.getLogger(f"Submissions_{__name__}")
logger = logging.getLogger(f"submissions.{__name__}")
@event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
"""
*should* allow automatic creation of foreign keys in the database
I have no idea how it actually works.
def submissions_to_df(ctx:Settings, submission_type:str|None=None, limit:int=0) -> pd.DataFrame:
Args:
dbapi_connection (_type_): _description_
connection_record (_type_): _description_
"""
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
def submissions_to_df(submission_type:str|None=None, limit:int=0) -> pd.DataFrame:
"""
Convert submissions looked up by type to dataframe
@@ -34,7 +45,8 @@ def submissions_to_df(ctx:Settings, submission_type:str|None=None, limit:int=0)
logger.debug(f"Querying Type: {submission_type}")
logger.debug(f"Using limit: {limit}")
# use lookup function to create list of dicts
subs = [item.to_dict() for item in lookup_submissions(ctx=ctx, submission_type=submission_type, limit=limit)]
# subs = [item.to_dict() for item in lookup_submissions(ctx=ctx, submission_type=submission_type, limit=limit)]
subs = [item.to_dict() for item in BasicSubmission.query(submission_type=submission_type, limit=limit)]
logger.debug(f"Got {len(subs)} results.")
# make df from dicts (records) in list
df = pd.DataFrame.from_records(subs)
@@ -66,7 +78,7 @@ def submissions_to_df(ctx:Settings, submission_type:str|None=None, limit:int=0)
pass
return df
def get_control_subtypes(ctx:Settings, type:str, mode:str) -> list[str]:
def get_control_subtypes(type:str, mode:str) -> list[str]:
"""
Get subtypes for a control analysis mode
@@ -80,7 +92,8 @@ def get_control_subtypes(ctx:Settings, type:str, mode:str) -> list[str]:
"""
# Only the first control of type is necessary since they all share subtypes
try:
outs = lookup_controls(ctx=ctx, control_type=type, limit=1)
# outs = lookup_controls(ctx=ctx, control_type=type, limit=1)
outs = Control.query(control_type=type, limit=1)
except (TypeError, IndexError):
return []
# Get analysis mode data as dict
@@ -93,7 +106,7 @@ def get_control_subtypes(ctx:Settings, type:str, mode:str) -> list[str]:
subtypes = [item for item in jsoner[genera] if "_hashes" not in item and "_ratio" not in item]
return subtypes
def update_last_used(ctx:Settings, reagent:models.Reagent, kit:models.KitType):
def update_last_used(reagent:Reagent, kit:KitType):
"""
Updates the 'last_used' field in kittypes/reagenttypes
@@ -104,78 +117,73 @@ def update_last_used(ctx:Settings, reagent:models.Reagent, kit:models.KitType):
"""
# rt = list(set(reagent.type).intersection(kit.reagent_types))[0]
logger.debug(f"Attempting update of reagent type at intersection of ({reagent}), ({kit})")
rt = lookup_reagent_types(ctx=ctx, kit_type=kit, reagent=reagent)
# rt = lookup_reagent_types(ctx=ctx, kit_type=kit, reagent=reagent)
rt = ReagentType.query(kit_type=kit, reagent=reagent)
if rt != None:
assoc = lookup_reagenttype_kittype_association(ctx=ctx, kit_type=kit, reagent_type=rt)
# assoc = lookup_reagenttype_kittype_association(ctx=ctx, kit_type=kit, reagent_type=rt)
assoc = KitTypeReagentTypeAssociation.query(kit_type=kit, reagent_type=rt)
if assoc != None:
if assoc.last_used != reagent.lot:
logger.debug(f"Updating {assoc} last used to {reagent.lot}")
assoc.last_used = reagent.lot
# ctx.database_session.merge(assoc)
# ctx.database_session.commit()
result = store_object(ctx=ctx, object=assoc)
# result = store_object(ctx=ctx, object=assoc)
result = assoc.save()
return result
return dict(message=f"Updating last used {rt} was not performed.")
def delete_submission(ctx:Settings, id:int) -> dict|None:
"""
Deletes a submission and its associated samples from the database.
# def delete_submission(id:int) -> dict|None:
# """
# Deletes a submission and its associated samples from the database.
Args:
ctx (Settings): settings object passed down from gui
id (int): id of submission to be deleted.
"""
# In order to properly do this Im' going to have to delete all of the secondary table stuff as well.
# Retrieve submission
sub = lookup_submissions(ctx=ctx, id=id)
# Convert to dict for storing backup as a yml
backup = sub.to_dict()
try:
with open(Path(ctx.backup_path).joinpath(f"{sub.rsl_plate_num}-backup({date.today().strftime('%Y%m%d')}).yml"), "w") as f:
yaml.dump(backup, f)
except KeyError:
pass
ctx.database_session.delete(sub)
try:
ctx.database_session.commit()
except (SQLIntegrityError, SQLOperationalError, AlcIntegrityError, AlcOperationalError) as e:
ctx.database_session.rollback()
raise e
return None
# Args:
# ctx (Settings): settings object passed down from gui
# id (int): id of submission to be deleted.
# """
# # In order to properly do this Im' going to have to delete all of the secondary table stuff as well.
# # Retrieve submission
# # sub = lookup_submissions(ctx=ctx, id=id)
# sub = models.BasicSubmission.query(id=id)
# # Convert to dict for storing backup as a yml
# sub.delete()
# return None
def update_ww_sample(ctx:Settings, sample_obj:dict) -> dict|None:
"""
Retrieves wastewater sample by rsl number (sample_obj['sample']) and updates values from constructed dictionary
# def update_ww_sample(sample_obj:dict) -> dict|None:
# """
# Retrieves wastewater sample by rsl number (sample_obj['sample']) and updates values from constructed dictionary
Args:
ctx (Settings): settings object passed down from gui
sample_obj (dict): dictionary representing new values for database object
"""
logger.debug(f"dictionary to use for update: {pformat(sample_obj)}")
logger.debug(f"Looking up {sample_obj['sample']} in plate {sample_obj['plate_rsl']}")
assoc = lookup_submission_sample_association(ctx=ctx, submission=sample_obj['plate_rsl'], sample=sample_obj['sample'])
if assoc != None:
for key, value in sample_obj.items():
# set attribute 'key' to 'value'
try:
check = getattr(assoc, key)
except AttributeError as e:
logger.error(f"Item doesn't have field {key} due to {e}")
continue
if check != value:
logger.debug(f"Setting association key: {key} to {value}")
try:
setattr(assoc, key, value)
except AttributeError as e:
logger.error(f"Can't set field {key} to {value} due to {e}")
continue
else:
logger.error(f"Unable to find sample {sample_obj['sample']}")
return
result = store_object(ctx=ctx, object=assoc)
return result
# Args:
# ctx (Settings): settings object passed down from gui
# sample_obj (dict): dictionary representing new values for database object
# """
# logger.debug(f"dictionary to use for update: {pformat(sample_obj)}")
# logger.debug(f"Looking up {sample_obj['sample']} in plate {sample_obj['plate_rsl']}")
# # assoc = lookup_submission_sample_association(ctx=ctx, submission=sample_obj['plate_rsl'], sample=sample_obj['sample'])
# assoc = models.SubmissionSampleAssociation.query(submission=sample_obj['plate_rsl'], sample=sample_obj['sample'])
# if assoc != None:
# for key, value in sample_obj.items():
# # set attribute 'key' to 'value'
# try:
# check = getattr(assoc, key)
# except AttributeError as e:
# logger.error(f"Item doesn't have field {key} due to {e}")
# continue
# if check != value:
# logger.debug(f"Setting association key: {key} to {value}")
# try:
# setattr(assoc, key, value)
# except AttributeError as e:
# logger.error(f"Can't set field {key} to {value} due to {e}")
# continue
# else:
# logger.error(f"Unable to find sample {sample_obj['sample']}")
# return
# # result = store_object(ctx=ctx, object=assoc)
# result = assoc.save()
# return result
def check_kit_integrity(ctx:Settings, sub:models.BasicSubmission|models.KitType|pydant.PydSubmission, reagenttypes:list=[]) -> dict|None:
def check_kit_integrity(sub:BasicSubmission|KitType|PydSubmission, reagenttypes:list=[]) -> dict|None:
"""
Ensures all reagents expected in kit are listed in Submission
@@ -190,11 +198,12 @@ def check_kit_integrity(ctx:Settings, sub:models.BasicSubmission|models.KitType|
# What type is sub?
# reagenttypes = []
match sub:
case pydant.PydSubmission():
ext_kit = lookup_kit_types(ctx=ctx, name=sub.extraction_kit['value'])
case PydSubmission():
# ext_kit = lookup_kit_types(ctx=ctx, name=sub.extraction_kit['value'])
ext_kit = KitType.query(name=sub.extraction_kit['value'])
ext_kit_rtypes = [item.name for item in ext_kit.get_reagents(required=True, submission_type=sub.submission_type['value'])]
reagenttypes = [item.type for item in sub.reagents]
case models.BasicSubmission():
case BasicSubmission():
# Get all required reagent types for this kit.
ext_kit_rtypes = [item.name for item in sub.extraction_kit.get_reagents(required=True, submission_type=sub.submission_type_name)]
# Overwrite function parameter reagenttypes
@@ -202,9 +211,10 @@ def check_kit_integrity(ctx:Settings, sub:models.BasicSubmission|models.KitType|
logger.debug(f"For kit integrity, looking up reagent: {reagent}")
try:
# rt = list(set(reagent.type).intersection(sub.extraction_kit.reagent_types))[0].name
rt = lookup_reagent_types(ctx=ctx, kit_type=sub.extraction_kit, reagent=reagent)
# rt = lookup_reagent_types(ctx=ctx, kit_type=sub.extraction_kit, reagent=reagent)
rt = ReagentType.query(kit_type=sub.extraction_kit, reagent=reagent)
logger.debug(f"Got reagent type: {rt}")
if isinstance(rt, models.ReagentType):
if isinstance(rt, ReagentType):
reagenttypes.append(rt.name)
except AttributeError as e:
logger.error(f"Problem parsing reagents: {[f'{reagent.lot}, {reagent.type}' for reagent in sub.reagents]}")
@@ -212,7 +222,7 @@ def check_kit_integrity(ctx:Settings, sub:models.BasicSubmission|models.KitType|
except IndexError:
logger.error(f"No intersection of {reagent} type {reagent.type} and {sub.extraction_kit.reagent_types}")
raise ValueError(f"No intersection of {reagent} type {reagent.type} and {sub.extraction_kit.reagent_types}")
case models.KitType():
case KitType():
ext_kit_rtypes = [item.name for item in sub.get_reagents(required=True)]
case _:
raise ValueError(f"There was no match for the integrity object.\n\nCheck to make sure they are imported from the same place because it matters.")
@@ -231,7 +241,7 @@ def check_kit_integrity(ctx:Settings, sub:models.BasicSubmission|models.KitType|
result = {'message' : f"The submission you are importing is missing some reagents expected by the kit.\n\nIt looks like you are missing: {[item.upper() for item in missing]}\n\nAlternatively, you may have set the wrong extraction kit.\n\nThe program will populate lists using existing reagents.\n\nPlease make sure you check the lots carefully!", 'missing': missing}
return result
def update_subsampassoc_with_pcr(ctx:Settings, submission:models.BasicSubmission, sample:models.BasicSample, input_dict:dict) -> dict|None:
def update_subsampassoc_with_pcr(submission:BasicSubmission, sample:BasicSample, input_dict:dict) -> dict|None:
"""
Inserts PCR results into wastewater submission/sample association
@@ -244,35 +254,39 @@ def update_subsampassoc_with_pcr(ctx:Settings, submission:models.BasicSubmission
Returns:
dict|None: result object
"""
assoc = lookup_submission_sample_association(ctx, submission=submission, sample=sample)
# assoc = lookup_submission_sample_association(ctx, submission=submission, sample=sample)
assoc = SubmissionSampleAssociation.query(submission=submission, sample=sample)
for k,v in input_dict.items():
try:
setattr(assoc, k, v)
except AttributeError:
logger.error(f"Can't set {k} to {v}")
result = store_object(ctx=ctx, object=assoc)
# result = store_object(ctx=ctx, object=assoc)
result = assoc.save()
return result
# def get_polymorphic_subclass(base:object|models.BasicSubmission=models.BasicSubmission, polymorphic_identity:str|None=None):
# def store_object(ctx:Settings, object) -> dict|None:
# """
# Retrieves any subclasses of given base class whose polymorphic identity matches the string input.
# NOTE: Depreciated in favour of class based finders in 'submissions.py'
# Store an object in the database
# Args:
# base (object): Base (parent) class
# polymorphic_identity (str | None): Name of subclass of interest. (Defaults to None)
# ctx (Settings): Settings object passed down from gui
# object (_type_): Object to be stored
# Returns:
# _type_: Subclass, or parent class on
# dict|None: Result of action
# """
# if isinstance(polymorphic_identity, dict):
# polymorphic_identity = polymorphic_identity['value']
# if polymorphic_identity == None:
# return base
# else:
# try:
# return [item for item in base.__subclasses__() if item.__mapper_args__['polymorphic_identity']==polymorphic_identity][0]
# except Exception as e:
# logger.error(f"Could not get polymorph {polymorphic_identity} of {base} due to {e}")
# return base
# dbs = ctx.database_session
# dbs.merge(object)
# try:
# dbs.commit()
# except (SQLIntegrityError, AlcIntegrityError) as e:
# logger.debug(f"Hit an integrity error : {e}")
# dbs.rollback()
# return {"message":f"This object {object} already exists, so we can't add it.\n{e}", "status":"Critical"}
# except (SQLOperationalError, AlcOperationalError):
# logger.error(f"Hit an operational error: {e}")
# dbs.rollback()
# return {"message":"The database is locked for editing."}
# return None

View File

@@ -1,91 +0,0 @@
'''Contains or imports all database convenience functions'''
from tools import Settings, package_dir
from sqlalchemy.orm import Session
from sqlalchemy import create_engine, event
from sqlalchemy.engine import Engine
from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
from pathlib import Path
import logging
logger = logging.getLogger(f"Submissions_{__name__}")
@event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
"""
*should* allow automatic creation of foreign keys in the database
I have no idea how it actually works.
Args:
dbapi_connection (_type_): _description_
connection_record (_type_): _description_
"""
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
def create_database_session(ctx:Settings) -> Session:
"""
Create database session for app.
Args:
ctx (Settings): settings passed down from gui
Raises:
FileNotFoundError: Raised if sqlite file not found
Returns:
Session: Sqlalchemy session object.
"""
database_path = ctx.database_path
if database_path == None:
# check in user's .submissions directory for submissions.db
if Path.home().joinpath(".submissions", "submissions.db").exists():
database_path = Path.home().joinpath(".submissions", "submissions.db")
# finally, look in the local dir
else:
database_path = package_dir.joinpath("submissions.db")
else:
if database_path == ":memory:":
pass
# check if user defined path is directory
elif database_path.is_dir():
database_path = database_path.joinpath("submissions.db")
# check if user defined path is a file
elif database_path.is_file():
database_path = database_path
else:
raise FileNotFoundError("No database file found. Exiting program.")
logger.debug(f"Using {database_path} for database file.")
engine = create_engine(f"sqlite:///{database_path}", echo=True, future=True)
session = Session(engine)
return session
def store_object(ctx:Settings, object) -> dict|None:
"""
Store an object in the database
Args:
ctx (Settings): Settings object passed down from gui
object (_type_): Object to be stored
Returns:
dict|None: Result of action
"""
dbs = ctx.database_session
dbs.merge(object)
try:
dbs.commit()
except (SQLIntegrityError, AlcIntegrityError) as e:
logger.debug(f"Hit an integrity error : {e}")
dbs.rollback()
return {"message":f"This object {object} already exists, so we can't add it.\n{e}", "status":"Critical"}
except (SQLOperationalError, AlcOperationalError):
logger.error(f"Hit an operational error: {e}")
dbs.rollback()
return {"message":"The database is locked for editing."}
return None
from .lookups import *
# from .constructions import *
from .misc import *

View File

@@ -1,515 +0,0 @@
from .. import models
from tools import Settings
# from backend.namer import RSLNamer
from typing import List
import logging
from datetime import date, datetime
from dateutil.parser import parse
from sqlalchemy.orm.query import Query
from sqlalchemy import and_, JSON
from sqlalchemy.orm import Session
logger = logging.getLogger(f"submissions.{__name__}")
def query_return(query:Query, limit:int=0):
with query.session.no_autoflush:
match limit:
case 0:
return query.all()
case 1:
return query.first()
case _:
return query.limit(limit).all()
def setup_lookup(ctx:Settings, locals:dict) -> Session:
for k, v in locals.items():
if k == "kwargs":
continue
if isinstance(v, dict):
raise ValueError("Cannot use dictionary in query. Make sure you parse it first.")
# return create_database_session(ctx=ctx)
return ctx.database_session
################## Basic Lookups ####################################
def lookup_reagents(ctx:Settings,
reagent_type:str|models.ReagentType|None=None,
lot_number:str|None=None,
limit:int=0
) -> models.Reagent|List[models.Reagent]:
"""
Lookup a list of reagents from the database.
Args:
ctx (Settings): Settings object passed down from gui
reagent_type (str | models.ReagentType | None, optional): Reagent type. Defaults to None.
lot_number (str | None, optional): Reagent lot number. Defaults to None.
limit (int, optional): limit of results returned. Defaults to 0.
Returns:
models.Reagent | List[models.Reagent]: reagent or list of reagents matching filter.
"""
query = setup_lookup(ctx=ctx, locals=locals()).query(models.Reagent)
match reagent_type:
case str():
logger.debug(f"Looking up reagents by reagent type: {reagent_type}")
query = query.join(models.Reagent.type, aliased=True).filter(models.ReagentType.name==reagent_type)
case models.ReagentType():
logger.debug(f"Looking up reagents by reagent type: {reagent_type}")
query = query.filter(models.Reagent.type.contains(reagent_type))
case _:
pass
match lot_number:
case str():
logger.debug(f"Looking up reagent by lot number: {lot_number}")
query = query.filter(models.Reagent.lot==lot_number)
# In this case limit number returned.
limit = 1
case _:
pass
return query_return(query=query, limit=limit)
def lookup_kit_types(ctx:Settings,
name:str=None,
used_for:str|None=None,
id:int|None=None,
limit:int=0
) -> models.KitType|List[models.KitType]:
query = setup_lookup(ctx=ctx, locals=locals()).query(models.KitType)
match used_for:
case str():
logger.debug(f"Looking up kit type by use: {used_for}")
query = query.filter(models.KitType.used_for.any(name=used_for))
case _:
pass
match name:
case str():
logger.debug(f"Looking up kit type by name: {name}")
query = query.filter(models.KitType.name==name)
limit = 1
case _:
pass
match id:
case int():
logger.debug(f"Looking up kit type by id: {id}")
query = query.filter(models.KitType.id==id)
limit = 1
case str():
logger.debug(f"Looking up kit type by id: {id}")
query = query.filter(models.KitType.id==int(id))
limit = 1
case _:
pass
return query_return(query=query, limit=limit)
def lookup_reagent_types(ctx:Settings,
name: str|None=None,
kit_type: models.KitType|str|None=None,
reagent: models.Reagent|str|None=None,
limit:int=0,
) -> models.ReagentType|List[models.ReagentType]:
"""
_summary_
Args:
ctx (Settings): Settings object passed down from gui.
name (str | None, optional): Reagent type name. Defaults to None.
limit (int, optional): limit of results to return. Defaults to 0.
Returns:
models.ReagentType|List[models.ReagentType]: ReagentType or list of ReagentTypes matching filter.
"""
query = setup_lookup(ctx=ctx, locals=locals()).query(models.ReagentType)
if (kit_type != None and reagent == None) or (reagent != None and kit_type == None):
raise ValueError("Cannot filter without both reagent and kit type.")
elif kit_type == None and reagent == None:
pass
else:
match kit_type:
case str():
kit_type = lookup_kit_types(ctx=ctx, name=kit_type)
case _:
pass
match reagent:
case str():
reagent = lookup_reagents(ctx=ctx, lot_number=reagent)
case _:
pass
assert reagent.type != []
logger.debug(f"Looking up reagent type for {type(kit_type)} {kit_type} and {type(reagent)} {reagent}")
logger.debug(f"Kit reagent types: {kit_type.reagent_types}")
# logger.debug(f"Reagent reagent types: {reagent._sa_instance_state}")
result = list(set(kit_type.reagent_types).intersection(reagent.type))
logger.debug(f"Result: {result}")
try:
return result[0]
except IndexError:
return result
match name:
case str():
logger.debug(f"Looking up reagent type by name: {name}")
query = query.filter(models.ReagentType.name==name)
limit = 1
case _:
pass
return query_return(query=query, limit=limit)
def lookup_submissions(ctx:Settings,
submission_type:str|models.SubmissionType|None=None,
id:int|str|None=None,
rsl_number:str|None=None,
start_date:date|str|int|None=None,
end_date:date|str|int|None=None,
reagent:models.Reagent|str|None=None,
chronologic:bool=False, limit:int=0,
**kwargs
) -> models.BasicSubmission | List[models.BasicSubmission]:
if submission_type == None:
model = models.BasicSubmission.find_subclasses(ctx=ctx, attrs=kwargs)
else:
if isinstance(submission_type, models.SubmissionType):
model = models.BasicSubmission.find_subclasses(ctx=ctx, submission_type=submission_type.name)
else:
model = models.BasicSubmission.find_subclasses(ctx=ctx, submission_type=submission_type)
query = setup_lookup(ctx=ctx, locals=locals()).query(model)
# by submission type
match submission_type:
case models.SubmissionType():
logger.debug(f"Looking up BasicSubmission with submission type: {submission_type}")
# query = query.filter(models.BasicSubmission.submission_type_name==submission_type.name)
query = query.filter(model.submission_type_name==submission_type.name)
case str():
logger.debug(f"Looking up BasicSubmission with submission type: {submission_type}")
# query = query.filter(models.BasicSubmission.submission_type_name==submission_type)
query = query.filter(model.submission_type_name==submission_type)
case _:
pass
# by date range
if start_date != None and end_date == None:
logger.warning(f"Start date with no end date, using today.")
end_date = date.today()
if end_date != None and start_date == None:
logger.warning(f"End date with no start date, using Jan 1, 2023")
start_date = date(2023, 1, 1)
if start_date != None:
match start_date:
case date():
start_date = start_date.strftime("%Y-%m-%d")
case int():
start_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d")
case _:
start_date = parse(start_date).strftime("%Y-%m-%d")
match end_date:
case date():
end_date = end_date.strftime("%Y-%m-%d")
case int():
end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime("%Y-%m-%d")
case _:
end_date = parse(end_date).strftime("%Y-%m-%d")
logger.debug(f"Looking up BasicSubmissions from start date: {start_date} and end date: {end_date}")
# query = query.filter(models.BasicSubmission.submitted_date.between(start_date, end_date))
query = query.filter(model.submitted_date.between(start_date, end_date))
# by reagent (for some reason)
match reagent:
case str():
logger.debug(f"Looking up BasicSubmission with reagent: {reagent}")
reagent = lookup_reagents(ctx=ctx, lot_number=reagent)
query = query.join(models.submissions.reagents_submissions).filter(models.submissions.reagents_submissions.c.reagent_id==reagent.id).all()
case models.Reagent:
logger.debug(f"Looking up BasicSubmission with reagent: {reagent}")
query = query.join(models.submissions.reagents_submissions).filter(models.submissions.reagents_submissions.c.reagent_id==reagent.id).all()
case _:
pass
# by rsl number (returns only a single value)
match rsl_number:
case str():
# query = query.filter(models.BasicSubmission.rsl_plate_num==rsl_number)
query = query.filter(model.rsl_plate_num==rsl_number)
logger.debug(f"At this point the query gets: {query.all()}")
limit = 1
case _:
pass
# by id (returns only a single value)
match id:
case int():
logger.debug(f"Looking up BasicSubmission with id: {id}")
# query = query.filter(models.BasicSubmission.id==id)
query = query.filter(model.id==id)
limit = 1
case str():
logger.debug(f"Looking up BasicSubmission with id: {id}")
# query = query.filter(models.BasicSubmission.id==int(id))
query = query.filter(model.id==int(id))
limit = 1
case _:
pass
for k, v in kwargs.items():
attr = getattr(model, k)
logger.debug(f"Got attr: {attr}")
query = query.filter(attr==v)
if len(kwargs) > 0:
limit = 1
if chronologic:
# query.order_by(models.BasicSubmission.submitted_date)
query.order_by(model.submitted_date)
# logger.debug(f"At the end of the search, the query gets: {query.all()}")
return query_return(query=query, limit=limit)
def lookup_submission_type(ctx:Settings,
name:str|None=None,
limit:int=0
) -> models.SubmissionType|List[models.SubmissionType]:
query = setup_lookup(ctx=ctx, locals=locals()).query(models.SubmissionType)
match name:
case str():
logger.debug(f"Looking up submission type by name: {name}")
query = query.filter(models.SubmissionType.name==name)
limit = 1
case _:
pass
return query_return(query=query, limit=limit)
def lookup_organizations(ctx:Settings,
name:str|None=None,
limit:int=0,
) -> models.Organization|List[models.Organization]:
query = setup_lookup(ctx=ctx, locals=locals()).query(models.Organization)
match name:
case str():
logger.debug(f"Looking up organization with name: {name}")
query = query.filter(models.Organization.name==name)
limit = 1
case _:
pass
return query_return(query=query, limit=limit)
def lookup_discounts(ctx:Settings,
organization:models.Organization|str|int,
kit_type:models.KitType|str|int,
) -> models.Discount|List[models.Discount]:
query = setup_lookup(ctx=ctx, locals=locals()).query(models.Discount)
match organization:
case models.Organization():
logger.debug(f"Looking up discount with organization: {organization}")
organization = organization.id
case str():
logger.debug(f"Looking up discount with organization: {organization}")
organization = lookup_organizations(ctx=ctx, name=organization).id
case int():
logger.debug(f"Looking up discount with organization id: {organization}")
pass
case _:
raise ValueError(f"Invalid value for organization: {organization}")
match kit_type:
case models.KitType():
logger.debug(f"Looking up discount with kit type: {kit_type}")
kit_type = kit_type.id
case str():
logger.debug(f"Looking up discount with kit type: {kit_type}")
kit_type = lookup_kit_types(ctx=ctx, name=kit_type).id
case int():
logger.debug(f"Looking up discount with kit type id: {organization}")
pass
case _:
raise ValueError(f"Invalid value for kit type: {kit_type}")
return query.join(models.KitType).join(models.Organization).filter(and_(
models.KitType.id==kit_type,
models.Organization.id==organization
)).all()
def lookup_controls(ctx:Settings,
control_type:models.ControlType|str|None=None,
start_date:date|str|int|None=None,
end_date:date|str|int|None=None,
control_name:str|None=None,
limit:int=0
) -> models.Control|List[models.Control]:
query = setup_lookup(ctx=ctx, locals=locals()).query(models.Control)
# by control type
match control_type:
case models.ControlType():
logger.debug(f"Looking up control by control type: {control_type}")
query = query.join(models.ControlType).filter(models.ControlType==control_type)
case str():
logger.debug(f"Looking up control by control type: {control_type}")
query = query.join(models.ControlType).filter(models.ControlType.name==control_type)
case _:
pass
# by date range
if start_date != None and end_date == None:
logger.warning(f"Start date with no end date, using today.")
end_date = date.today()
if end_date != None and start_date == None:
logger.warning(f"End date with no start date, using Jan 1, 2023")
start_date = date(2023, 1, 1)
if start_date != None:
match start_date:
case date():
start_date = start_date.strftime("%Y-%m-%d")
case int():
start_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d")
case _:
start_date = parse(start_date).strftime("%Y-%m-%d")
match end_date:
case date():
end_date = end_date.strftime("%Y-%m-%d")
case int():
end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime("%Y-%m-%d")
case _:
end_date = parse(end_date).strftime("%Y-%m-%d")
logger.debug(f"Looking up BasicSubmissions from start date: {start_date} and end date: {end_date}")
query = query.filter(models.Control.submitted_date.between(start_date, end_date))
match control_name:
case str():
query = query.filter(models.Control.name.startswith(control_name))
limit = 1
case _:
pass
return query_return(query=query, limit=limit)
def lookup_control_types(ctx:Settings, limit:int=0) -> models.ControlType|List[models.ControlType]:
query = setup_lookup(ctx=ctx, locals=locals()).query(models.ControlType)
return query_return(query=query, limit=limit)
def lookup_samples(ctx:Settings,
submitter_id:str|None=None,
sample_type:str|None=None,
limit:int=0,
**kwargs
) -> models.BasicSample|models.WastewaterSample|List[models.BasicSample]:
logger.debug(f"Length of kwargs: {len(kwargs)}")
# model = models.find_subclasses(parent=models.BasicSample, attrs=kwargs)
model = models.BasicSample.find_subclasses(ctx=ctx, attrs=kwargs)
query = setup_lookup(ctx=ctx, locals=locals()).query(model)
match submitter_id:
case str():
logger.debug(f"Looking up {model} with submitter id: {submitter_id}")
query = query.filter(models.BasicSample.submitter_id==submitter_id)
limit = 1
case _:
pass
match sample_type:
case str():
logger.debug(f"Looking up {model} with sample type: {sample_type}")
query = query.filter(models.BasicSample.sample_type==sample_type)
case _:
pass
for k, v in kwargs.items():
attr = getattr(model, k)
logger.debug(f"Got attr: {attr}")
query = query.filter(attr==v)
if len(kwargs) > 0:
limit = 1
return query_return(query=query, limit=limit)
def lookup_reagenttype_kittype_association(ctx:Settings,
kit_type:models.KitType|str|None,
reagent_type:models.ReagentType|str|None,
limit:int=0
) -> models.KitTypeReagentTypeAssociation|List[models.KitTypeReagentTypeAssociation]:
query = setup_lookup(ctx=ctx, locals=locals()).query(models.KitTypeReagentTypeAssociation)
match kit_type:
case models.KitType():
query = query.filter(models.KitTypeReagentTypeAssociation.kit_type==kit_type)
case str():
query = query.join(models.KitType).filter(models.KitType.name==kit_type)
case _:
pass
match reagent_type:
case models.ReagentType():
query = query.filter(models.KitTypeReagentTypeAssociation.reagent_type==reagent_type)
case str():
query = query.join(models.ReagentType).filter(models.ReagentType.name==reagent_type)
case _:
pass
if kit_type != None and reagent_type != None:
limit = 1
return query_return(query=query, limit=limit)
def lookup_submission_sample_association(ctx:Settings,
submission:models.BasicSubmission|str|None=None,
sample:models.BasicSample|str|None=None,
row:int=0,
column:int=0,
limit:int=0,
chronologic:bool=False
) -> models.SubmissionSampleAssociation|List[models.SubmissionSampleAssociation]:
query = setup_lookup(ctx=ctx, locals=locals()).query(models.SubmissionSampleAssociation)
match submission:
case models.BasicSubmission():
query = query.filter(models.SubmissionSampleAssociation.submission==submission)
case str():
query = query.join(models.BasicSubmission).filter(models.BasicSubmission.rsl_plate_num==submission)
case _:
pass
match sample:
case models.BasicSample():
query = query.filter(models.SubmissionSampleAssociation.sample==sample)
case str():
query = query.join(models.BasicSample).filter(models.BasicSample.submitter_id==sample)
case _:
pass
if row > 0:
query = query.filter(models.SubmissionSampleAssociation.row==row)
if column > 0:
query = query.filter(models.SubmissionSampleAssociation.column==column)
logger.debug(f"Query count: {query.count()}")
if chronologic:
query.join(models.BasicSubmission).order_by(models.BasicSubmission.submitted_date)
if query.count() <= 1:
limit = 1
return query_return(query=query, limit=limit)
def lookup_modes(ctx:Settings) -> List[str]:
rel = ctx.database_session.query(models.Control).first()
try:
cols = [item.name for item in list(rel.__table__.columns) if isinstance(item.type, JSON)]
except AttributeError as e:
logger.debug(f"Failed to get available modes from db: {e}")
cols = []
return cols
############### Complex Lookups ###################################
def lookup_sub_samp_association_by_plate_sample(ctx:Settings, rsl_plate_num:str|models.BasicSample, rsl_sample_num:str|models.BasicSubmission) -> models.WastewaterAssociation:
"""
_summary_
Args:
ctx (Settings): _description_
rsl_plate_num (str): _description_
sample_submitter_id (_type_): _description_
Returns:
models.SubmissionSampleAssociation: _description_
"""
# logger.debug(f"{type(rsl_plate_num)}, {type(rsl_sample_num)}")
match rsl_plate_num:
case models.BasicSubmission()|models.Wastewater():
# logger.debug(f"Model for rsl_plate_num: {rsl_plate_num}")
first_query = ctx.database_session.query(models.SubmissionSampleAssociation)\
.filter(models.SubmissionSampleAssociation.submission==rsl_plate_num)
case str():
# logger.debug(f"String for rsl_plate_num: {rsl_plate_num}")
first_query = ctx.database_session.query(models.SubmissionSampleAssociation)\
.join(models.BasicSubmission)\
.filter(models.BasicSubmission.rsl_plate_num==rsl_plate_num)
case _:
logger.error(f"Unknown case for rsl_plate_num {rsl_plate_num}")
match rsl_sample_num:
case models.BasicSample()|models.WastewaterSample():
# logger.debug(f"Model for rsl_sample_num: {rsl_sample_num}")
second_query = first_query.filter(models.SubmissionSampleAssociation.sample==rsl_sample_num)
# case models.WastewaterSample:
# second_query = first_query.filter(models.SubmissionSampleAssociation.sample==rsl_sample_num)
case str():
# logger.debug(f"String for rsl_sample_num: {rsl_sample_num}")
second_query = first_query.join(models.BasicSample)\
.filter(models.BasicSample.submitter_id==rsl_sample_num)
case _:
logger.error(f"Unknown case for rsl_sample_num {rsl_sample_num}")
try:
return second_query.first()
except UnboundLocalError:
logger.error(f"Couldn't construct second query")
return None

View File

@@ -1,45 +1,10 @@
'''
Contains all models for sqlalchemy
'''
from sqlalchemy.orm import declarative_base, DeclarativeMeta
import logging
Base: DeclarativeMeta = declarative_base()
metadata = Base.metadata
logger = logging.getLogger(f"submissions.{__name__}")
# def find_subclasses(parent:Any, attrs:dict|None=None, rsl_number:str|None=None) -> Any:
# """
# Finds subclasses of a parent that does contain all
# attributes if the parent does not.
# NOTE: Depreciated, moved to classmethods in individual base models.
# Args:
# parent (_type_): Parent class.
# attrs (dict): Key:Value dictionary of attributes
# Raises:
# AttributeError: Raised if no subclass is found.
# Returns:
# _type_: Parent or subclass.
# """
# if len(attrs) == 0 or attrs == None:
# return parent
# if any([not hasattr(parent, attr) for attr in attrs]):
# # looks for first model that has all included kwargs
# try:
# model = [subclass for subclass in parent.__subclasses__() if all([hasattr(subclass, attr) for attr in attrs])][0]
# except IndexError as e:
# raise AttributeError(f"Couldn't find existing class/subclass of {parent} with all attributes:\n{pformat(attrs)}")
# else:
# model = parent
# logger.debug(f"Using model: {model}")
# return model
from .controls import Control, ControlType
from .kits import KitType, ReagentType, Reagent, Discount, KitTypeReagentTypeAssociation, SubmissionType, SubmissionTypeKitTypeAssociation
# import order must go: orgs, kit, subs due to circular import issues
from .organizations import Organization, Contact
from .submissions import BasicSubmission, BacterialCulture, Wastewater, WastewaterArtic, WastewaterSample, BacterialCultureSample, BasicSample, SubmissionSampleAssociation, WastewaterAssociation
from .kits import KitType, ReagentType, Reagent, Discount, KitTypeReagentTypeAssociation, SubmissionType, SubmissionTypeKitTypeAssociation
from .submissions import (BasicSubmission, BacterialCulture, Wastewater, WastewaterArtic, WastewaterSample, BacterialCultureSample,
BasicSample, SubmissionSampleAssociation, WastewaterAssociation)

View File

@@ -1,12 +1,16 @@
'''
All control related models.
'''
from . import Base
from __future__ import annotations
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.orm import relationship, Query
import logging
from operator import itemgetter
import json
from tools import Base, setup_lookup, query_return
from datetime import date, datetime
from typing import List
from dateutil.parser import parse
logger = logging.getLogger(f"submissions.{__name__}")
@@ -21,6 +25,31 @@ class ControlType(Base):
targets = Column(JSON) #: organisms checked for
instances = relationship("Control", back_populates="controltype") #: control samples created of this type.
@classmethod
@setup_lookup
def query(cls,
name:str=None,
limit:int=0
) -> ControlType|List[ControlType]:
"""
Lookup control archetypes in the database
Args:
ctx (Settings): Settings object passed down from gui.
name (str, optional): Control type name (limits results to 1). Defaults to None.
limit (int, optional): Maximum number of results to return. Defaults to 0.
Returns:
models.ControlType|List[models.ControlType]: ControlType(s) of interest.
"""
query = cls.metadata.session.query(cls)
match name:
case str():
query = query.filter(cls.name==name)
limit = 1
case _:
pass
return query_return(query=query, limit=limit)
class Control(Base):
"""
@@ -136,3 +165,88 @@ class Control(Base):
data = {}
return data
@classmethod
@setup_lookup
def query(cls,
control_type:ControlType|str|None=None,
start_date:date|str|int|None=None,
end_date:date|str|int|None=None,
control_name:str|None=None,
limit:int=0
) -> Control|List[Control]:
"""
Lookup control objects in the database based on a number of parameters.
Args:
control_type (models.ControlType | str | None, optional): Control archetype. Defaults to None.
start_date (date | str | int | None, optional): Beginning date to search by. Defaults to 2023-01-01 if end_date not None.
end_date (date | str | int | None, optional): End date to search by. Defaults to today if start_date not None.
control_name (str | None, optional): Name of control. Defaults to None.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
Returns:
models.Control|List[models.Control]: Control object of interest.
"""
query: Query = cls.metadata.session.query(cls)
# by control type
match control_type:
case ControlType():
logger.debug(f"Looking up control by control type: {control_type}")
# query = query.join(models.ControlType).filter(models.ControlType==control_type)
query = query.filter(cls.controltype==control_type)
case str():
logger.debug(f"Looking up control by control type: {control_type}")
query = query.join(ControlType).filter(ControlType.name==control_type)
case _:
pass
# by date range
if start_date != None and end_date == None:
logger.warning(f"Start date with no end date, using today.")
end_date = date.today()
if end_date != None and start_date == None:
logger.warning(f"End date with no start date, using Jan 1, 2023")
start_date = date(2023, 1, 1)
if start_date != None:
match start_date:
case date():
start_date = start_date.strftime("%Y-%m-%d")
case int():
start_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d")
case _:
start_date = parse(start_date).strftime("%Y-%m-%d")
match end_date:
case date():
end_date = end_date.strftime("%Y-%m-%d")
case int():
end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime("%Y-%m-%d")
case _:
end_date = parse(end_date).strftime("%Y-%m-%d")
logger.debug(f"Looking up BasicSubmissions from start date: {start_date} and end date: {end_date}")
query = query.filter(cls.submitted_date.between(start_date, end_date))
match control_name:
case str():
query = query.filter(cls.name.startswith(control_name))
limit = 1
case _:
pass
return query_return(query=query, limit=limit)
@classmethod
def get_modes(cls):
"""
Get all control modes from database
Args:
ctx (Settings): Settings object passed down from gui.
Returns:
List[str]: List of control mode names.
"""
rel = cls.metadata.session.query(cls).first()
try:
cols = [item.name for item in list(rel.__table__.columns) if isinstance(item.type, JSON)]
except AttributeError as e:
logger.debug(f"Failed to get available modes from db: {e}")
cols = []
return cols

View File

@@ -1,19 +1,20 @@
'''
All kit and reagent related models
'''
from . import Base
from __future__ import annotations
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT
from sqlalchemy.orm import relationship, validates
from sqlalchemy.orm import relationship, validates, Query
from sqlalchemy.ext.associationproxy import association_proxy
from datetime import date
import logging
from tools import Settings, check_authorization
from tools import Settings, check_authorization, Base, setup_lookup, query_return
from typing import List
from . import Organization
logger = logging.getLogger(f'submissions.{__name__}')
reagenttypes_reagents = Table("_reagenttypes_reagents", Base.metadata, Column("reagent_id", INTEGER, ForeignKey("_reagents.id")), Column("reagenttype_id", INTEGER, ForeignKey("_reagent_types.id")))
class KitType(Base):
"""
Base of kits used in submission processing
@@ -102,9 +103,59 @@ class KitType(Base):
return map
@check_authorization
def save(self, ctx:Settings):
ctx.database_session.add(self)
ctx.database_session.commit()
def save(self):
self.metadata.session.add(self)
self.metadata.session.commit()
@classmethod
@setup_lookup
def query(cls,
name:str=None,
used_for:str|SubmissionType|None=None,
id:int|None=None,
limit:int=0
) -> KitType|List[KitType]:
"""
Lookup a list of or single KitType.
Args:
ctx (Settings): Settings object passed down from gui
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.
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.
Returns:
models.KitType|List[models.KitType]: KitType(s) of interest.
"""
query: Query = cls.metadata.session.query(cls)
match used_for:
case str():
logger.debug(f"Looking up kit type by use: {used_for}")
query = query.filter(cls.used_for.any(name=used_for))
case SubmissionType():
query = query.filter(cls.used_for.contains(used_for))
case _:
pass
match name:
case str():
logger.debug(f"Looking up kit type by name: {name}")
query = query.filter(cls.name==name)
limit = 1
case _:
pass
match id:
case int():
logger.debug(f"Looking up kit type by id: {id}")
query = query.filter(cls.id==id)
limit = 1
case str():
logger.debug(f"Looking up kit type by id: {id}")
query = query.filter(cls.id==int(id))
limit = 1
case _:
pass
return query_return(query=query, limit=limit)
class ReagentType(Base):
"""
@@ -140,6 +191,60 @@ class ReagentType(Base):
def __repr__(self):
return f"ReagentType({self.name})"
@classmethod
@setup_lookup
def query(cls,
name: str|None=None,
kit_type: KitType|str|None=None,
reagent: Reagent|str|None=None,
limit:int=0,
) -> ReagentType|List[ReagentType]:
"""
Lookup reagent types in the database.
Args:
ctx (Settings): Settings object passed down from gui.
name (str | None, optional): Reagent type name. Defaults to None.
limit (int, optional): maxmimum number of results to return (0 = all). Defaults to 0.
Returns:
models.ReagentType|List[models.ReagentType]: ReagentType or list of ReagentTypes matching filter.
"""
query: Query = cls.metadata.session.query(cls)
if (kit_type != None and reagent == None) or (reagent != None and kit_type == None):
raise ValueError("Cannot filter without both reagent and kit type.")
elif kit_type == None and reagent == None:
pass
else:
match kit_type:
case str():
kit_type = KitType.query(name=kit_type)
case _:
pass
match reagent:
case str():
reagent = Reagent.query(lot_number=reagent)
case _:
pass
assert reagent.type != []
logger.debug(f"Looking up reagent type for {type(kit_type)} {kit_type} and {type(reagent)} {reagent}")
logger.debug(f"Kit reagent types: {kit_type.reagent_types}")
# logger.debug(f"Reagent reagent types: {reagent._sa_instance_state}")
result = list(set(kit_type.reagent_types).intersection(reagent.type))
logger.debug(f"Result: {result}")
try:
return result[0]
except IndexError:
return None
match name:
case str():
logger.debug(f"Looking up reagent type by name: {name}")
query = query.filter(cls.name==name)
limit = 1
case _:
pass
return query_return(query=query, limit=limit)
class KitTypeReagentTypeAssociation(Base):
"""
table containing reagenttype/kittype associations
@@ -178,6 +283,49 @@ class KitTypeReagentTypeAssociation(Base):
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,
reagent_type:ReagentType|str|None,
limit:int=0
) -> KitTypeReagentTypeAssociation|List[KitTypeReagentTypeAssociation]:
"""
Lookup junction of ReagentType and KitType
Args:
ctx (Settings): Settings object passed down from gui.
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.metadata.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):
self.metadata.session.add(self)
self.metadata.session.commit()
return None
class Reagent(Base):
"""
@@ -199,7 +347,6 @@ class Reagent(Base):
else:
return f"<Reagent({self.type.name}-{self.lot})>"
def __str__(self) -> str:
"""
string representing this object
@@ -209,7 +356,6 @@ class Reagent(Base):
"""
return str(self.lot)
def to_sub_dict(self, extraction_kit:KitType=None) -> dict:
"""
dictionary containing values necessary for gui
@@ -282,10 +428,47 @@ class Reagent(Base):
"expiry": self.expiry.strftime("%Y-%m-%d")
}
def save(self, ctx:Settings):
ctx.database_session.add(self)
ctx.database_session.commit()
def save(self):
self.metadata.session.add(self)
self.metadata.session.commit()
@classmethod
@setup_lookup
def query(cls, reagent_type:str|ReagentType|None=None,
lot_number:str|None=None,
limit:int=0
) -> Reagent|List[Reagent]:
"""
Lookup a list of reagents from the database.
Args:
ctx (Settings): Settings object passed down from gui
reagent_type (str | models.ReagentType | None, optional): Reagent type. Defaults to None.
lot_number (str | None, optional): Reagent lot number. Defaults to None.
limit (int, optional): limit of results returned. Defaults to 0.
Returns:
models.Reagent | List[models.Reagent]: reagent or list of reagents matching filter.
"""
query: Query = cls.metadata.session.query(cls)
match reagent_type:
case str():
logger.debug(f"Looking up reagents by reagent type: {reagent_type}")
query = query.join(cls.type, aliased=True).filter(ReagentType.name==reagent_type)
case ReagentType():
logger.debug(f"Looking up reagents by reagent type: {reagent_type}")
query = query.filter(cls.type.contains(reagent_type))
case _:
pass
match lot_number:
case str():
logger.debug(f"Looking up reagent by lot number: {lot_number}")
query = query.filter(cls.lot==lot_number)
# In this case limit number returned.
limit = 1
case _:
pass
return query_return(query=query, limit=limit)
class Discount(Base):
"""
@@ -303,6 +486,56 @@ class Discount(Base):
def __repr__(self) -> str:
return f"<Discount({self.name})>"
@classmethod
@setup_lookup
def query(cls,
organization:Organization|str|int|None=None,
kit_type:KitType|str|int|None=None,
) -> Discount|List[Discount]:
"""
Lookup discount objects (union of kit and organization)
Args:
ctx (Settings): Settings object passed down from the gui.
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.
"""
query: Query = cls.metadata.session.query(cls)
match organization:
case Organization():
logger.debug(f"Looking up discount with organization: {organization}")
query = query.filter(cls.client==Organization)
case str():
logger.debug(f"Looking up discount with organization: {organization}")
query = query.join(Organization).filter(Organization.name==organization)
case int():
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():
logger.debug(f"Looking up discount with kit type: {kit_type}")
query = query.filter(cls.kit==kit_type)
case str():
logger.debug(f"Looking up discount with kit type: {kit_type}")
query = query.join(KitType).filter(KitType.name==kit_type)
case int():
logger.debug(f"Looking up discount with kit type id: {organization}")
query = query.join(KitType).filter(KitType.id==kit_type)
case _:
# raise ValueError(f"Invalid value for kit type: {kit_type}")
pass
return query.all()
class SubmissionType(Base):
"""
@@ -326,8 +559,34 @@ class SubmissionType(Base):
def __repr__(self) -> str:
return f"<SubmissionType({self.name})>"
@classmethod
@setup_lookup
def query(cls,
name:str|None=None,
limit:int=0
) -> SubmissionType|List[SubmissionType]:
"""
Lookup submission type in the database by a number of parameters
Args:
ctx (Settings): Settings object passed down from gui
name (str | None, optional): Name of submission type. Defaults to None.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
Returns:
models.SubmissionType|List[models.SubmissionType]: SubmissionType(s) of interest.
"""
query: Query = cls.metadata.session.query(cls)
match name:
case str():
logger.debug(f"Looking up submission type by name: {name}")
query = query.filter(cls.name==name)
limit = 1
case _:
pass
return query_return(query=query, limit=limit)
class SubmissionTypeKitTypeAssociation(Base):
"""
Abstract of relationship between kits and their submission type.
@@ -352,7 +611,39 @@ class SubmissionTypeKitTypeAssociation(Base):
self.constant_cost = 0.00
def __repr__(self) -> str:
return f"<SubmissionTypeKitTypeAssociation({self.submission_type.name})"
return f"<SubmissionTypeKitTypeAssociation({self.submission_type.name})>"
def set_attrib(self, name, value):
self.__setattr__(name, value)
self.__setattr__(name, value)
@classmethod
@setup_lookup
def query(cls,
submission_type:SubmissionType|str|int|None=None,
kit_type:KitType|str|int|None=None,
limit:int=0
):
query: Query = cls.metadata.session.query(cls)
match submission_type:
case SubmissionType():
logger.debug(f"Looking up {cls.__name__} by SubmissionType {submission_type}")
query = query.filter(cls.submission_type==submission_type)
case str():
logger.debug(f"Looking up {cls.__name__} by name {submission_type}")
query = query.join(SubmissionType).filter(SubmissionType.name==submission_type)
case int():
logger.debug(f"Looking up {cls.__name__} by id {submission_type}")
query = query.join(SubmissionType).filter(SubmissionType.id==submission_type)
match kit_type:
case KitType():
logger.debug(f"Looking up {cls.__name__} by KitType {kit_type}")
query = query.filter(cls.kit_type==kit_type)
case str():
logger.debug(f"Looking up {cls.__name__} by name {kit_type}")
query = query.join(KitType).filter(KitType.name==kit_type)
case int():
logger.debug(f"Looking up {cls.__name__} by id {kit_type}")
query = query.join(KitType).filter(KitType.id==kit_type)
limit = query.count()
return query_return(query=query, limit=limit)

View File

@@ -1,14 +1,18 @@
'''
All client organization related models.
'''
from . import Base
from __future__ import annotations
from sqlalchemy import Column, String, INTEGER, ForeignKey, Table
from sqlalchemy.orm import relationship
from sqlalchemy.orm import relationship, Query
from tools import Base, check_authorization, setup_lookup, query_return
from typing import List
import logging
logger = logging.getLogger(f"submissions.{__name__}")
# table containing organization/contact relationship
orgs_contacts = Table("_orgs_contacts", Base.metadata, Column("org_id", INTEGER, ForeignKey("_organizations.id")), Column("contact_id", INTEGER, ForeignKey("_contacts.id")))
class Organization(Base):
"""
Base of organization
@@ -33,6 +37,7 @@ class Organization(Base):
def __repr__(self) -> str:
return f"<Organization({self.name})>"
@check_authorization
def save(self, ctx):
ctx.database_session.add(self)
ctx.database_session.commit()
@@ -40,6 +45,31 @@ class Organization(Base):
def set_attribute(self, name:str, value):
setattr(self, name, value)
@classmethod
@setup_lookup
def query(cls,
name:str|None=None,
limit:int=0,
) -> Organization|List[Organization]:
"""
Lookup organizations in the database by a number of parameters.
Args:
name (str | None, optional): Name of the organization. Defaults to None.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
Returns:
Organization|List[Organization]: _description_
"""
query: Query = cls.metadata.session.query(cls)
match name:
case str():
logger.debug(f"Looking up organization with name: {name}")
query = query.filter(cls.name==name)
limit = 1
case _:
pass
return query_return(query=query, limit=limit)
class Contact(Base):
"""
@@ -56,3 +86,44 @@ class Contact(Base):
def __repr__(self) -> str:
return f"<Contact({self.name})>"
@classmethod
@setup_lookup
def query(cls,
name:str|None=None,
email:str|None=None,
phone:str|None=None,
limit:int=0,
) -> Contact|List[Contact]:
"""
Lookup contacts in the database by a number of parameters.
Args:
name (str | None, optional): Name of the contact. Defaults to None.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
Returns:
Contact|List[Contact]: _description_
"""
query: Query = cls.metadata.session.query(cls)
match name:
case str():
logger.debug(f"Looking up contact with name: {name}")
query = query.filter(cls.name==name)
limit = 1
case _:
pass
match email:
case str():
logger.debug(f"Looking up contact with email: {name}")
query = query.filter(cls.email==email)
limit = 1
case _:
pass
match phone:
case str():
logger.debug(f"Looking up contact with phone: {name}")
query = query.filter(cls.phone==phone)
limit = 1
case _:
pass
return query_return(query=query, limit=limit)

View File

@@ -1,25 +1,29 @@
'''
Models for the main submission types.
'''
from __future__ import annotations
from getpass import getuser
import math
from pprint import pformat
from . import Base
from . import Reagent, SubmissionType
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, Table, JSON, FLOAT, case
from sqlalchemy.orm import relationship, validates
from sqlalchemy.orm import relationship, validates, Query
import logging
import json
from json.decoder import JSONDecodeError
from math import ceil
from sqlalchemy.ext.associationproxy import association_proxy
import uuid
from dateutil.parser import parse
import re
import pandas as pd
from openpyxl import Workbook
from tools import check_not_nan, row_map, Settings
from pathlib import Path
from datetime import datetime
from tools import check_not_nan, row_map, Base, query_return, setup_lookup
from datetime import datetime, date
from typing import List
from dateutil.parser import parse
import yaml
from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
logger = logging.getLogger(f"submissions.{__name__}")
@@ -299,7 +303,7 @@ class BasicSubmission(Base):
return input_excel
@classmethod
def enforce_name(cls, ctx:Settings, instr:str) -> str:
def enforce_name(cls, instr:str) -> str:
logger.debug(f"Hello from {cls.__mapper_args__['polymorphic_identity']} Enforcer!")
logger.debug(f"Attempting enforcement on {instr}")
return instr
@@ -311,7 +315,7 @@ class BasicSubmission(Base):
return regex
@classmethod
def find_subclasses(cls, ctx:Settings, attrs:dict|None=None, submission_type:str|None=None):
def find_subclasses(cls, attrs:dict|None=None, submission_type:str|None=None):
if submission_type != None:
return cls.find_polymorphic_subclass(submission_type)
if len(attrs) == 0 or attrs == None:
@@ -331,24 +335,156 @@ class BasicSubmission(Base):
def find_polymorphic_subclass(cls, polymorphic_identity:str|None=None):
if isinstance(polymorphic_identity, dict):
polymorphic_identity = polymorphic_identity['value']
if polymorphic_identity == None:
return cls
else:
if polymorphic_identity != None:
try:
return [item for item in cls.__subclasses__() if item.__mapper_args__['polymorphic_identity']==polymorphic_identity][0]
cls = [item for item in cls.__subclasses__() if item.__mapper_args__['polymorphic_identity']==polymorphic_identity][0]
logger.info(f"Recruiting: {cls}")
except Exception as e:
logger.error(f"Could not get polymorph {polymorphic_identity} of {cls} due to {e}")
return cls
return cls
@classmethod
def parse_pcr(cls, xl:pd.DataFrame, rsl_number:str) -> list:
logger.debug(f"Hello from {cls.__mapper_args__['polymorphic_identity']} PCR parser!")
return []
def save(self, ctx:Settings):
def save(self):
self.uploaded_by = getuser()
ctx.database_session.add(self)
ctx.database_session.commit()
self.metadata.session.add(self)
self.metadata.session.commit()
return None
def delete(self):
backup = self.to_dict()
try:
with open(self.metadata.backup_path.joinpath(f"{self.rsl_plate_num}-backup({date.today().strftime('%Y%m%d')}).yml"), "w") as f:
yaml.dump(backup, f)
except KeyError:
pass
self.metadata.database_session.delete(self)
try:
self.metadata.session.commit()
except (SQLIntegrityError, SQLOperationalError, AlcIntegrityError, AlcOperationalError) as e:
self.metadata.session.rollback()
raise e
@classmethod
@setup_lookup
def query(cls,
submission_type:str|SubmissionType|None=None,
id:int|str|None=None,
rsl_number:str|None=None,
start_date:date|str|int|None=None,
end_date:date|str|int|None=None,
reagent:Reagent|str|None=None,
chronologic:bool=False, limit:int=0,
**kwargs
) -> BasicSubmission | List[BasicSubmission]:
"""
Lookup submissions based on a number of parameters.
Args:
submission_type (str | models.SubmissionType | None, optional): Submission type of interest. Defaults to None.
id (int | str | None, optional): Submission id in the database (limits results to 1). Defaults to None.
rsl_number (str | None, optional): Submission name in the database (limits results to 1). Defaults to None.
start_date (date | str | int | None, optional): Beginning date to search by. Defaults to None.
end_date (date | str | int | None, optional): Ending date to search by. Defaults to None.
reagent (models.Reagent | str | None, optional): A reagent used in the submission. Defaults to None.
chronologic (bool, optional): Return results in chronologic order. Defaults to False.
limit (int, optional): Maximum number of results to return. Defaults to 0.
Returns:
models.BasicSubmission | List[models.BasicSubmission]: Submission(s) of interest
"""
# NOTE: if you go back to using 'model' change the appropriate cls to model in the query filters
if submission_type == None:
model = cls.find_subclasses(attrs=kwargs)
else:
if isinstance(submission_type, SubmissionType):
model = cls.find_subclasses(submission_type=submission_type.name)
else:
model = cls.find_subclasses(submission_type=submission_type)
# query: Query = setup_lookup(ctx=ctx, locals=locals()).query(model)
query: Query = cls.metadata.session.query(model)
# by submission type
# match submission_type:
# case SubmissionType():
# logger.debug(f"Looking up BasicSubmission with submission type: {submission_type}")
# query = query.filter(model.submission_type_name==submission_type.name)
# case str():
# logger.debug(f"Looking up BasicSubmission with submission type: {submission_type}")
# query = query.filter(model.submission_type_name==submission_type)
# case _:
# pass
# by date range
if start_date != None and end_date == None:
logger.warning(f"Start date with no end date, using today.")
end_date = date.today()
if end_date != None and start_date == None:
logger.warning(f"End date with no start date, using Jan 1, 2023")
start_date = date(2023, 1, 1)
if start_date != None:
match start_date:
case date():
start_date = start_date.strftime("%Y-%m-%d")
case int():
start_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + start_date - 2).date().strftime("%Y-%m-%d")
case _:
start_date = parse(start_date).strftime("%Y-%m-%d")
match end_date:
case date():
end_date = end_date.strftime("%Y-%m-%d")
case int():
end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date().strftime("%Y-%m-%d")
case _:
end_date = parse(end_date).strftime("%Y-%m-%d")
logger.debug(f"Looking up BasicSubmissions from start date: {start_date} and end date: {end_date}")
query = query.filter(cls.submitted_date.between(start_date, end_date))
# by reagent (for some reason)
match reagent:
case str():
logger.debug(f"Looking up BasicSubmission with reagent: {reagent}")
# reagent = Reagent.query(lot_number=reagent)
# query = query.join(reagents_submissions).filter(reagents_submissions.c.reagent_id==reagent.id)
query = query.join(cls.reagents).filter(Reagent.lot==reagent)
case Reagent():
logger.debug(f"Looking up BasicSubmission with reagent: {reagent}")
query = query.join(reagents_submissions).filter(reagents_submissions.c.reagent_id==reagent.id)
case _:
pass
# by rsl number (returns only a single value)
match rsl_number:
case str():
query = query.filter(cls.rsl_plate_num==rsl_number)
logger.debug(f"At this point the query gets: {query.all()}")
limit = 1
case _:
pass
# by id (returns only a single value)
match id:
case int():
logger.debug(f"Looking up BasicSubmission with id: {id}")
query = query.filter(cls.id==id)
limit = 1
case str():
logger.debug(f"Looking up BasicSubmission with id: {id}")
query = query.filter(cls.id==int(id))
limit = 1
case _:
pass
for k, v in kwargs.items():
attr = getattr(cls, k)
logger.debug(f"Got attr: {attr}")
query = query.filter(attr==v)
if len(kwargs) > 0:
limit = 1
if chronologic:
query.order_by(cls.submitted_date)
return query_return(query=query, limit=limit)
@classmethod
def filename_template(cls):
return "{{ rsl_plate_num }}"
# Below are the custom submission types
@@ -415,9 +551,9 @@ class BacterialCulture(BasicSubmission):
return input_excel
@classmethod
def enforce_name(cls, ctx:Settings, instr:str) -> str:
outstr = super().enforce_name(ctx=ctx, instr=instr)
def construct(ctx) -> str:
def enforce_name(cls, instr:str) -> str:
outstr = super().enforce_name(instr=instr)
def construct() -> str:
"""
DEPRECIATED due to slowness. Search for the largest rsl number and increment by 1
@@ -426,7 +562,8 @@ class BacterialCulture(BasicSubmission):
"""
logger.debug(f"Attempting to construct RSL number from scratch...")
# directory = Path(self.ctx['directory_path']).joinpath("Bacteria")
directory = Path(ctx.directory_path).joinpath("Bacteria")
# directory = Path(ctx.directory_path).joinpath("Bacteria")
directory = cls.metadata.directory_path.joinpath("Bacteria")
year = str(datetime.now().year)[-2:]
if directory.exists():
logger.debug(f"Year: {year}")
@@ -449,7 +586,7 @@ class BacterialCulture(BasicSubmission):
try:
outstr = re.sub(r"RSL(\d{2})", r"RSL-\1", outstr, flags=re.IGNORECASE)
except (AttributeError, TypeError) as e:
outstr = construct(ctx=ctx)
outstr = construct()
# year = datetime.now().year
# self.parsed_name = f"RSL-{str(year)[-2:]}-0000"
return re.sub(r"RSL-(\d{2})(\d{4})", r"RSL-\1-\2", outstr, flags=re.IGNORECASE)
@@ -458,6 +595,12 @@ class BacterialCulture(BasicSubmission):
def get_regex(cls):
return "(?P<Bacterial_Culture>RSL-?\\d{2}-?\\d{4})"
@classmethod
def filename_template(cls):
template = super().filename_template()
template += "_{{ submitting_lab }}_{{ submitter_plate_num }}"
return template
class Wastewater(BasicSubmission):
"""
derivative submission type from BasicSubmission
@@ -537,8 +680,8 @@ class Wastewater(BasicSubmission):
return samples
@classmethod
def enforce_name(cls, ctx:Settings, instr:str) -> str:
outstr = super().enforce_name(ctx=ctx, instr=instr)
def enforce_name(cls, instr:str) -> str:
outstr = super().enforce_name(instr=instr)
def construct():
today = datetime.now()
return f"RSL-WW-{today.year}{str(today.month).zfill(2)}{str(today.day).zfill(2)}"
@@ -620,8 +763,8 @@ class WastewaterArtic(BasicSubmission):
return input_dict
@classmethod
def enforce_name(cls, ctx:Settings, instr:str) -> str:
outstr = super().enforce_name(ctx=ctx, instr=instr)
def enforce_name(cls, instr:str) -> str:
outstr = super().enforce_name(instr=instr)
def construct():
today = datetime.now()
return f"RSL-AR-{today.year}{str(today.month).zfill(2)}{str(today.day).zfill(2)}"
@@ -727,7 +870,7 @@ class BasicSample(Base):
return dict(name=self.submitter_id[:10], positive=False, tooltip=tooltip_text)
@classmethod
def find_subclasses(cls, ctx:Settings, attrs:dict|None=None, rsl_number:str|None=None):
def find_subclasses(cls, attrs:dict|None=None, rsl_number:str|None=None):
if len(attrs) == 0 or attrs == None:
return cls
if any([not hasattr(cls, attr) for attr in attrs]):
@@ -737,7 +880,7 @@ class BasicSample(Base):
except IndexError as e:
raise AttributeError(f"Couldn't find existing class/subclass of {cls} with all attributes:\n{pformat(attrs)}")
else:
model = cls
return cls
logger.debug(f"Using model: {model}")
return model
@@ -758,6 +901,51 @@ class BasicSample(Base):
def parse_sample(cls, input_dict:dict) -> dict:
# logger.debug(f"Called {cls.__name__} sample parser")
return input_dict
@classmethod
@setup_lookup
def query(cls,
submitter_id:str|None=None,
# sample_type:str|None=None,
limit:int=0,
**kwargs
) -> BasicSample|List[BasicSample]:
"""
Lookup samples in the database by a number of parameters.
Args:
ctx (Settings): Settings object passed down from gui
submitter_id (str | None, optional): Name of the sample (limits results to 1). Defaults to None.
sample_type (str | None, optional): Sample type. Defaults to None.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
Returns:
models.BasicSample|List[models.BasicSample]: Sample(s) of interest.
"""
logger.debug(f"Length of kwargs: {len(kwargs)}")
# model = models.BasicSample.find_subclasses(ctx=ctx, attrs=kwargs)
# query: Query = setup_lookup(ctx=ctx, locals=locals()).query(model)
query: Query = cls.metadata.session.query(cls)
match submitter_id:
case str():
logger.debug(f"Looking up {cls} with submitter id: {submitter_id}")
query = query.filter(cls.submitter_id==submitter_id)
limit = 1
case _:
pass
# match sample_type:
# case str():
# logger.debug(f"Looking up {model} with sample type: {sample_type}")
# query = query.filter(models.BasicSample.sample_type==sample_type)
# case _:
# pass
for k, v in kwargs.items():
attr = getattr(cls, k)
logger.debug(f"Got attr: {attr}")
query = query.filter(attr==v)
if len(kwargs) > 0:
limit = 1
return query_return(query=query, limit=limit)
class WastewaterSample(BasicSample):
"""
@@ -772,53 +960,6 @@ class WastewaterSample(BasicSample):
sample_location = Column(String(8)) #: location on 24 well plate
__mapper_args__ = {"polymorphic_identity": "Wastewater Sample", "polymorphic_load": "inline"}
# @validates("collected-date")
# def convert_cdate_time(self, key, value):
# logger.debug(f"Validating {key}: {value}")
# if isinstance(value, Timestamp):
# return value.date()
# if isinstance(value, str):
# return parse(value)
# return value
# @validates("rsl_number")
# def use_submitter_id(self, key, value):
# logger.debug(f"Validating {key}: {value}")
# return value or self.submitter_id
# def set_attribute(self, name:str, value):
# """
# Set an attribute of this object. Extends parent.
# Args:
# name (str): name of the attribute
# value (_type_): value to be set
# """
# # Due to the plate map being populated with RSL numbers, we have to do some shuffling.
# match name:
# case "submitter_id":
# # If submitter_id already has a value, stop
# if self.submitter_id != None:
# return
# # otherwise also set rsl_number to the same value
# else:
# super().set_attribute("rsl_number", value)
# case "ww_full_sample_id":
# # If value present, set ww_full_sample_id and make this the submitter_id
# if value != None:
# super().set_attribute(name, value)
# name = "submitter_id"
# case 'collection_date':
# # If this is a string use dateutils to parse into date()
# if isinstance(value, str):
# logger.debug(f"collection_date {value} is a string. Attempting parse...")
# value = parse(value)
# case "rsl_number":
# if value == None:
# value = self.submitter_id
# super().set_attribute(name, value)
def to_hitpick(self, submission_rsl:str) -> dict|None:
"""
Outputs a dictionary usable for html plate maps. Extends parent method.
@@ -924,6 +1065,61 @@ class SubmissionSampleAssociation(Base):
except Exception as e:
logger.error(f"Could not get polymorph {polymorphic_identity} of {cls} due to {e}")
return cls
@classmethod
@setup_lookup
def query(cls,
submission:BasicSubmission|str|None=None,
sample:BasicSample|str|None=None,
row:int=0,
column:int=0,
limit:int=0,
chronologic:bool=False
) -> SubmissionSampleAssociation|List[SubmissionSampleAssociation]:
"""
Lookup junction of Submission and Sample in the database
Args:
submission (models.BasicSubmission | str | None, optional): Submission of interest. Defaults to None.
sample (models.BasicSample | str | None, optional): Sample of interest. Defaults to None.
row (int, optional): Row of the sample location on submission plate. Defaults to 0.
column (int, optional): Column of the sample location on the submission plate. Defaults to 0.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
chronologic (bool, optional): Return results in chronologic order. Defaults to False.
Returns:
models.SubmissionSampleAssociation|List[models.SubmissionSampleAssociation]: Junction(s) of interest
"""
query: Query = cls.metadata.session.query(cls)
match submission:
case BasicSubmission():
query = query.filter(cls.submission==submission)
case str():
query = query.join(BasicSubmission).filter(BasicSubmission.rsl_plate_num==submission)
case _:
pass
match sample:
case BasicSample():
query = query.filter(cls.sample==sample)
case str():
query = query.join(BasicSample).filter(BasicSample.submitter_id==sample)
case _:
pass
if row > 0:
query = query.filter(cls.row==row)
if column > 0:
query = query.filter(cls.column==column)
logger.debug(f"Query count: {query.count()}")
if chronologic:
query.join(BasicSubmission).order_by(BasicSubmission.submitted_date)
if query.count() == 1:
limit = 1
return query_return(query=query, limit=limit)
def save(self):
self.metadata.session.add(self)
self.metadata.session.commit()
return None
class WastewaterAssociation(SubmissionSampleAssociation):
"""