Second round of code cleanup.

This commit is contained in:
lwark
2024-10-30 07:34:39 -05:00
parent 1f83b61c81
commit ba1b3e5cf3
19 changed files with 176 additions and 110 deletions

View File

@@ -1,3 +1,7 @@
## 202410.05
- Code clean up.
## 202410.03 ## 202410.03
- Added code for cataloging of PCR controls. - Added code for cataloging of PCR controls.

Binary file not shown.

View File

@@ -28,6 +28,8 @@ class BaseClass(Base):
__table_args__ = {'extend_existing': True} #: Will only add new columns __table_args__ = {'extend_existing': True} #: Will only add new columns
singles = ['id']
@classmethod @classmethod
@declared_attr @declared_attr
def __tablename__(cls) -> str: def __tablename__(cls) -> str:
@@ -92,17 +94,21 @@ class BaseClass(Base):
Returns: Returns:
dict | list | str: Output of key:value dict or single (list, str) desired variable dict | list | str: Output of key:value dict or single (list, str) desired variable
""" """
dicto = dict(singles=['id']) # if issubclass(cls, BaseClass) and cls.__name__ != "BaseClass":
output = {} singles = list(set(cls.singles + BaseClass.singles))
for k, v in dicto.items(): # else:
if len(args) > 0 and k not in args: # singles = cls.singles
# logger.debug(f"{k} not selected as being of interest.") # output = dict(singles=singles)
continue # output = {}
else: # for k, v in dicto.items():
output[k] = v # if len(args) > 0 and k not in args:
if len(args) == 1: # # logger.debug(f"{k} not selected as being of interest.")
return output[args[0]] # continue
return output # else:
# output[k] = v
# if len(args) == 1:
# return output[args[0]]
return dict(singles=singles)
@classmethod @classmethod
def query(cls, **kwargs) -> Any | List[Any]: def query(cls, **kwargs) -> Any | List[Any]:
@@ -190,10 +196,15 @@ class ConfigItem(BaseClass):
Returns: Returns:
ConfigItem|List[ConfigItem]: Config item(s) ConfigItem|List[ConfigItem]: Config item(s)
""" """
config_items = cls.__database_session__.query(cls).all() query = cls.__database_session__.query(cls)
config_items = [item for item in config_items if item.key in args] # config_items = [item for item in config_items if item.key in args]
if len(args) == 1: match len(args):
config_items = config_items[0] case 0:
config_items = query.all()
case 1:
config_items = query.filter(cls.key == args[0]).first()
case _:
config_items = query.filter(cls.key.in_(args)).all()
return config_items return config_items

View File

