Minor bug fixes.

This commit is contained in:
Landon Wark
2023-12-07 12:50:03 -06:00
parent cbf36a5b0b
commit 0dd51827a0
22 changed files with 308 additions and 370 deletions

View File

@@ -1,7 +1,9 @@
## 202312.02
## 202312.01 ## 202312.01
- Control samples info now available in plate map. - Control samples info now available in plate map.
- Backups will now create an regenerated xlsx file. - Backups will now create a regenerated xlsx file.
- Report generator now does sums automatically. - Report generator now does sums automatically.
## 202311.04 ## 202311.04

View File

@@ -1,9 +1,10 @@
- [x] SubmissionReagentAssociation.query
- [x] Move as much from db.functions to objects as possible.
- [x] Clean up DB objects after failed test fix. - [x] Clean up DB objects after failed test fix.
- [x] Fix tests. - [x] Fix tests.
- [ ] Fix pydant.PydSample.handle_duplicate_samples? - [x] Fix pydant.PydSample.handle_duplicate_samples?
- [ ] See if the number of queries in BasicSubmission functions (and others) can be trimmed down. - [ ] See if the number of queries in BasicSubmission functions (and others) can be trimmed down.
- [x] Document code - [x] Document code
- Done Submissions up to BasicSample
- [x] Create a result object to facilitate returning function results. - [x] Create a result object to facilitate returning function results.
- [x] Refactor main_window_functions into as many objects (forms, etc.) as possible to clean it up. - [x] Refactor main_window_functions into as many objects (forms, etc.) as possible to clean it up.
- [x] Integrate 'Construct First Strand' into the Artic import. - [x] Integrate 'Construct First Strand' into the Artic import.

View File

@@ -55,9 +55,9 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
# are written from script.py.mako # are written from script.py.mako
# output_encoding = utf-8 # output_encoding = utf-8
; sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db
; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\Submissions_app_backups\DB_backups\submissions-new.db ; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\Submissions_app_backups\DB_backups\submissions-new.db
sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\tests\test_assets\submissions-test.db ; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\python\submissions\tests\test_assets\submissions-test.db
[post_write_hooks] [post_write_hooks]

View File