@@ -131,10 +131,8 @@ class Control(BaseClass):
__mapper_args__ = { __mapper_args__ = {
"polymorphic_identity": "Basic Control", "polymorphic_identity": "Basic Control",
"polymorphic_on": case( "polymorphic_on": case(
(controltype_name == "PCR Control", "PCR Control"), (controltype_name == "PCR Control", "PCR Control"),
(controltype_name == "Irida Control", "Irida Control"), (controltype_name == "Irida Control", "Irida Control"),
else_="Basic Control" else_="Basic Control"
), ),
"with_polymorphic": "*", "with_polymorphic": "*",
@@ -147,15 +145,15 @@ class Control(BaseClass):
def find_polymorphic_subclass(cls, polymorphic_identity: str | ControlType | None = None, def find_polymorphic_subclass(cls, polymorphic_identity: str | ControlType | None = None,
attrs: dict | None = None) -> Control: attrs: dict | None = None) -> Control:
""" """
Find subclass based on polymorphic identity or relevant attributes. Find subclass based on polymorphic identity or relevant attributes.
Args: Args:
polymorphic_identity (str | None, optional): String representing polymorphic identity. Defaults to None. polymorphic_identity (str | None, optional): String representing polymorphic identity. Defaults to None.
attrs (str | SubmissionType | None, optional): Attributes of the relevant class. Defaults to None. attrs (str | SubmissionType | None, optional): Attributes of the relevant class. Defaults to None.
Returns: Returns:
Control: Subclass of interest. Control: Subclass of interest.
""" """
if isinstance(polymorphic_identity, dict): if isinstance(polymorphic_identity, dict):
# logger.debug(f"Controlling for dict value") # logger.debug(f"Controlling for dict value")
polymorphic_identity = polymorphic_identity['value'] polymorphic_identity = polymorphic_identity['value']
@@ -189,14 +187,11 @@ class Control(BaseClass):
Args: Args:
parent (QWidget): chart holding widget to add buttons to. parent (QWidget): chart holding widget to add buttons to.
Returns:
""" """
pass return None
@classmethod @classmethod
def make_chart(cls, parent, chart_settings: dict, ctx): def make_chart(cls, parent, chart_settings: dict, ctx) -> Tuple[Report, "CustomFigure" | None]:
""" """
Dummy operation to be overridden by child classes. Dummy operation to be overridden by child classes.
@@ -307,6 +302,7 @@ class PCRControl(Control):
return cls.execute_query(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
@classmethod @classmethod
@report_result
def make_chart(cls, parent, chart_settings: dict, ctx: Settings) -> Tuple[Report, "PCRFigure"]: def make_chart(cls, parent, chart_settings: dict, ctx: Settings) -> Tuple[Report, "PCRFigure"]:
""" """
Creates a PCRFigure. Overrides parent Creates a PCRFigure. Overrides parent

View File

@@ -4,6 +4,7 @@ All kit and reagent related models
from __future__ import annotations from __future__ import annotations
import datetime import datetime
import json import json
import sys
from pprint import pformat from pprint import pformat
import yaml import yaml
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB
@@ -693,6 +694,9 @@ class SubmissionType(BaseClass):
Returns: Returns:
List[str]: List of sheet names List[str]: List of sheet names
""" """
# print(f"Getting template file from {self.__database_session__.get_bind()}")
if "pytest" in sys.modules:
return ExcelFile("C:\\Users\lwark\Documents\python\submissions\mytests\\test_assets\RSL-AR-20240513-1.xlsx").sheet_names
return ExcelFile(BytesIO(self.template_file), engine="openpyxl").sheet_names return ExcelFile(BytesIO(self.template_file), engine="openpyxl").sheet_names
def set_template_file(self, filepath: Path | str): def set_template_file(self, filepath: Path | str):

View File

@@ -6,7 +6,7 @@ import sys
import types import types
from copy import deepcopy from copy import deepcopy
from getpass import getuser from getpass import getuser
import logging, uuid, tempfile, re, yaml, base64 import logging, uuid, tempfile, re, base64
from zipfile import ZipFile from zipfile import ZipFile
from tempfile import TemporaryDirectory, TemporaryFile from tempfile import TemporaryDirectory, TemporaryFile
from operator import itemgetter from operator import itemgetter
@@ -167,28 +167,24 @@ class BasicSubmission(BaseClass):
""" """
# NOTE: Create defaults for all submission_types # NOTE: Create defaults for all submission_types
parent_defs = super().get_default_info() # NOTE: Singles tells the query which fields to set limit to 1
dicto = super().get_default_info()
recover = ['filepath', 'samples', 'csv', 'comment', 'equipment'] recover = ['filepath', 'samples', 'csv', 'comment', 'equipment']
dicto = dict( dicto.update(dict(
details_ignore=['excluded', 'reagents', 'samples', details_ignore=['excluded', 'reagents', 'samples',
'extraction_info', 'comment', 'barcode', 'extraction_info', 'comment', 'barcode',
'platemap', 'export_map', 'equipment', 'tips', 'custom'], 'platemap', 'export_map', 'equipment', 'tips', 'custom'],
# NOTE: Fields not placed in ui form # NOTE: Fields not placed in ui form
form_ignore=['reagents', 'ctx', 'id', 'cost', 'extraction_info', 'signed_by', 'comment', 'namer', form_ignore=['reagents', 'ctx', 'id', 'cost', 'extraction_info', 'signed_by', 'comment', 'namer',
'submission_object', "tips", 'contact_phone', 'custom'] + recover, 'submission_object', "tips", 'contact_phone', 'custom', 'cost_centre'] + recover,
# NOTE: Fields not placed in ui form to be moved to pydantic # NOTE: Fields not placed in ui form to be moved to pydantic
form_recover=recover form_recover=recover
) ))
# NOTE: Singles tells the query which fields to set limit to 1
dicto['singles'] = parent_defs['singles']
# NOTE: Grab mode_sub_type specific info. # NOTE: Grab mode_sub_type specific info.
output = {} if args:
for k, v in dicto.items(): output = {k: v for k, v in dicto.items() if k in args}
if len(args) > 0 and k not in args: else:
# logger.debug(f"Don't want {k}") output = {k: v for k, v in dicto.items()}
continue
else:
output[k] = v
if isinstance(submission_type, SubmissionType): if isinstance(submission_type, SubmissionType):
st = submission_type st = submission_type
else: else:
@@ -198,7 +194,7 @@ class BasicSubmission(BaseClass):
else: else:
output['submission_type'] = st.name output['submission_type'] = st.name
for k, v in st.defaults.items(): for k, v in st.defaults.items():
if len(args) > 0 and k not in args: if args and k not in args:
# logger.debug(f"Don't want {k}") # logger.debug(f"Don't want {k}")
continue continue
else: else:
@@ -272,6 +268,7 @@ class BasicSubmission(BaseClass):
field = self.__getattribute__(name) field = self.__getattribute__(name)
except AttributeError: except AttributeError:
return None return None
# assert isinstance(field, list)
for item in field: for item in field:
if extra: if extra:
yield item.to_sub_dict(extra) yield item.to_sub_dict(extra)
@@ -1137,9 +1134,9 @@ class BasicSubmission(BaseClass):
limit = 1 limit = 1
case _: case _:
pass pass
if chronologic: # if chronologic:
logger.debug("Attempting sort by date descending") # logger.debug("Attempting sort by date descending")
query = query.order_by(cls.submitted_date.desc()) query = query.order_by(cls.submitted_date.desc())
if page_size is not None: if page_size is not None:
query = query.limit(page_size) query = query.limit(page_size)
page = page - 1 page = page - 1
@@ -2980,7 +2977,6 @@ class WastewaterArticAssociation(SubmissionSampleAssociation):
Returns: Returns:
dict: Updated dictionary with row, column and well updated dict: Updated dictionary with row, column and well updated
""" """
sample = super().to_sub_dict() sample = super().to_sub_dict()
sample['ct'] = self.ct sample['ct'] = self.ct
sample['source_plate'] = self.source_plate sample['source_plate'] = self.source_plate

View File

@@ -2,6 +2,7 @@
contains parser objects for pulling values from client generated submission sheets. contains parser objects for pulling values from client generated submission sheets.
''' '''
import json import json
import sys
from copy import copy from copy import copy
from getpass import getuser from getpass import getuser
from pprint import pformat from pprint import pformat
@@ -95,6 +96,7 @@ class SheetParser(object):
parser = ReagentParser(xl=self.xl, submission_type=self.submission_type, parser = ReagentParser(xl=self.xl, submission_type=self.submission_type,
extraction_kit=extraction_kit) extraction_kit=extraction_kit)
self.sub['reagents'] = parser.parse_reagents() self.sub['reagents'] = parser.parse_reagents()
logger.debug(f"Reagents out of parser: {pformat(self.sub['reagents'])}")
def parse_samples(self): def parse_samples(self):
""" """
@@ -273,7 +275,8 @@ class ReagentParser(object):
# logger.debug(f"Reagent Parser map: {self.map}") # logger.debug(f"Reagent Parser map: {self.map}")
self.xl = xl self.xl = xl
def fetch_kit_info_map(self, submission_type: str) -> dict: @report_result
def fetch_kit_info_map(self, submission_type: str) -> Tuple[Report, dict]:
""" """
Gets location of kit reagents from database Gets location of kit reagents from database
@@ -283,7 +286,7 @@ class ReagentParser(object):
Returns: Returns:
dict: locations of reagent info for the kit. dict: locations of reagent info for the kit.
""" """
report = Report()
if isinstance(submission_type, dict): if isinstance(submission_type, dict):
submission_type = submission_type['value'] submission_type = submission_type['value']
reagent_map = {k: v for k, v in self.kit_object.construct_xl_map_for_use(submission_type)} reagent_map = {k: v for k, v in self.kit_object.construct_xl_map_for_use(submission_type)}
@@ -291,7 +294,12 @@ class ReagentParser(object):
del reagent_map['info'] del reagent_map['info']
except KeyError: except KeyError:
pass pass
return reagent_map # logger.debug(f"Reagent map: {pformat(reagent_map)}")
if not reagent_map.keys():
report.add_result(Result(owner=__name__, code=0, msg=f"No kit map found for {self.kit_object.name}.\n\n"
f"Are you sure you used the right kit?",
status="Critical"))
return report, reagent_map
def parse_reagents(self) -> Generator[dict, None, None]: def parse_reagents(self) -> Generator[dict, None, None]:
""" """
@@ -401,6 +409,7 @@ class SampleParser(object):
""" """
invalids = [0, "0", "EMPTY"] invalids = [0, "0", "EMPTY"]
smap = self.sample_info_map['plate_map'] smap = self.sample_info_map['plate_map']
print(smap)
ws = self.xl[smap['sheet']] ws = self.xl[smap['sheet']]
plate_map_samples = [] plate_map_samples = []
for ii, row in enumerate(range(smap['start_row'], smap['end_row'] + 1), start=1): for ii, row in enumerate(range(smap['start_row'], smap['end_row'] + 1), start=1):
@@ -469,8 +478,10 @@ class SampleParser(object):
yield new yield new
else: else:
merge_on_id = self.sample_info_map['lookup_table']['merge_on_id'] merge_on_id = self.sample_info_map['lookup_table']['merge_on_id']
plate_map_samples = sorted(copy(self.plate_map_samples), key=lambda d: d['id']) # plate_map_samples = sorted(copy(self.plate_map_samples), key=lambda d: d['id'])
lookup_samples = sorted(copy(self.lookup_samples), key=lambda d: d[merge_on_id]) # lookup_samples = sorted(copy(self.lookup_samples), key=lambda d: d[merge_on_id])
plate_map_samples = sorted(copy(self.plate_map_samples), key=itemgetter('id'))
lookup_samples = sorted(copy(self.lookup_samples), key=itemgetter(merge_on_id))
for ii, psample in enumerate(plate_map_samples): for ii, psample in enumerate(plate_map_samples):
# NOTE: See if we can do this the easy way and just use the same list index. # NOTE: See if we can do this the easy way and just use the same list index.
try: try:
@@ -483,6 +494,8 @@ class SampleParser(object):
lookup_samples[ii] = {} lookup_samples[ii] = {}
else: else:
logger.warning(f"Match for {psample['id']} not direct, running search.") logger.warning(f"Match for {psample['id']} not direct, running search.")
searchables = [(jj, sample) for jj, sample in enumerate(lookup_samples)
if merge_on_id in sample.keys()]
# for jj, lsample in enumerate(lookup_samples): # for jj, lsample in enumerate(lookup_samples):
# try: # try:
# check = lsample[merge_on_id] == psample['id'] # check = lsample[merge_on_id] == psample['id']
@@ -494,14 +507,18 @@ class SampleParser(object):
# break # break
# else: # else:
# new = psample # new = psample
jj, new = next(((jj, lsample) for jj, lsample in enumerate(lookup_samples) if lsample[merge_on_id] == psample['id']), (-1, psample)) jj, new = next(((jj, lsample | psample) for jj, lsample in searchables
if lsample[merge_on_id] == psample['id']), (-1, psample))
logger.debug(f"Assigning from index {jj} - {new}") logger.debug(f"Assigning from index {jj} - {new}")
if jj >= 0: if jj >= 0:
lookup_samples[jj] = {} lookup_samples[jj] = {}
if not check_key_or_attr(key='submitter_id', interest=new, check_none=True): if not check_key_or_attr(key='submitter_id', interest=new, check_none=True):
new['submitter_id'] = psample['id'] new['submitter_id'] = psample['id']
new = self.sub_object.parse_samples(new) new = self.sub_object.parse_samples(new)
del new['id'] try:
del new['id']
except KeyError:
pass
yield new yield new
@@ -586,7 +603,7 @@ class EquipmentParser(object):
nickname=eq.nickname) nickname=eq.nickname)
except AttributeError: except AttributeError:
logger.error(f"Unable to add {eq} to list.") logger.error(f"Unable to add {eq} to list.")
class TipParser(object): class TipParser(object):
""" """
@@ -649,7 +666,7 @@ class TipParser(object):
yield dict(name=eq.name, role=k, lot=lot) yield dict(name=eq.name, role=k, lot=lot)
except AttributeError: except AttributeError:
logger.error(f"Unable to add {eq} to PydTips list.") logger.error(f"Unable to add {eq} to PydTips list.")
class PCRParser(object): class PCRParser(object):
"""Object to pull data from Design and Analysis PCR export file.""" """Object to pull data from Design and Analysis PCR export file."""
@@ -705,4 +722,3 @@ class PCRParser(object):
pcr['imported_by'] = getuser() pcr['imported_by'] = getuser()
# logger.debug(f"PCR: {pformat(pcr)}") # logger.debug(f"PCR: {pformat(pcr)}")
return pcr return pcr

View File

@@ -3,6 +3,7 @@ contains writer objects for pushing values to submission sheet templates.
""" """
import logging import logging
from copy import copy from copy import copy
from operator import itemgetter
from pprint import pformat from pprint import pformat
from typing import List, Generator, Tuple from typing import List, Generator, Tuple
from openpyxl import load_workbook, Workbook from openpyxl import load_workbook, Workbook
@@ -272,7 +273,8 @@ class SampleWriter(object):
self.sample_map = submission_type.construct_sample_map()['lookup_table'] self.sample_map = submission_type.construct_sample_map()['lookup_table']
# NOTE: exclude any samples without a submission rank. # NOTE: exclude any samples without a submission rank.
samples = [item for item in self.reconcile_map(sample_list) if item['submission_rank'] > 0] samples = [item for item in self.reconcile_map(sample_list) if item['submission_rank'] > 0]
self.samples = sorted(samples, key=lambda k: k['submission_rank']) # self.samples = sorted(samples, key=lambda k: k['submission_rank'])
self.samples = sorted(samples, key=itemgetter('submission_rank'))
def reconcile_map(self, sample_list: list) -> Generator[dict, None, None]: def reconcile_map(self, sample_list: list) -> Generator[dict, None, None]:
""" """

View File

@@ -11,7 +11,7 @@ from dateutil.parser import ParserError
from typing import List, Tuple, Literal from typing import List, Tuple, Literal
from . import RSLNamer from . import RSLNamer
from pathlib import Path from pathlib import Path
from tools import check_not_nan, convert_nans_to_nones, Report, Result from tools import check_not_nan, convert_nans_to_nones, Report, Result, timezone
from backend.db.models import * from backend.db.models import *
from sqlalchemy.exc import StatementError, IntegrityError from sqlalchemy.exc import StatementError, IntegrityError
from PyQt6.QtWidgets import QWidget from PyQt6.QtWidgets import QWidget
@@ -148,7 +148,9 @@ class PydReagent(BaseModel):
case "expiry": case "expiry":
if isinstance(value, str): if isinstance(value, str):
value = date(year=1970, month=1, day=1) value = date(year=1970, month=1, day=1)
reagent.expiry = value value = datetime.combine(value, datetime.min.time())
logger.debug(f"Expiry date coming into sql: {value} with type {type(value)}")
reagent.expiry = value.replace(tzinfo=timezone)
case _: case _:
try: try:
reagent.__setattr__(key, value) reagent.__setattr__(key, value)
@@ -187,7 +189,11 @@ class PydSample(BaseModel, extra='allow'):
for k, v in data.model_extra.items(): for k, v in data.model_extra.items():
if k in model.timestamps(): if k in model.timestamps():
if isinstance(v, str): if isinstance(v, str):
# try:
v = datetime.strptime(v, "%Y-%m-%d") v = datetime.strptime(v, "%Y-%m-%d")
# except ValueError:
# logger.warning(f"Attribute {k} value {v} for sample {data.submitter_id} could not be coerced into date. Setting to None.")
# v = None
data.__setattr__(k, v) data.__setattr__(k, v)
# logger.debug(f"Data coming out of validation: {pformat(data)}") # logger.debug(f"Data coming out of validation: {pformat(data)}")
return data return data
@@ -678,6 +684,7 @@ class PydSubmission(BaseModel, extra='allow'):
return value return value
def __init__(self, run_custom: bool = False, **data): def __init__(self, run_custom: bool = False, **data):
logger.debug(f"{__name__} input data: {data}")
super().__init__(**data) super().__init__(**data)
# NOTE: this could also be done with default_factory # NOTE: this could also be done with default_factory
self.submission_object = BasicSubmission.find_polymorphic_subclass( self.submission_object = BasicSubmission.find_polymorphic_subclass(
@@ -833,6 +840,18 @@ class PydSubmission(BaseModel, extra='allow'):
continue continue
if association is not None and association not in instance.submission_tips_associations: if association is not None and association not in instance.submission_tips_associations:
instance.submission_tips_associations.append(association) instance.submission_tips_associations.append(association)
case item if item in instance.timestamps():
logger.warning(f"Incoming timestamp key: {item}, with value: {value}")
# value = value.replace(tzinfo=timezone)
if isinstance(value, date):
value = datetime.combine(value, datetime.min.time())
value = value.replace(tzinfo=timezone)
elif isinstance(value, str):
value: datetime = datetime.strptime(value, "%Y-%m-%d")
value = value.replace(tzinfo=timezone)
else:
value = value
instance.set_attribute(key=key, value=value)
case item if item in instance.jsons(): case item if item in instance.jsons():
# logger.debug(f"{item} is a json.") # logger.debug(f"{item} is a json.")
try: try:
@@ -941,7 +960,7 @@ class PydSubmission(BaseModel, extra='allow'):
# NOTE: Exclude any reagenttype found in this pyd not expected in kit. # NOTE: Exclude any reagenttype found in this pyd not expected in kit.
expected_check = [item.role for item in ext_kit_rtypes] expected_check = [item.role for item in ext_kit_rtypes]
output_reagents = [rt for rt in self.reagents if rt.role in expected_check] output_reagents = [rt for rt in self.reagents if rt.role in expected_check]
# logger.debug(f"Already have these reagent types: {output_reagents}") logger.debug(f"Already have these reagent types: {output_reagents}")
missing_check = [item.role for item in output_reagents] missing_check = [item.role for item in output_reagents]
missing_reagents = [rt for rt in ext_kit_rtypes if rt.role not in missing_check] missing_reagents = [rt for rt in ext_kit_rtypes if rt.role not in missing_check]
missing_reagents += [rt for rt in output_reagents if rt.missing] missing_reagents += [rt for rt in output_reagents if rt.missing]

View File

@@ -2,12 +2,13 @@
Contains all operations for creating charts, graphs and visual effects. Contains all operations for creating charts, graphs and visual effects.
''' '''
from PyQt6.QtWidgets import QWidget from PyQt6.QtWidgets import QWidget
import plotly import plotly, logging
from plotly.graph_objects import Figure from plotly.graph_objects import Figure
from plotly.graph_objs import FigureWidget
import pandas as pd import pandas as pd
from frontend.widgets.functions import select_save_file from frontend.widgets.functions import select_save_file
logger = logging.getLogger(f"submissions.{__name__}")
class CustomFigure(Figure): class CustomFigure(Figure):
@@ -40,16 +41,12 @@ class CustomFigure(Figure):
""" """
Creates final html code from plotly Creates final html code from plotly
Args:
figure (Figure): input figure
Returns: Returns:
str: html string str: html string
""" """
html = '<html><body>' html = '<html><body>'
if self is not None: if self is not None:
html += plotly.offline.plot(self, output_type='div', html += plotly.offline.plot(self, output_type='div', include_plotlyjs='cdn')
include_plotlyjs='cdn') #, image = 'png', auto_open=True, image_filename='plot_image')
else: else:
html += "<h1>No data was retrieved for the given parameters.</h1>" html += "<h1>No data was retrieved for the given parameters.</h1>"
html += '</body></html>' html += '</body></html>'

View File

@@ -2,9 +2,6 @@
Functions for constructing irida controls graphs using plotly. Functions for constructing irida controls graphs using plotly.
""" """
from pprint import pformat from pprint import pformat
from plotly.graph_objs import FigureWidget, Scatter
from . import CustomFigure from . import CustomFigure
import plotly.express as px import plotly.express as px
import pandas as pd import pandas as pd
@@ -13,30 +10,23 @@ import logging
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
# NOTE: For click events try (haven't got working yet) ipywidgets >=7.0.0 required for figurewidgets:
# https://plotly.com/python/click-events/
class PCRFigure(CustomFigure): class PCRFigure(CustomFigure):
def __init__(self, df: pd.DataFrame, modes: list, ytitle: str | None = None, parent: QWidget | None = None, def __init__(self, df: pd.DataFrame, modes: list, ytitle: str | None = None, parent: QWidget | None = None,
months: int = 6): months: int = 6):
super().__init__(df=df, modes=modes) super().__init__(df=df, modes=modes)
logger.debug(f"DF: {self.df}") # logger.debug(f"DF: {self.df}")
self.construct_chart(df=df) self.construct_chart(df=df)
def hello(self):
print("hello")
def construct_chart(self, df: pd.DataFrame): def construct_chart(self, df: pd.DataFrame):
logger.debug(f"PCR df:\n {df}") # logger.debug(f"PCR df:\n {df}")
try: try:
express = px.scatter(data_frame=df, x='submitted_date', y="ct", scatter = px.scatter(data_frame=df, x='submitted_date', y="ct",
hover_data=["name", "target", "ct", "reagent_lot"], hover_data=["name", "target", "ct", "reagent_lot"],
color="target") color="target")
except ValueError: except ValueError:
express = px.scatter() scatter = px.scatter()
scatter = FigureWidget([datum for datum in express.data])
self.add_traces(scatter.data) self.add_traces(scatter.data)
self.update_traces(marker={'size': 15}) self.update_traces(marker={'size': 15})

View File

@@ -2,6 +2,7 @@
Constructs main application. Constructs main application.
""" """
from pprint import pformat from pprint import pformat
from PyQt6.QtCore import qInstallMessageHandler
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QTabWidget, QWidget, QVBoxLayout, QTabWidget, QWidget, QVBoxLayout,
QHBoxLayout, QScrollArea, QMainWindow, QHBoxLayout, QScrollArea, QMainWindow,
@@ -11,6 +12,7 @@ from PyQt6.QtGui import QAction
from pathlib import Path from pathlib import Path
from markdown import markdown from markdown import markdown
from __init__ import project_path from __init__ import project_path
from backend import SubmissionType
from tools import check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size from tools import check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size
from .functions import select_save_file, select_open_file from .functions import select_save_file, select_open_file
from datetime import date from datetime import date
@@ -32,12 +34,13 @@ class App(QMainWindow):
def __init__(self, ctx: Settings = None): def __init__(self, ctx: Settings = None):
# logger.debug(f"Initializing main window...") # logger.debug(f"Initializing main window...")
super().__init__() super().__init__()
qInstallMessageHandler(lambda x, y, z: None)
self.ctx = ctx self.ctx = ctx
self.last_dir = ctx.directory_path self.last_dir = ctx.directory_path
self.report = Report() self.report = Report()
# NOTE: indicate version and connected database in title bar # NOTE: indicate version and connected database in title bar
try: try:
self.title = f"Submissions App (v{ctx.package.__version__}) - {ctx.database_session.get_bind().url}" self.title = f"Submissions App (v{ctx.package.__version__}) - {ctx.database_path}/{ctx.database_name}"
except (AttributeError, KeyError): except (AttributeError, KeyError):
self.title = f"Submissions App" self.title = f"Submissions App"
# NOTE: set initial app position and size # NOTE: set initial app position and size

View File

@@ -155,7 +155,7 @@ class ControlsViewer(QWidget):
chart_settings = dict(sub_type=self.con_sub_type, start_date=self.start_date, end_date=self.end_date, chart_settings = dict(sub_type=self.con_sub_type, start_date=self.start_date, end_date=self.end_date,
mode=self.mode, mode=self.mode,
sub_mode=self.mode_sub_type, parent=self, months=months) sub_mode=self.mode_sub_type, parent=self, months=months)
_, self.fig = self.archetype.get_instance_class().make_chart(chart_settings=chart_settings, parent=self, ctx=self.app.ctx) self.fig = self.archetype.get_instance_class().make_chart(chart_settings=chart_settings, parent=self, ctx=self.app.ctx)
if issubclass(self.fig.__class__, CustomFigure): if issubclass(self.fig.__class__, CustomFigure):
self.save_button.setEnabled(True) self.save_button.setEnabled(True)
# logger.debug(f"Updating figure...") # logger.debug(f"Updating figure...")

View File

@@ -1,8 +1,9 @@
''' '''
Creates forms that the user can enter equipment info into. Creates forms that the user can enter equipment info into.
''' '''
import time
from pprint import pformat from pprint import pformat
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt, QSignalBlocker
from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox, from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,
QLabel, QWidget, QVBoxLayout, QDialogButtonBox, QGridLayout) QLabel, QWidget, QVBoxLayout, QDialogButtonBox, QGridLayout)
from backend.db.models import Equipment, BasicSubmission, Process from backend.db.models import Equipment, BasicSubmission, Process
@@ -127,13 +128,14 @@ class RoleComboBox(QWidget):
# logger.debug(f"Updating equipment: {equip}") # logger.debug(f"Updating equipment: {equip}")
equip2 = next((item for item in self.role.equipment if item.name == equip), self.role.equipment[0]) equip2 = next((item for item in self.role.equipment if item.name == equip), self.role.equipment[0])
# logger.debug(f"Using: {equip2}") # logger.debug(f"Using: {equip2}")
self.process.clear() with QSignalBlocker(self.process) as blocker:
self.process.clear()
self.process.addItems([item for item in equip2.processes if item in self.role.processes]) self.process.addItems([item for item in equip2.processes if item in self.role.processes])
def update_tips(self): def update_tips(self):
""" """
Changes what tips are available when process is changed Changes what tips are available when process is changed
""" """
process = self.process.currentText().strip() process = self.process.currentText().strip()
# logger.debug(f"Checking process: {process} for equipment {self.role.name}") # logger.debug(f"Checking process: {process} for equipment {self.role.name}")
process = Process.query(name=process) process = Process.query(name=process)

View File

@@ -1,6 +1,7 @@
""" """
Gel box for artic quality control Gel box for artic quality control
""" """
from operator import itemgetter
from PyQt6.QtWidgets import (QWidget, QDialog, QGridLayout, from PyQt6.QtWidgets import (QWidget, QDialog, QGridLayout,
QLabel, QLineEdit, QDialogButtonBox, QLabel, QLineEdit, QDialogButtonBox,
QTextEdit, QComboBox QTextEdit, QComboBox
@@ -65,7 +66,8 @@ class GelBox(QDialog):
layout.addWidget(self.imv, 0, 1, 20, 20) layout.addWidget(self.imv, 0, 1, 20, 20)
# NOTE: setting this widget as central widget of the main window # NOTE: setting this widget as central widget of the main window
try: try:
control_info = sorted(self.submission.gel_controls, key=lambda d: d['location']) # control_info = sorted(self.submission.gel_controls, key=lambda d: d['location'])
control_info = sorted(self.submission.gel_controls, key=itemgetter('location'))
except KeyError: except KeyError:
control_info = None control_info = None
self.form = ControlsForm(parent=self, control_info=control_info) self.form = ControlsForm(parent=self, control_info=control_info)

View File

@@ -99,19 +99,21 @@ class SubmissionsSheet(QTableView):
proxyModel.setSourceModel(pandasModel(self.data)) proxyModel.setSourceModel(pandasModel(self.data))
self.setModel(proxyModel) self.setModel(proxyModel)
def contextMenuEvent(self): def contextMenuEvent(self, event):
""" """
Creates actions for right click menu events. Creates actions for right click menu events.
Args: Args:
event (_type_): the item of interest event (_type_): the item of interest
""" """
# logger.debug(event().__dict__) # logger.debug(event.__dict__)
id = self.selectionModel().currentIndex() id = self.selectionModel().currentIndex()
id = id.sibling(id.row(), 0).data() id = id.sibling(id.row(), 0).data()
submission = BasicSubmission.query(id=id) submission = BasicSubmission.query(id=id)
# logger.debug(f"Event submission: {submission}")
self.menu = QMenu(self) self.menu = QMenu(self)
self.con_actions = submission.custom_context_events() self.con_actions = submission.custom_context_events()
# logger.debug(f"Menu options: {self.con_actions}")
for k in self.con_actions.keys(): for k in self.con_actions.keys():
# logger.debug(f"Adding {k}") # logger.debug(f"Adding {k}")
action = QAction(k, self) action = QAction(k, self)

View File

@@ -1,6 +1,8 @@
''' '''
Contains all submission related frontend functions Contains all submission related frontend functions
''' '''
import sys
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QPushButton, QVBoxLayout, QWidget, QPushButton, QVBoxLayout,
QComboBox, QDateEdit, QLineEdit, QLabel QComboBox, QDateEdit, QLineEdit, QLabel
@@ -190,7 +192,8 @@ class SubmissionFormWidget(QWidget):
self.app = parent.app self.app = parent.app
self.pyd = submission self.pyd = submission
self.missing_info = [] self.missing_info = []
st = SubmissionType.query(name=self.pyd.submission_type['value']).get_submission_class() self.submission_type = SubmissionType.query(name=self.pyd.submission_type['value'])
st = self.submission_type.get_submission_class()
defaults = st.get_default_info("form_recover", "form_ignore", submission_type=self.pyd.submission_type['value']) defaults = st.get_default_info("form_recover", "form_ignore", submission_type=self.pyd.submission_type['value'])
self.recover = defaults['form_recover'] self.recover = defaults['form_recover']
self.ignore = defaults['form_ignore'] self.ignore = defaults['form_ignore']
@@ -215,7 +218,7 @@ class SubmissionFormWidget(QWidget):
value = self.pyd.model_extra[k] value = self.pyd.model_extra[k]
except KeyError: except KeyError:
value = dict(value=None, missing=True) value = dict(value=None, missing=True)
add_widget = self.create_widget(key=k, value=value, submission_type=self.pyd.submission_type['value'], add_widget = self.create_widget(key=k, value=value, submission_type=self.submission_type,
sub_obj=st, disable=check) sub_obj=st, disable=check)
if add_widget is not None: if add_widget is not None:
self.layout.addWidget(add_widget) self.layout.addWidget(add_widget)
@@ -224,7 +227,7 @@ class SubmissionFormWidget(QWidget):
self.setStyleSheet(main_form_style) self.setStyleSheet(main_form_style)
self.scrape_reagents(self.pyd.extraction_kit) self.scrape_reagents(self.pyd.extraction_kit)
def create_widget(self, key: str, value: dict | PydReagent, submission_type: str | None = None, def create_widget(self, key: str, value: dict | PydReagent, submission_type: str | SubmissionType| None = None,
extraction_kit: str | None = None, sub_obj: BasicSubmission | None = None, extraction_kit: str | None = None, sub_obj: BasicSubmission | None = None,
disable: bool = False) -> "self.InfoItem": disable: bool = False) -> "self.InfoItem":
""" """
@@ -240,6 +243,8 @@ class SubmissionFormWidget(QWidget):
self.InfoItem: Form widget to hold name:value self.InfoItem: Form widget to hold name:value
""" """
# logger.debug(f"Key: {key}, Disable: {disable}") # logger.debug(f"Key: {key}, Disable: {disable}")
if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type)
if key not in self.ignore: if key not in self.ignore:
match value: match value:
case PydReagent(): case PydReagent():
@@ -272,7 +277,7 @@ class SubmissionFormWidget(QWidget):
""" """
extraction_kit = args[0] extraction_kit = args[0]
report = Report() report = Report()
# logger.debug(f"Extraction kit: {extraction_kit}") logger.debug(f"Extraction kit: {extraction_kit}")
# NOTE: Remove previous reagent widgets # NOTE: Remove previous reagent widgets
try: try:
old_reagents = self.find_widgets() old_reagents = self.find_widgets()
@@ -284,7 +289,7 @@ class SubmissionFormWidget(QWidget):
if isinstance(reagent, self.ReagentFormWidget) or isinstance(reagent, QPushButton): if isinstance(reagent, self.ReagentFormWidget) or isinstance(reagent, QPushButton):
reagent.setParent(None) reagent.setParent(None)
reagents, integrity_report = self.pyd.check_kit_integrity(extraction_kit=extraction_kit) reagents, integrity_report = self.pyd.check_kit_integrity(extraction_kit=extraction_kit)
# logger.debug(f"Missing reagents: {obj.missing_reagents}") logger.debug(f"Got reagents: {pformat(reagents)}")
for reagent in reagents: for reagent in reagents:
add_widget = self.ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.pyd.extraction_kit) add_widget = self.ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.pyd.extraction_kit)
self.layout.addWidget(add_widget) self.layout.addWidget(add_widget)
@@ -454,9 +459,11 @@ class SubmissionFormWidget(QWidget):
class InfoItem(QWidget): class InfoItem(QWidget):
def __init__(self, parent: QWidget, key: str, value: dict, submission_type: str | None = None, def __init__(self, parent: QWidget, key: str, value: dict, submission_type: str | SubmissionType | None = None,
sub_obj: BasicSubmission | None = None) -> None: sub_obj: BasicSubmission | None = None) -> None:
super().__init__(parent) super().__init__(parent)
if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type)
layout = QVBoxLayout() layout = QVBoxLayout()
self.label = self.ParsedQLabel(key=key, value=value) self.label = self.ParsedQLabel(key=key, value=value)
self.input: QWidget = self.set_widget(parent=parent, key=key, value=value, submission_type=submission_type, self.input: QWidget = self.set_widget(parent=parent, key=key, value=value, submission_type=submission_type,
@@ -497,7 +504,7 @@ class SubmissionFormWidget(QWidget):
return None, None return None, None
return self.input.objectName(), dict(value=value, missing=self.missing) return self.input.objectName(), dict(value=value, missing=self.missing)
def set_widget(self, parent: QWidget, key: str, value: dict, submission_type: str | None = None, def set_widget(self, parent: QWidget, key: str, value: dict, submission_type: str | SubmissionType | None = None,
sub_obj: BasicSubmission | None = None) -> QWidget: sub_obj: BasicSubmission | None = None) -> QWidget:
""" """
Creates form widget Creates form widget
@@ -511,8 +518,10 @@ class SubmissionFormWidget(QWidget):
Returns: Returns:
QWidget: Form object QWidget: Form object
""" """
if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type)
if sub_obj is None: if sub_obj is None:
sub_obj = SubmissionType.query(name=submission_type).get_submission_class() sub_obj = submission_type.get_submission_class()
try: try:
value = value['value'] value = value['value']
except (TypeError, KeyError): except (TypeError, KeyError):
@@ -544,7 +553,8 @@ class SubmissionFormWidget(QWidget):
add_widget = MyQComboBox(scrollWidget=parent) add_widget = MyQComboBox(scrollWidget=parent)
# NOTE: lookup existing kits by 'submission_type' decided on by sheetparser # NOTE: lookup existing kits by 'submission_type' decided on by sheetparser
# logger.debug(f"Looking up kits used for {submission_type}") # logger.debug(f"Looking up kits used for {submission_type}")
uses = [item.name for item in KitType.query(used_for=submission_type)] # uses = [item.name for item in KitType.query(used_for=submission_type)]
uses = [item.name for item in submission_type.kit_types]
obj.uses = uses obj.uses = uses
# logger.debug(f"Kits received for {submission_type}: {uses}") # logger.debug(f"Kits received for {submission_type}: {uses}")
if check_not_nan(value): if check_not_nan(value):
@@ -668,7 +678,7 @@ class SubmissionFormWidget(QWidget):
dlg = QuestionAsker(title=f"Add {lot}?", dlg = QuestionAsker(title=f"Add {lot}?",
message=f"Couldn't find reagent type {self.reagent.role}: {lot} in the database.\n\nWould you like to add it?") message=f"Couldn't find reagent type {self.reagent.role}: {lot} in the database.\n\nWould you like to add it?")
if dlg.exec(): if dlg.exec():
wanted_reagent, _ = self.parent().parent().add_reagent(reagent_lot=lot, wanted_reagent = self.parent().parent().add_reagent(reagent_lot=lot,
reagent_role=self.reagent.role, reagent_role=self.reagent.role,
expiry=self.reagent.expiry, expiry=self.reagent.expiry,
name=self.reagent.name) name=self.reagent.name)

View File

@@ -42,7 +42,6 @@ class Summary(QWidget):
self.setLayout(self.layout) self.setLayout(self.layout)
self.get_report() self.get_report()
def get_report(self): def get_report(self):
orgs = [self.org_select.itemText(i) for i in range(self.org_select.count()) if self.org_select.itemChecked(i)] orgs = [self.org_select.itemText(i) for i in range(self.org_select.count()) if self.org_select.itemChecked(i)]
if self.datepicker.start_date.date() > self.datepicker.end_date.date(): if self.datepicker.start_date.date() > self.datepicker.end_date.date():

View File

@@ -17,13 +17,15 @@ from sqlalchemy import create_engine, text, MetaData
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
print(inspect.stack()[1]) # print(inspect.stack()[1])
from __init__ import project_path from __init__ import project_path
from configparser import ConfigParser from configparser import ConfigParser
from tkinter import Tk # NOTE: This is for choosing database path before app is created. from tkinter import Tk # NOTE: This is for choosing database path before app is created.
from tkinter.filedialog import askdirectory from tkinter.filedialog import askdirectory
from sqlalchemy.exc import IntegrityError as sqlalcIntegrityError from sqlalchemy.exc import IntegrityError as sqlalcIntegrityError
from pytz import timezone as tz
timezone = tz("America/Winnipeg")
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -386,16 +388,22 @@ class Settings(BaseSettings, extra="allow"):
case "sqlite": case "sqlite":
value = f"/{values.data['database_path']}" value = f"/{values.data['database_path']}"
db_name = f"{values.data['database_name']}.db" db_name = f"{values.data['database_name']}.db"
template = jinja_template_loading().from_string(
"{{ values['database_schema'] }}://{{ value }}/{{ db_name }}")
case "mssql+pyodbc":
value = values.data['database_path']
db_name = values.data['database_name']
template = jinja_template_loading().from_string(
"{{ values['database_schema'] }}://{{ value }}/{{ db_name }}?driver=ODBC+Driver+18+for+SQL+Server&TrustServerCertificate=yes&Trusted_Connection=yes"
)
case _: case _:
# print(pprint.pprint(values.data)) # print(pprint.pprint(values.data))
tmp = jinja_template_loading().from_string( tmp = jinja_template_loading().from_string(
"{% if values['database_user'] %}{{ values['database_user'] }}{% if values['database_password'] %}:{{ values['database_password'] }}{% endif %}{% endif %}@{{ values['database_path'] }}") "{% if values['database_user'] %}{{ values['database_user'] }}{% if values['database_password'] %}:{{ values['database_password'] }}{% endif %}{% endif %}@{{ values['database_path'] }}")
value = tmp.render(values=values.data) value = tmp.render(values=values.data)
db_name = values.data['database_name'] db_name = values.data['database_name']
template = jinja_template_loading().from_string(
"{{ values['database_schema'] }}://{{ value }}/{{ db_name }}")
database_path = template.render(values=values.data, value=value, db_name=db_name) database_path = template.render(values=values.data, value=value, db_name=db_name)
# print(f"Using {database_path} for database path") print(f"Using {database_path} for database path")
engine = create_engine(database_path) engine = create_engine(database_path)
session = Session(engine) session = Session(engine)
return session return session
@@ -939,8 +947,7 @@ def report_result(func):
""" """
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
# logger.debug(f"Arguments: {args}") logger.debug(f"Report result being called by {func.__name__}")
# logger.debug(f"Keyword arguments: {kwargs}")
output = func(*args, **kwargs) output = func(*args, **kwargs)
match output: match output:
case Report(): case Report():
@@ -966,6 +973,12 @@ def report_result(func):
except Exception as e: except Exception as e:
logger.error(f"Problem reporting due to {e}") logger.error(f"Problem reporting due to {e}")
logger.error(result.msg) logger.error(result.msg)
# logger.debug(f"Returning: {output}") if output:
return output true_output = tuple(item for item in output if not isinstance(item, Report))
if len(true_output) == 1:
true_output = true_output[0]
else:
true_output = None
# logger.debug(f"Returning true output: {true_output}")
return true_output
return wrapper return wrapper