@@ -0,0 +1,44 @@
"""SubmissionReagentAssociations added
Revision ID: 238c3c3e5863
Revises: 2684f065037c
Create Date: 2023-12-05 12:57:17.446606
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '238c3c3e5863'
down_revision = '2684f065037c'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_reagents_submissions', schema=None) as batch_op:
batch_op.add_column(sa.Column('comments', sa.String(length=1024), nullable=True))
batch_op.alter_column('reagent_id',
existing_type=sa.INTEGER(),
nullable=False)
batch_op.alter_column('submission_id',
existing_type=sa.INTEGER(),
nullable=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_reagents_submissions', schema=None) as batch_op:
batch_op.alter_column('submission_id',
existing_type=sa.INTEGER(),
nullable=True)
batch_op.alter_column('reagent_id',
existing_type=sa.INTEGER(),
nullable=True)
batch_op.drop_column('comments')
# ### end Alembic commands ###

View File

@@ -4,7 +4,7 @@ from pathlib import Path
# Version of the realpython-reader package # Version of the realpython-reader package
__project__ = "submissions" __project__ = "submissions"
__version__ = "202312.1b" __version__ = "202312.2b"
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"} __author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
__copyright__ = "2022-2023, Government of Canada" __copyright__ = "2022-2023, Government of Canada"

View File

@@ -1,7 +1,6 @@
import sys import sys, os
import os
# environment variable must be set to enable qtwebengine in network path
from tools import ctx, setup_logger, check_if_app from tools import ctx, setup_logger, check_if_app
# environment variable must be set to enable qtwebengine in network path
if check_if_app(): if check_if_app():
os.environ['QTWEBENGINE_DISABLE_SANDBOX'] = "1" os.environ['QTWEBENGINE_DISABLE_SANDBOX'] = "1"
# setup custom logger # setup custom logger

View File

@@ -1,5 +1,21 @@
''' '''
All database related operations. All database related operations.
''' '''
from .functions import * from sqlalchemy import event
from sqlalchemy.engine import Engine
@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()
from .models import * from .models import *

View File

@@ -1,211 +0,0 @@
'''Contains or imports all database convenience functions'''
from tools import Result, Report
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 .models import *
import logging
from backend.validators.pydant import *
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 submissions_to_df(submission_type:str|None=None, limit:int=0) -> pd.DataFrame:
"""
Convert submissions looked up by type to dataframe
Args:
ctx (Settings): settings object passed by gui
submission_type (str | None, optional): submission type (should be string in D3 of excel sheet) Defaults to None.
limit (int): Maximum number of submissions to return. Defaults to 0.
Returns:
pd.DataFrame: dataframe constructed from retrieved submissions
"""
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 BasicSubmission.query(submission_type=submission_type, limit=limit)]
logger.debug(f"Got {len(subs)} submissions.")
# make df from dicts (records) in list
df = pd.DataFrame.from_records(subs)
# Exclude sub information
try:
df = df.drop("controls", axis=1)
except:
logger.warning(f"Couldn't drop 'controls' column from submissionsheet df.")
try:
df = df.drop("ext_info", axis=1)
except:
logger.warning(f"Couldn't drop 'ext_info' column from submissionsheet df.")
try:
df = df.drop("pcr_info", axis=1)
except:
logger.warning(f"Couldn't drop 'pcr_info' column from submissionsheet df.")
# NOTE: Moved to submissions_to_df function
try:
del df['samples']
except KeyError:
pass
try:
del df['reagents']
except KeyError:
pass
try:
del df['comments']
except KeyError:
pass
return df
def get_control_subtypes(type:str, mode:str) -> list[str]:
"""
Get subtypes for a control analysis mode
Args:
ctx (Settings): settings object passed from gui
type (str): control type name
mode (str): analysis mode name
Returns:
list[str]: list of subtype names
"""
# 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 = Control.query(control_type=type, limit=1)
except (TypeError, IndexError):
return []
# Get analysis mode data as dict
jsoner = json.loads(getattr(outs, mode))
logger.debug(f"JSON out: {jsoner}")
try:
genera = list(jsoner.keys())[0]
except IndexError:
return []
subtypes = [item for item in jsoner[genera] if "_hashes" not in item and "_ratio" not in item]
return subtypes
def update_last_used(reagent:Reagent, kit:KitType):
"""
Updates the 'last_used' field in kittypes/reagenttypes
Args:
reagent (models.Reagent): reagent to be used for update
kit (models.KitType): kit to be used for lookup
"""
report = Report()
logger.debug(f"Attempting update of reagent type at intersection of ({reagent}), ({kit})")
rt = ReagentType.query(kit_type=kit, reagent=reagent)
if rt != None:
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
result = assoc.save()
return(report.add_result(result))
return report.add_result(Result(msg=f"Updating last used {rt} was not performed.", status="Information"))
def check_kit_integrity(sub:BasicSubmission|KitType|PydSubmission, reagenttypes:list=[]) -> Tuple[list, Report]:
"""
Ensures all reagents expected in kit are listed in Submission
Args:
sub (BasicSubmission | KitType): Object containing complete list of reagent types.
reagenttypes (list | None, optional): List to check against complete list. Defaults to None.
Returns:
dict|None: Result object containing a message and any missing components.
"""
report = Report()
logger.debug(type(sub))
# What type is sub?
# reagenttypes = []
match sub:
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 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
for reagent in sub.reagents:
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 = ReagentType.query(kit_type=sub.extraction_kit, reagent=reagent)
logger.debug(f"Got reagent type: {rt}")
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]}")
reagenttypes.append(reagent.type[0].name)
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 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.")
logger.debug(f"Kit reagents: {ext_kit_rtypes}")
logger.debug(f"Submission reagents: {reagenttypes}")
# check if lists are equal
check = set(ext_kit_rtypes) == set(reagenttypes)
logger.debug(f"Checking if reagents match kit contents: {check}")
# what reagent types are in both lists?
missing = list(set(ext_kit_rtypes).difference(reagenttypes))
logger.debug(f"Missing reagents types: {missing}")
# if lists are equal return no problem
if len(missing)==0:
result = None
else:
result = Result(msg=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!", status="Warning")
report.add_result(result)
return report
def update_subsampassoc_with_pcr(submission:BasicSubmission, sample:BasicSample, input_dict:dict) -> dict|None:
"""
Inserts PCR results into wastewater submission/sample association
Args:
ctx (Settings): settings object passed down from gui
submission (models.BasicSubmission): Submission object
sample (models.BasicSample): Sample object
input_dict (dict): dictionary with info to be updated.
Returns:
dict|None: result object
"""
# assoc = lookup_submission_sample_association(ctx, submission=submission, sample=sample)
assoc = SubmissionSampleAssociation.query(submission=submission, sample=sample, limit=1)
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 = assoc.save()
return result

View File

@@ -4,9 +4,8 @@ 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 import logging, json
from operator import itemgetter from operator import itemgetter
import json
from . import BaseClass from . import BaseClass
from tools import setup_lookup, query_return from tools import setup_lookup, query_return
from datetime import date, datetime from datetime import date, datetime
@@ -51,6 +50,26 @@ class ControlType(BaseClass):
pass pass
return query_return(query=query, limit=limit) return query_return(query=query, limit=limit)
def get_subtypes(self, mode:str) -> List[str]:
"""
Get subtypes associated with this controltype
Args:
mode (str): analysis mode name
Returns:
List[str]: list of subtypes available
"""
outs = self.instances[0]
jsoner = json.loads(getattr(outs, mode))
logger.debug(f"JSON out: {jsoner.keys()}")
try:
genera = list(jsoner.keys())[0]
except IndexError:
return []
subtypes = [item for item in jsoner[genera] if "_hashes" not in item and "_ratio" not in item]
return subtypes
class Control(BaseClass): class Control(BaseClass):
""" """
Base class of a control sample. Base class of a control sample.
@@ -249,4 +268,3 @@ class Control(BaseClass):
def save(self): def save(self):
self.__database_session__.add(self) self.__database_session__.add(self)
self.__database_session__.commit() self.__database_session__.commit()

View File

@@ -183,15 +183,6 @@ class ReagentType(BaseClass):
# creator function: https://stackoverflow.com/questions/11091491/keyerror-when-adding-objects-to-sqlalchemy-association-object/11116291#11116291 # creator function: https://stackoverflow.com/questions/11091491/keyerror-when-adding-objects-to-sqlalchemy-association-object/11116291#11116291
kit_types = association_proxy("reagenttype_kit_associations", "kit_type", creator=lambda kit: KitTypeReagentTypeAssociation(kit_type=kit)) kit_types = association_proxy("reagenttype_kit_associations", "kit_type", creator=lambda kit: KitTypeReagentTypeAssociation(kit_type=kit))
# def __str__(self) -> str:
# """
# string representing this object
# Returns:
# str: string representing this object's name
# """
# return self.name
def __repr__(self): def __repr__(self):
return f"<ReagentType({self.name})>" return f"<ReagentType({self.name})>"
@@ -379,7 +370,17 @@ class Reagent(BaseClass):
name = Column(String(64)) #: reagent name name = Column(String(64)) #: reagent name
lot = Column(String(64)) #: lot number of reagent lot = Column(String(64)) #: lot number of reagent
expiry = Column(TIMESTAMP) #: expiry date - extended by eol_ext of parent programmatically expiry = Column(TIMESTAMP) #: expiry date - extended by eol_ext of parent programmatically
submissions = relationship("BasicSubmission", back_populates="reagents", uselist=True) #: submissions this reagent is used in # submissions = relationship("BasicSubmission", back_populates="reagents", uselist=True) #: submissions this reagent is used in
reagent_submission_associations = relationship(
"SubmissionReagentAssociation",
back_populates="reagent",
cascade="all, delete-orphan",
) #: Relation to SubmissionSampleAssociation
# association proxy of "user_keyword_associations" collection
# to "keyword" attribute
submissions = association_proxy("reagent_submission_associations", "submission") #: Association proxy to SubmissionSampleAssociation.samples
def __repr__(self): def __repr__(self):
if self.name != None: if self.name != None:
@@ -706,3 +707,69 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
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 query_return(query=query, limit=limit) return query_return(query=query, limit=limit)
class SubmissionReagentAssociation(BaseClass):
__tablename__ = "_reagents_submissions"
reagent_id = Column(INTEGER, ForeignKey("_reagents.id"), primary_key=True) #: id of associated sample
submission_id = Column(INTEGER, ForeignKey("_submissions.id"), primary_key=True)
comments = Column(String(1024))
submission = relationship("BasicSubmission", back_populates="submission_reagent_associations") #: associated submission
reagent = relationship(Reagent, back_populates="reagent_submission_associations")
def __repr__(self):
return f"<{self.submission.rsl_plate_num}&{self.reagent.lot}>"
def __init__(self, reagent=None, submission=None):
self.reagent = reagent
self.submission = submission
self.comments = ""
@classmethod
@setup_lookup
def query(cls,
submission:"BasicSubmission"|str|int|None=None,
reagent:Reagent|str|None=None,
limit:int=0) -> SubmissionReagentAssociation|List[SubmissionReagentAssociation]:
"""
Lookup SubmissionReagentAssociations of interest.
Args:
submission (BasicSubmission&quot; | str | int | None, optional): Identifier of joined submission. Defaults to None.
reagent (Reagent | str | None, optional): Identifier of joined reagent. Defaults to None.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
Returns:
SubmissionReagentAssociation|List[SubmissionReagentAssociation]: SubmissionReagentAssociation(s) of interest
"""
from . import BasicSubmission
query: Query = cls.__database_session__.query(cls)
match reagent:
case Reagent():
query = query.filter(cls.reagent==reagent)
case str():
# logger.debug(f"Filtering query with reagent: {reagent}")
reagent = Reagent.query(lot_number=reagent)
query = query.filter(cls.reagent==reagent)
# logger.debug([item.reagent.lot for item in query.all()])
# query = query.join(Reagent).filter(Reagent.lot==reagent)
case _:
pass
# logger.debug(f"Result of query after reagent: {query.all()}")
match submission:
case BasicSubmission():
query = query.filter(cls.submission==submission)
case str():
query = query.join(BasicSubmission).filter(BasicSubmission.rsl_plate_num==submission)
case int():
query = query.join(BasicSubmission).filter(BasicSubmission.id==submission)
case _:
pass
# logger.debug(f"Result of query after submission: {query.all()}")
# limit = query.count()
return query_return(query=query, limit=limit)

View File

@@ -6,13 +6,13 @@ from getpass import getuser
import math, json, logging, uuid, tempfile, re, yaml import math, json, logging, uuid, tempfile, re, yaml
from pprint import pformat from pprint import pformat
from . import Reagent, SubmissionType, KitType, Organization from . import Reagent, SubmissionType, KitType, Organization
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, Table, JSON, FLOAT, case from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case
from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.orm import relationship, validates, Query
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
import pandas as pd import pandas as pd
from openpyxl import Workbook from openpyxl import Workbook
from . import Base, BaseClass from . import BaseClass
from tools import check_not_nan, row_map, query_return, setup_lookup from tools import check_not_nan, row_map, query_return, setup_lookup
from datetime import datetime, date from datetime import datetime, date
from typing import List from typing import List
@@ -24,15 +24,6 @@ from pathlib import Path
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
# table containing reagents/submission relationships
reagents_submissions = Table(
"_reagents_submissions",
Base.metadata,
Column("reagent_id", INTEGER, ForeignKey("_reagents.id")),
Column("submission_id", INTEGER, ForeignKey("_submissions.id")),
extend_existing = True
)
class BasicSubmission(BaseClass): class BasicSubmission(BaseClass):
""" """
Concrete of basic submission which polymorphs into BacterialCulture and Wastewater Concrete of basic submission which polymorphs into BacterialCulture and Wastewater
@@ -51,7 +42,7 @@ class BasicSubmission(BaseClass):
submission_type_name = Column(String, ForeignKey("_submission_types.name", ondelete="SET NULL", name="fk_BS_subtype_name")) #: name of joined submission type submission_type_name = Column(String, ForeignKey("_submission_types.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 = relationship("Reagent", back_populates="submissions", secondary=reagents_submissions) #: relationship to reagents
reagents_id = Column(String, ForeignKey("_reagents.id", ondelete="SET NULL", name="fk_BS_reagents_id")) #: id of used reagents reagents_id = Column(String, ForeignKey("_reagents.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.
pcr_info = Column(JSON) #: unstructured output from pcr table logger or user(Artic) pcr_info = Column(JSON) #: unstructured output from pcr table logger or user(Artic)
@@ -69,6 +60,15 @@ class BasicSubmission(BaseClass):
# to "keyword" attribute # to "keyword" attribute
samples = association_proxy("submission_sample_associations", "sample") #: Association proxy to SubmissionSampleAssociation.samples samples = association_proxy("submission_sample_associations", "sample") #: Association proxy to SubmissionSampleAssociation.samples
submission_reagent_associations = relationship(
"SubmissionReagentAssociation",
back_populates="submission",
cascade="all, delete-orphan",
) #: Relation to SubmissionSampleAssociation
# association proxy of "user_keyword_associations" collection
# to "keyword" attribute
reagents = association_proxy("submission_reagent_associations", "reagent") #: Association proxy to SubmissionSampleAssociation.samples
# Allows for subclassing into ex. BacterialCulture, Wastewater, etc. # Allows for subclassing into ex. BacterialCulture, Wastewater, etc.
__mapper_args__ = { __mapper_args__ = {
"polymorphic_identity": "Basic Submission", "polymorphic_identity": "Basic Submission",
@@ -438,6 +438,22 @@ class BasicSubmission(BaseClass):
""" """
return "{{ rsl_plate_num }}" return "{{ rsl_plate_num }}"
@classmethod
def submissions_to_df(cls, submission_type:str|None=None, limit:int=0) -> pd.DataFrame:
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 cls.query(submission_type=submission_type, limit=limit)]
logger.debug(f"Got {len(subs)} submissions.")
df = pd.DataFrame.from_records(subs)
# Exclude sub information
for item in ['controls', 'extraction_info', 'pcr_info', 'comment', 'comments', 'samples', 'reagents']:
try:
df = df.drop(item, axis=1)
except:
logger.warning(f"Couldn't drop '{item}' column from submissionsheet df.")
return df
def set_attribute(self, key:str, value): def set_attribute(self, key:str, value):
""" """
Performs custom attribute setting based on values. Performs custom attribute setting based on values.
@@ -479,6 +495,11 @@ class BasicSubmission(BaseClass):
field_value = value field_value = value
case "ctx" | "csv" | "filepath": case "ctx" | "csv" | "filepath":
return return
case "comment":
if value == "" or value == None or value == 'null':
field_value = None
else:
field_value = dict(name="submitter", text=value, time=datetime.now())
case _: case _:
field_value = value field_value = value
# insert into field # insert into field
@@ -595,7 +616,8 @@ class BasicSubmission(BaseClass):
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,
reagent:Reagent|str|None=None, reagent:Reagent|str|None=None,
chronologic:bool=False, limit:int=0, chronologic:bool=False,
limit:int=0,
**kwargs **kwargs
) -> BasicSubmission | List[BasicSubmission]: ) -> BasicSubmission | List[BasicSubmission]:
""" """

View File

@@ -9,9 +9,8 @@ import numpy as np
from pathlib import Path from pathlib import Path
from backend.db.models import * from backend.db.models import *
from backend.validators import PydSubmission, PydReagent, RSLNamer, PydSample from backend.validators import PydSubmission, PydReagent, RSLNamer, PydSample
import logging import logging, re
from collections import OrderedDict from collections import OrderedDict
import re
from datetime import date from datetime import date
from dateutil.parser import parse, ParserError from dateutil.parser import parse, ParserError
from tools import check_not_nan, convert_nans_to_nones, Settings from tools import check_not_nan, convert_nans_to_nones, Settings
@@ -256,8 +255,12 @@ class ReagentParser(object):
name = df.iat[relevant[item]['name']['row']-1, relevant[item]['name']['column']-1] name = df.iat[relevant[item]['name']['row']-1, relevant[item]['name']['column']-1]
lot = df.iat[relevant[item]['lot']['row']-1, relevant[item]['lot']['column']-1] lot = df.iat[relevant[item]['lot']['row']-1, relevant[item]['lot']['column']-1]
expiry = df.iat[relevant[item]['expiry']['row']-1, relevant[item]['expiry']['column']-1] expiry = df.iat[relevant[item]['expiry']['row']-1, relevant[item]['expiry']['column']-1]
if 'comment' in relevant[item].keys():
comment = df.iat[relevant[item]['comment']['row']-1, relevant[item]['comment']['column']-1]
else:
comment = ""
except (KeyError, IndexError): except (KeyError, IndexError):
listo.append(PydReagent(type=item.strip(), lot=None, expiry=None, name=None, missing=True)) listo.append(PydReagent(type=item.strip(), lot=None, expiry=None, name=None, comment="", missing=True))
continue continue
# If the cell is blank tell the PydReagent # If the cell is blank tell the PydReagent
if check_not_nan(lot): if check_not_nan(lot):
@@ -266,8 +269,8 @@ class ReagentParser(object):
missing = True missing = True
# logger.debug(f"Got lot for {item}-{name}: {lot} as {type(lot)}") # logger.debug(f"Got lot for {item}-{name}: {lot} as {type(lot)}")
lot = str(lot) lot = str(lot)
logger.debug(f"Going into pydantic: name: {name}, lot: {lot}, expiry: {expiry}, type: {item.strip()}") logger.debug(f"Going into pydantic: name: {name}, lot: {lot}, expiry: {expiry}, type: {item.strip()}, comment: {comment}")
listo.append(PydReagent(type=item.strip(), lot=lot, expiry=expiry, name=name, missing=missing)) listo.append(PydReagent(type=item.strip(), lot=lot, expiry=expiry, name=name, comment=comment, missing=missing))
# logger.debug(f"Returning listo: {listo}") # logger.debug(f"Returning listo: {listo}")
return listo return listo

View File

@@ -2,9 +2,8 @@
Contains functions for generating summary reports Contains functions for generating summary reports
''' '''
from pandas import DataFrame from pandas import DataFrame
import logging import logging, re
from datetime import date, timedelta from datetime import date, timedelta
import re
from typing import List, Tuple from typing import List, Tuple
from tools import jinja_template_loading, Settings from tools import jinja_template_loading, Settings

View File

@@ -3,7 +3,6 @@ from pathlib import Path
from openpyxl import load_workbook from openpyxl import load_workbook
from backend.db.models import BasicSubmission, SubmissionType from backend.db.models import BasicSubmission, SubmissionType
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
class RSLNamer(object): class RSLNamer(object):

View File

@@ -2,7 +2,7 @@
Contains pydantic models and accompanying validators Contains pydantic models and accompanying validators
''' '''
from operator import attrgetter from operator import attrgetter
import uuid import uuid, re, logging
from pydantic import BaseModel, field_validator, Field from pydantic import BaseModel, field_validator, Field
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from dateutil.parser import parse from dateutil.parser import parse
@@ -10,8 +10,6 @@ from dateutil.parser._parser import ParserError
from typing import List, Any, Tuple from typing import List, Any, Tuple
from . import RSLNamer from . import RSLNamer
from pathlib import Path from pathlib import Path
import re
import logging
from tools import check_not_nan, convert_nans_to_nones, jinja_template_loading, Report, Result, row_map from tools import check_not_nan, convert_nans_to_nones, jinja_template_loading, Report, Result, row_map
from backend.db.models import * from backend.db.models import *
from sqlalchemy.exc import StatementError, IntegrityError from sqlalchemy.exc import StatementError, IntegrityError
@@ -28,6 +26,14 @@ class PydReagent(BaseModel):
expiry: date|None expiry: date|None
name: str|None name: str|None
missing: bool = Field(default=True) missing: bool = Field(default=True)
comment: str|None = Field(default="", validate_default=True)
@field_validator('comment', mode='before')
@classmethod
def create_comment(cls, value):
if value == None:
return ""
return value
@field_validator("type", mode='before') @field_validator("type", mode='before')
@classmethod @classmethod
@@ -88,7 +94,7 @@ class PydReagent(BaseModel):
else: else:
return values.data['type'] return values.data['type']
def toSQL(self) -> Tuple[Reagent, Report]: def toSQL(self, submission:BasicSubmission|str=None) -> Tuple[Reagent, Report]:
""" """
Converts this instance into a backend.db.models.kit.Reagent instance Converts this instance into a backend.db.models.kit.Reagent instance
@@ -96,6 +102,8 @@ class PydReagent(BaseModel):
Tuple[Reagent, Report]: Reagent instance and result of function Tuple[Reagent, Report]: Reagent instance and result of function
""" """
report = Report() report = Report()
if self.model_extra != None:
self.__dict__.update(self.model_extra)
logger.debug(f"Reagent SQL constructor is looking up type: {self.type}, lot: {self.lot}") logger.debug(f"Reagent SQL constructor is looking up type: {self.type}, lot: {self.lot}")
reagent = Reagent.query(lot_number=self.lot) reagent = Reagent.query(lot_number=self.lot)
logger.debug(f"Result: {reagent}") logger.debug(f"Result: {reagent}")
@@ -117,6 +125,11 @@ class PydReagent(BaseModel):
reagent.type.append(reagent_type) reagent.type.append(reagent_type)
case "name": case "name":
reagent.name = value reagent.name = value
case "comment":
continue
assoc = SubmissionReagentAssociation(reagent=reagent, submission=submission)
assoc.comments = self.comment
reagent.reagent_submission_associations.append(assoc)
# add end-of-life extension from reagent type to expiry date # add end-of-life extension from reagent type to expiry date
# NOTE: this will now be done only in the reporting phase to account for potential changes in end-of-life extensions # NOTE: this will now be done only in the reporting phase to account for potential changes in end-of-life extensions
return reagent, report return reagent, report
@@ -203,9 +216,17 @@ class PydSubmission(BaseModel, extra='allow'):
extraction_kit: dict|None extraction_kit: dict|None
technician: dict|None technician: dict|None
submission_category: dict|None = Field(default=dict(value=None, missing=True), validate_default=True) submission_category: dict|None = Field(default=dict(value=None, missing=True), validate_default=True)
comment: dict|None = Field(default=dict(value="", missing=True), validate_default=True)
reagents: List[dict]|List[PydReagent] = [] reagents: List[dict]|List[PydReagent] = []
samples: List[Any] samples: List[Any]
@field_validator('comment', mode='before')
@classmethod
def create_comment(cls, value):
if value == None:
return ""
return value
@field_validator("submitter_plate_num") @field_validator("submitter_plate_num")
@classmethod @classmethod
def enforce_with_uuid(cls, value): def enforce_with_uuid(cls, value):
@@ -218,13 +239,19 @@ class PydSubmission(BaseModel, extra='allow'):
@field_validator("submitted_date", mode="before") @field_validator("submitted_date", mode="before")
@classmethod @classmethod
def rescue_date(cls, value): def rescue_date(cls, value):
if value == None: logger.debug(f"\n\nDate coming into pydantic: {value}\n\n")
try:
check = value['value'] == None
except TypeError:
check = True
if check:
return dict(value=date.today(), missing=True) return dict(value=date.today(), missing=True)
return value return value
@field_validator("submitted_date") @field_validator("submitted_date")
@classmethod @classmethod
def strip_datetime_string(cls, value): def strip_datetime_string(cls, value):
if isinstance(value['value'], datetime): if isinstance(value['value'], datetime):
return value return value
if isinstance(value['value'], date): if isinstance(value['value'], date):
@@ -307,7 +334,6 @@ class PydSubmission(BaseModel, extra='allow'):
@field_validator("submission_type", mode='before') @field_validator("submission_type", mode='before')
@classmethod @classmethod
def make_submission_type(cls, value, values): def make_submission_type(cls, value, values):
logger.debug(f"Submission type coming into pydantic: {value}")
if not isinstance(value, dict): if not isinstance(value, dict):
value = {"value": value} value = {"value": value}
if check_not_nan(value['value']): if check_not_nan(value['value']):
@@ -331,8 +357,8 @@ class PydSubmission(BaseModel, extra='allow'):
def handle_duplicate_samples(self): def handle_duplicate_samples(self):
""" """
Collapses multiple samples with same submitter id into one with lists for rows, columns Collapses multiple samples with same submitter id into one with lists for rows, columns.
TODO: Find out if this is really necessary Necessary to prevent trying to create duplicate samples in SQL creation.
""" """
submitter_ids = list(set([sample.submitter_id for sample in self.samples])) submitter_ids = list(set([sample.submitter_id for sample in self.samples]))
output = [] output = []
@@ -581,9 +607,37 @@ class PydSubmission(BaseModel, extra='allow'):
logger.debug(f"Template rendered as: {render}") logger.debug(f"Template rendered as: {render}")
return render return render
def check_kit_integrity(self, reagenttypes:list=[]) -> Report:
"""
Ensures all reagents expected in kit are listed in Submission
Args:
reagenttypes (list | None, optional): List to check against complete list. Defaults to None.
Returns:
Report: Result object containing a message and any missing components.
"""
report = Report()
ext_kit = KitType.query(name=self.extraction_kit['value'])
ext_kit_rtypes = [item.name for item in ext_kit.get_reagents(required=True, submission_type=self.submission_type['value'])]
reagenttypes = [item.type for item in self.reagents]
logger.debug(f"Kit reagents: {ext_kit_rtypes}")
logger.debug(f"Submission reagents: {reagenttypes}")
# check if lists are equal
check = set(ext_kit_rtypes) == set(reagenttypes)
logger.debug(f"Checking if reagents match kit contents: {check}")
# what reagent types are in both lists?
missing = list(set(ext_kit_rtypes).difference(reagenttypes))
logger.debug(f"Missing reagents types: {missing}")
# if lists are equal return no problem
if len(missing)==0:
result = None
else:
result = Result(msg=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!", status="Warning")
report.add_result(result)
return report
class PydContact(BaseModel): class PydContact(BaseModel):
name: str name: str
phone: str|None phone: str|None
email: str|None email: str|None

View File

@@ -194,73 +194,6 @@ def construct_chart(df:pd.DataFrame, modes:list, ytitle:str|None=None) -> Figure
fig.add_traces(bar.data) fig.add_traces(bar.data)
return generic_figure_markers(fig=fig, modes=modes, ytitle=ytitle) return generic_figure_markers(fig=fig, modes=modes, ytitle=ytitle)
# Below are the individual construction functions. They must be named "construct_{mode}_chart" and
# take only json_in and mode to hook into the main processor.
# def construct_refseq_chart(df:pd.DataFrame, group_name:str, mode:str) -> Figure:
# """
# Constructs intial refseq chart for both contains and matches (depreciated).
# Args:
# df (pd.DataFrame): dataframe containing all sample data for the group.
# group_name (str): name of the group being processed.
# mode (str): contains or matches, overwritten by hardcoding, so don't think about it too hard.
# Returns:
# Figure: initial figure with contains and matches traces.
# """
# # This overwrites the mode from the signature, might get confusing.
# fig = Figure()
# modes = ['contains', 'matches']
# for ii, mode in enumerate(modes):
# bar = px.bar(df, x="submitted_date",
# y=f"{mode}_ratio",
# color="target",
# title=f"{group_name}_{mode}",
# barmode='stack',
# hover_data=["genus", "name", f"{mode}_hashes"],
# text="genera"
# )
# bar.update_traces(visible = ii == 0)
# # Plotly express returns a full figure, so we have to use the data from that figure only.
# fig.add_traces(bar.data)
# # sys.exit(f"number of traces={len(fig.data)}")
# return generic_figure_markers(fig=fig, modes=modes)
# def construct_kraken_chart(settings:dict, df:pd.DataFrame, group_name:str, mode:str) -> Figure:
# """
# Constructs intial refseq chart for each mode in the kraken config settings. (depreciated)
# Args:
# settings (dict): settings passed down from click.
# df (pd.DataFrame): dataframe containing all sample data for the group.
# group_name (str): name of the group being processed.
# mode (str): kraken modes retrieved from config file by setup.
# Returns:
# Figure: initial figure with traces for modes
# """
# df[f'{mode}_count'] = pd.to_numeric(df[f'{mode}_count'],errors='coerce')
# df = df.groupby('submitted_date')[f'{mode}_count'].nlargest(2)
# # The actual percentage from kraken was off due to exclusion of NaN, recalculating.
# df[f'{mode}_percent'] = 100 * df[f'{mode}_count'] / df.groupby('submitted_date')[f'{mode}_count'].transform('sum')
# modes = settings['modes'][mode]
# # This overwrites the mode from the signature, might get confusing.
# fig = Figure()
# for ii, entry in enumerate(modes):
# bar = px.bar(df, x="submitted_date",
# y=entry,
# color="genus",
# title=f"{group_name}_{entry}",
# barmode="stack",
# hover_data=["genus", "name", "target"],
# text="genera",
# )
# bar.update_traces(visible = ii == 0)
# fig.add_traces(bar.data)
# return generic_figure_markers(fig=fig, modes=modes)
def divide_chunks(input_list:list, chunk_count:int): def divide_chunks(input_list:list, chunk_count:int):
""" """
Divides a list into {chunk_count} equal parts Divides a list into {chunk_count} equal parts

View File

@@ -1,9 +1,8 @@
from pathlib import Path from pathlib import Path
import sys
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
import numpy as np import numpy as np
from tools import check_if_app, jinja_template_loading from tools import check_if_app, jinja_template_loading
import logging import logging, sys
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")

View File

@@ -1,8 +1,6 @@
''' '''
Constructs main application. Constructs main application.
TODO: Complete.
''' '''
import sys
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QTabWidget, QWidget, QVBoxLayout, QTabWidget, QWidget, QVBoxLayout,
QHBoxLayout, QScrollArea, QMainWindow, QHBoxLayout, QScrollArea, QMainWindow,
@@ -14,14 +12,12 @@ from backend.validators import PydReagent
from tools import check_if_app, Settings, Report from tools import check_if_app, Settings, Report
from .pop_ups import AlertPop from .pop_ups import AlertPop
from .misc import AddReagentForm, LogParser from .misc import AddReagentForm, LogParser
import logging import logging, webbrowser, sys
from datetime import date from datetime import date
import webbrowser
from .submission_table import SubmissionsSheet from .submission_table import SubmissionsSheet
from .submission_widget import SubmissionFormContainer from .submission_widget import SubmissionFormContainer
from .controls_chart import ControlsViewer from .controls_chart import ControlsViewer
from .kit_creator import KitAdder from .kit_creator import KitAdder
import webbrowser
logger = logging.getLogger(f'submissions.{__name__}') logger = logging.getLogger(f'submissions.{__name__}')
logger.info("Hello, I am a logger") logger.info("Hello, I am a logger")

View File

@@ -3,8 +3,8 @@ from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QComboBox, QHBoxLayout, QWidget, QVBoxLayout, QComboBox, QHBoxLayout,
QDateEdit, QLabel, QSizePolicy QDateEdit, QLabel, QSizePolicy
) )
from PyQt6.QtCore import QSignalBlocker, QLoggingCategory from PyQt6.QtCore import QSignalBlocker
from backend.db import ControlType, Control, get_control_subtypes from backend.db import ControlType, Control#, get_control_subtypes
from PyQt6.QtCore import QDate, QSize from PyQt6.QtCore import QDate, QSize
import logging, sys import logging, sys
from tools import Report, Result from tools import Report, Result
@@ -88,7 +88,8 @@ class ControlsViewer(QWidget):
self.mode = self.mode_typer.currentText() self.mode = self.mode_typer.currentText()
self.sub_typer.clear() self.sub_typer.clear()
# lookup subtypes # lookup subtypes
sub_types = get_control_subtypes(type=self.con_type, mode=self.mode) # sub_types = get_control_subtypes(type=self.con_type, mode=self.mode)
sub_types = ControlType.query(name=self.con_type).get_subtypes(mode=self.mode)
# sub_types = lookup_controls(ctx=obj.ctx, control_type=obj.con_type) # sub_types = lookup_controls(ctx=obj.ctx, control_type=obj.con_type)
if sub_types != []: if sub_types != []:
# block signal that will rerun controls getter and update sub_typer # block signal that will rerun controls getter and update sub_typer

View File

@@ -15,7 +15,6 @@ from PyQt6.QtWidgets import (
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
from PyQt6.QtGui import QAction, QCursor, QPixmap, QPainter from PyQt6.QtGui import QAction, QCursor, QPixmap, QPainter
from backend.db.functions import submissions_to_df
from backend.db.models import BasicSubmission from backend.db.models import BasicSubmission
from backend.excel import make_report_html, make_report_xlsx from backend.excel import make_report_html, make_report_xlsx
from tools import check_if_app, Report, Result, jinja_template_loading, get_first_blank_df_row, row_map from tools import check_if_app, Report, Result, jinja_template_loading, get_first_blank_df_row, row_map
@@ -102,7 +101,8 @@ class SubmissionsSheet(QTableView):
""" """
sets data in model sets data in model
""" """
self.data = submissions_to_df() # self.data = submissions_to_df()
self.data = BasicSubmission.submissions_to_df()
try: try:
self.data['id'] = self.data['id'].apply(str) self.data['id'] = self.data['id'].apply(str)
self.data['id'] = self.data['id'].str.zfill(3) self.data['id'] = self.data['id'].str.zfill(3)

View File

@@ -5,23 +5,20 @@ from PyQt6.QtWidgets import (
from PyQt6.QtCore import pyqtSignal from PyQt6.QtCore import pyqtSignal
from pathlib import Path from pathlib import Path
from . import select_open_file, select_save_file from . import select_open_file, select_save_file
import logging import logging, difflib, inspect, json, sys
from pathlib import Path from pathlib import Path
from tools import Report, Result, check_not_nan from tools import Report, Result, check_not_nan
from backend.excel.parser import SheetParser, PCRParser from backend.excel.parser import SheetParser, PCRParser
from backend.validators import PydSubmission, PydReagent from backend.validators import PydSubmission, PydReagent
from backend.db import ( from backend.db import (
check_kit_integrity, KitType, Organization, SubmissionType, Reagent, KitType, Organization, SubmissionType, Reagent,
ReagentType, KitTypeReagentTypeAssociation, BasicSubmission ReagentType, KitTypeReagentTypeAssociation, BasicSubmission
) )
from pprint import pformat from pprint import pformat
from .pop_ups import QuestionAsker, AlertPop from .pop_ups import QuestionAsker, AlertPop
# from .misc import ReagentFormWidget # from .misc import ReagentFormWidget
from typing import List, Tuple from typing import List, Tuple
import difflib
from datetime import date from datetime import date
import inspect
import json
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -143,14 +140,14 @@ class SubmissionFormContainer(QWidget):
return return
except AttributeError: except AttributeError:
self.prsr = SheetParser(ctx=self.app.ctx, filepath=fname) self.prsr = SheetParser(ctx=self.app.ctx, filepath=fname)
try: # try:
logger.debug(f"Submission dictionary:\n{pformat(self.prsr.sub)}") logger.debug(f"Submission dictionary:\n{pformat(self.prsr.sub)}")
self.pyd = self.prsr.to_pydantic() self.pyd = self.prsr.to_pydantic()
logger.debug(f"Pydantic result: \n\n{pformat(self.pyd)}\n\n") logger.debug(f"Pydantic result: \n\n{pformat(self.pyd)}\n\n")
except Exception as e: # except Exception as e:
report.add_result(Result(msg=f"Problem creating pydantic model:\n\n{e}", status="Critical")) # report.add_result(Result(msg=f"Problem creating pydantic model:\n\n{e}", status="Critical"))
self.report.add_result(report) # self.report.add_result(report)
return # return
self.form = self.pyd.toForm(parent=self) self.form = self.pyd.toForm(parent=self)
self.layout().addWidget(self.form) self.layout().addWidget(self.form)
kit_widget = self.form.find_widgets(object_name="extraction_kit")[0].input kit_widget = self.form.find_widgets(object_name="extraction_kit")[0].input
@@ -265,7 +262,8 @@ class SubmissionFormContainer(QWidget):
self.pyd: PydSubmission = self.form.parse_form() self.pyd: PydSubmission = self.form.parse_form()
logger.debug(f"Submission: {pformat(self.pyd)}") logger.debug(f"Submission: {pformat(self.pyd)}")
logger.debug("Checking kit integrity...") logger.debug("Checking kit integrity...")
result = check_kit_integrity(sub=self.pyd) # result = check_kit_integrity(sub=self.pyd)
result = self.pyd.check_kit_integrity()
report.add_result(result) report.add_result(result)
if len(result.results) > 0: if len(result.results) > 0:
self.report.add_result(report) self.report.add_result(report)
@@ -417,7 +415,8 @@ class SubmissionFormWidget(QWidget):
# self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer", # self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
# "qt_scrollarea_vcontainer", "submit_btn" # "qt_scrollarea_vcontainer", "submit_btn"
# ] # ]
self.ignore = ['filepath', 'samples', 'reagents', 'csv', 'ctx'] self.ignore = ['filepath', 'samples', 'reagents', 'csv', 'ctx', 'comment']
self.recover = ['filepath', 'samples', 'csv', 'comment']
layout = QVBoxLayout() layout = QVBoxLayout()
for k, v in kwargs.items(): for k, v in kwargs.items():
if k not in self.ignore: if k not in self.ignore:
@@ -448,8 +447,6 @@ class SubmissionFormWidget(QWidget):
logger.debug(f"Hello from form parser!") logger.debug(f"Hello from form parser!")
info = {} info = {}
reagents = [] reagents = []
if hasattr(self, 'csv'):
info['csv'] = self.csv
for widget in self.findChildren(QWidget): for widget in self.findChildren(QWidget):
# logger.debug(f"Parsed widget of type {type(widget)}") # logger.debug(f"Parsed widget of type {type(widget)}")
match widget: match widget:
@@ -463,9 +460,13 @@ class SubmissionFormWidget(QWidget):
info[field] = value info[field] = value
logger.debug(f"Info: {pformat(info)}") logger.debug(f"Info: {pformat(info)}")
logger.debug(f"Reagents: {pformat(reagents)}") logger.debug(f"Reagents: {pformat(reagents)}")
# sys.exit() # logger.debug(f"Attrs not in info: {[k for k, v in self.__dict__.items() if k not in info.keys()]}")
for item in self.recover:
if hasattr(self, item):
info[item] = getattr(self, item)
# app = self.parent().parent().parent().parent().parent().parent().parent().parent # app = self.parent().parent().parent().parent().parent().parent().parent().parent
submission = PydSubmission(filepath=self.filepath, reagents=reagents, samples=self.samples, **info) # submission = PydSubmission(filepath=self.filepath, reagents=reagents, samples=self.samples, **info)
submission = PydSubmission(reagents=reagents, **info)
return submission return submission
class InfoItem(QWidget): class InfoItem(QWidget):

View File

@@ -5,12 +5,9 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
import re import re
import numpy as np import numpy as np
import logging import logging, re, yaml, sys, os, stat, platform, getpass, inspect
import pandas as pd import pandas as pd
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
import yaml
import sys, os, stat, platform, getpass
import logging
from logging import handlers from logging import handlers
from pathlib import Path from pathlib import Path
from sqlalchemy.orm import Query, Session from sqlalchemy.orm import Query, Session
@@ -18,8 +15,6 @@ from sqlalchemy import create_engine
from pydantic import field_validator, BaseModel, Field from pydantic import field_validator, BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Any, Tuple, Literal, List from typing import Any, Tuple, Literal, List
import inspect
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")