Second round of code cleanup.
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
## 202410.05
|
||||
|
||||
- Code clean up.
|
||||
|
||||
## 202410.03
|
||||
|
||||
- Added code for cataloging of PCR controls.
|
||||
|
||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
@@ -28,6 +28,8 @@ class BaseClass(Base):
|
||||
|
||||
__table_args__ = {'extend_existing': True} #: Will only add new columns
|
||||
|
||||
singles = ['id']
|
||||
|
||||
@classmethod
|
||||
@declared_attr
|
||||
def __tablename__(cls) -> str:
|
||||
@@ -92,17 +94,21 @@ class BaseClass(Base):
|
||||
Returns:
|
||||
dict | list | str: Output of key:value dict or single (list, str) desired variable
|
||||
"""
|
||||
dicto = dict(singles=['id'])
|
||||
output = {}
|
||||
for k, v in dicto.items():
|
||||
if len(args) > 0 and k not in args:
|
||||
# logger.debug(f"{k} not selected as being of interest.")
|
||||
continue
|
||||
else:
|
||||
output[k] = v
|
||||
if len(args) == 1:
|
||||
return output[args[0]]
|
||||
return output
|
||||
# if issubclass(cls, BaseClass) and cls.__name__ != "BaseClass":
|
||||
singles = list(set(cls.singles + BaseClass.singles))
|
||||
# else:
|
||||
# singles = cls.singles
|
||||
# output = dict(singles=singles)
|
||||
# output = {}
|
||||
# for k, v in dicto.items():
|
||||
# if len(args) > 0 and k not in args:
|
||||
# # logger.debug(f"{k} not selected as being of interest.")
|
||||
# continue
|
||||
# else:
|
||||
# output[k] = v
|
||||
# if len(args) == 1:
|
||||
# return output[args[0]]
|
||||
return dict(singles=singles)
|
||||
|
||||
@classmethod
|
||||
def query(cls, **kwargs) -> Any | List[Any]:
|
||||
@@ -190,10 +196,15 @@ class ConfigItem(BaseClass):
|
||||
Returns:
|
||||
ConfigItem|List[ConfigItem]: Config item(s)
|
||||
"""
|
||||
config_items = cls.__database_session__.query(cls).all()
|
||||
config_items = [item for item in config_items if item.key in args]
|
||||
if len(args) == 1:
|
||||
config_items = config_items[0]
|
||||
query = cls.__database_session__.query(cls)
|
||||
# config_items = [item for item in config_items if item.key in args]
|
||||
match len(args):
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -131,10 +131,8 @@ class Control(BaseClass):
|
||||
__mapper_args__ = {
|
||||
"polymorphic_identity": "Basic Control",
|
||||
"polymorphic_on": case(
|
||||
|
||||
(controltype_name == "PCR Control", "PCR Control"),
|
||||
(controltype_name == "Irida Control", "Irida Control"),
|
||||
|
||||
else_="Basic Control"
|
||||
),
|
||||
"with_polymorphic": "*",
|
||||
@@ -147,15 +145,15 @@ class Control(BaseClass):
|
||||
def find_polymorphic_subclass(cls, polymorphic_identity: str | ControlType | None = None,
|
||||
attrs: dict | None = None) -> Control:
|
||||
"""
|
||||
Find subclass based on polymorphic identity or relevant attributes.
|
||||
Find subclass based on polymorphic identity or relevant attributes.
|
||||
|
||||
Args:
|
||||
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.
|
||||
Args:
|
||||
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.
|
||||
|
||||
Returns:
|
||||
Control: Subclass of interest.
|
||||
"""
|
||||
Returns:
|
||||
Control: Subclass of interest.
|
||||
"""
|
||||
if isinstance(polymorphic_identity, dict):
|
||||
# logger.debug(f"Controlling for dict value")
|
||||
polymorphic_identity = polymorphic_identity['value']
|
||||
@@ -189,14 +187,11 @@ class Control(BaseClass):
|
||||
|
||||
Args:
|
||||
parent (QWidget): chart holding widget to add buttons to.
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
pass
|
||||
return None
|
||||
|
||||
@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.
|
||||
|
||||
@@ -307,6 +302,7 @@ class PCRControl(Control):
|
||||
return cls.execute_query(query=query, limit=limit)
|
||||
|
||||
@classmethod
|
||||
@report_result
|
||||
def make_chart(cls, parent, chart_settings: dict, ctx: Settings) -> Tuple[Report, "PCRFigure"]:
|
||||
"""
|
||||
Creates a PCRFigure. Overrides parent
|
||||
|
||||
@@ -4,6 +4,7 @@ All kit and reagent related models
|
||||
from __future__ import annotations
|
||||
import datetime
|
||||
import json
|
||||
import sys
|
||||
from pprint import pformat
|
||||
import yaml
|
||||
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB
|
||||
@@ -693,6 +694,9 @@ class SubmissionType(BaseClass):
|
||||
Returns:
|
||||
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
|
||||
|
||||
def set_template_file(self, filepath: Path | str):
|
||||
|
||||
@@ -6,7 +6,7 @@ import sys
|
||||
import types
|
||||
from copy import deepcopy
|
||||
from getpass import getuser
|
||||
import logging, uuid, tempfile, re, yaml, base64
|
||||
import logging, uuid, tempfile, re, base64
|
||||
from zipfile import ZipFile
|
||||
from tempfile import TemporaryDirectory, TemporaryFile
|
||||
from operator import itemgetter
|
||||
@@ -167,28 +167,24 @@ class BasicSubmission(BaseClass):
|
||||
|
||||
"""
|
||||
# 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']
|
||||
dicto = dict(
|
||||
dicto.update(dict(
|
||||
details_ignore=['excluded', 'reagents', 'samples',
|
||||
'extraction_info', 'comment', 'barcode',
|
||||
'platemap', 'export_map', 'equipment', 'tips', 'custom'],
|
||||
# NOTE: Fields not placed in ui form
|
||||
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
|
||||
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.
|
||||
output = {}
|
||||
for k, v in dicto.items():
|
||||
if len(args) > 0 and k not in args:
|
||||
# logger.debug(f"Don't want {k}")
|
||||
continue
|
||||
else:
|
||||
output[k] = v
|
||||
if args:
|
||||
output = {k: v for k, v in dicto.items() if k in args}
|
||||
else:
|
||||
output = {k: v for k, v in dicto.items()}
|
||||
if isinstance(submission_type, SubmissionType):
|
||||
st = submission_type
|
||||
else:
|
||||
@@ -198,7 +194,7 @@ class BasicSubmission(BaseClass):
|
||||
else:
|
||||
output['submission_type'] = st.name
|
||||
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}")
|
||||
continue
|
||||
else:
|
||||
@@ -272,6 +268,7 @@ class BasicSubmission(BaseClass):
|
||||
field = self.__getattribute__(name)
|
||||
except AttributeError:
|
||||
return None
|
||||
# assert isinstance(field, list)
|
||||
for item in field:
|
||||
if extra:
|
||||
yield item.to_sub_dict(extra)
|
||||
@@ -1137,9 +1134,9 @@ class BasicSubmission(BaseClass):
|
||||
limit = 1
|
||||
case _:
|
||||
pass
|
||||
if chronologic:
|
||||
logger.debug("Attempting sort by date descending")
|
||||
query = query.order_by(cls.submitted_date.desc())
|
||||
# if chronologic:
|
||||
# logger.debug("Attempting sort by date descending")
|
||||
query = query.order_by(cls.submitted_date.desc())
|
||||
if page_size is not None:
|
||||
query = query.limit(page_size)
|
||||
page = page - 1
|
||||
@@ -2980,7 +2977,6 @@ class WastewaterArticAssociation(SubmissionSampleAssociation):
|
||||
Returns:
|
||||
dict: Updated dictionary with row, column and well updated
|
||||
"""
|
||||
|
||||
sample = super().to_sub_dict()
|
||||
sample['ct'] = self.ct
|
||||
sample['source_plate'] = self.source_plate
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
contains parser objects for pulling values from client generated submission sheets.
|
||||
'''
|
||||
import json
|
||||
import sys
|
||||
from copy import copy
|
||||
from getpass import getuser
|
||||
from pprint import pformat
|
||||
@@ -95,6 +96,7 @@ class SheetParser(object):
|
||||
parser = ReagentParser(xl=self.xl, submission_type=self.submission_type,
|
||||
extraction_kit=extraction_kit)
|
||||
self.sub['reagents'] = parser.parse_reagents()
|
||||
logger.debug(f"Reagents out of parser: {pformat(self.sub['reagents'])}")
|
||||
|
||||
def parse_samples(self):
|
||||
"""
|
||||
@@ -273,7 +275,8 @@ class ReagentParser(object):
|
||||
# logger.debug(f"Reagent Parser map: {self.map}")
|
||||
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
|
||||
|
||||
@@ -283,7 +286,7 @@ class ReagentParser(object):
|
||||
Returns:
|
||||
dict: locations of reagent info for the kit.
|
||||
"""
|
||||
|
||||
report = Report()
|
||||
if isinstance(submission_type, dict):
|
||||
submission_type = submission_type['value']
|
||||
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']
|
||||
except KeyError:
|
||||
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]:
|
||||
"""
|
||||
@@ -401,6 +409,7 @@ class SampleParser(object):
|
||||
"""
|
||||
invalids = [0, "0", "EMPTY"]
|
||||
smap = self.sample_info_map['plate_map']
|
||||
print(smap)
|
||||
ws = self.xl[smap['sheet']]
|
||||
plate_map_samples = []
|
||||
for ii, row in enumerate(range(smap['start_row'], smap['end_row'] + 1), start=1):
|
||||
@@ -469,8 +478,10 @@ class SampleParser(object):
|
||||
yield new
|
||||
else:
|
||||
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'])
|
||||
lookup_samples = sorted(copy(self.lookup_samples), key=lambda d: d[merge_on_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])
|
||||
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):
|
||||
# NOTE: See if we can do this the easy way and just use the same list index.
|
||||
try:
|
||||
@@ -483,6 +494,8 @@ class SampleParser(object):
|
||||
lookup_samples[ii] = {}
|
||||
else:
|
||||
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):
|
||||
# try:
|
||||
# check = lsample[merge_on_id] == psample['id']
|
||||
@@ -494,14 +507,18 @@ class SampleParser(object):
|
||||
# break
|
||||
# else:
|
||||
# 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}")
|
||||
if jj >= 0:
|
||||
lookup_samples[jj] = {}
|
||||
if not check_key_or_attr(key='submitter_id', interest=new, check_none=True):
|
||||
new['submitter_id'] = psample['id']
|
||||
new = self.sub_object.parse_samples(new)
|
||||
del new['id']
|
||||
try:
|
||||
del new['id']
|
||||
except KeyError:
|
||||
pass
|
||||
yield new
|
||||
|
||||
|
||||
@@ -586,7 +603,7 @@ class EquipmentParser(object):
|
||||
nickname=eq.nickname)
|
||||
except AttributeError:
|
||||
logger.error(f"Unable to add {eq} to list.")
|
||||
|
||||
|
||||
|
||||
class TipParser(object):
|
||||
"""
|
||||
@@ -649,7 +666,7 @@ class TipParser(object):
|
||||
yield dict(name=eq.name, role=k, lot=lot)
|
||||
except AttributeError:
|
||||
logger.error(f"Unable to add {eq} to PydTips list.")
|
||||
|
||||
|
||||
|
||||
class PCRParser(object):
|
||||
"""Object to pull data from Design and Analysis PCR export file."""
|
||||
@@ -705,4 +722,3 @@ class PCRParser(object):
|
||||
pcr['imported_by'] = getuser()
|
||||
# logger.debug(f"PCR: {pformat(pcr)}")
|
||||
return pcr
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ contains writer objects for pushing values to submission sheet templates.
|
||||
"""
|
||||
import logging
|
||||
from copy import copy
|
||||
from operator import itemgetter
|
||||
from pprint import pformat
|
||||
from typing import List, Generator, Tuple
|
||||
from openpyxl import load_workbook, Workbook
|
||||
@@ -272,7 +273,8 @@ class SampleWriter(object):
|
||||
self.sample_map = submission_type.construct_sample_map()['lookup_table']
|
||||
# NOTE: exclude any samples without a submission rank.
|
||||
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]:
|
||||
"""
|
||||
|
||||
@@ -11,7 +11,7 @@ from dateutil.parser import ParserError
|
||||
from typing import List, Tuple, Literal
|
||||
from . import RSLNamer
|
||||
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 sqlalchemy.exc import StatementError, IntegrityError
|
||||
from PyQt6.QtWidgets import QWidget
|
||||
@@ -148,7 +148,9 @@ class PydReagent(BaseModel):
|
||||
case "expiry":
|
||||
if isinstance(value, str):
|
||||
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 _:
|
||||
try:
|
||||
reagent.__setattr__(key, value)
|
||||
@@ -187,7 +189,11 @@ class PydSample(BaseModel, extra='allow'):
|
||||
for k, v in data.model_extra.items():
|
||||
if k in model.timestamps():
|
||||
if isinstance(v, str):
|
||||
# try:
|
||||
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)
|
||||
# logger.debug(f"Data coming out of validation: {pformat(data)}")
|
||||
return data
|
||||
@@ -678,6 +684,7 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
return value
|
||||
|
||||
def __init__(self, run_custom: bool = False, **data):
|
||||
logger.debug(f"{__name__} input data: {data}")
|
||||
super().__init__(**data)
|
||||
# NOTE: this could also be done with default_factory
|
||||
self.submission_object = BasicSubmission.find_polymorphic_subclass(
|
||||
@@ -833,6 +840,18 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
continue
|
||||
if association is not None and association not in instance.submission_tips_associations:
|
||||
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():
|
||||
# logger.debug(f"{item} is a json.")
|
||||
try:
|
||||
@@ -941,7 +960,7 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
# NOTE: Exclude any reagenttype found in this pyd not expected in kit.
|
||||
expected_check = [item.role for item in ext_kit_rtypes]
|
||||
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_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]
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
Contains all operations for creating charts, graphs and visual effects.
|
||||
'''
|
||||
from PyQt6.QtWidgets import QWidget
|
||||
import plotly
|
||||
import plotly, logging
|
||||
from plotly.graph_objects import Figure
|
||||
from plotly.graph_objs import FigureWidget
|
||||
import pandas as pd
|
||||
from frontend.widgets.functions import select_save_file
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
|
||||
class CustomFigure(Figure):
|
||||
|
||||
@@ -40,16 +41,12 @@ class CustomFigure(Figure):
|
||||
"""
|
||||
Creates final html code from plotly
|
||||
|
||||
Args:
|
||||
figure (Figure): input figure
|
||||
|
||||
Returns:
|
||||
str: html string
|
||||
"""
|
||||
html = '<html><body>'
|
||||
if self is not None:
|
||||
html += plotly.offline.plot(self, output_type='div',
|
||||
include_plotlyjs='cdn') #, image = 'png', auto_open=True, image_filename='plot_image')
|
||||
html += plotly.offline.plot(self, output_type='div', include_plotlyjs='cdn')
|
||||
else:
|
||||
html += "<h1>No data was retrieved for the given parameters.</h1>"
|
||||
html += '</body></html>'
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
Functions for constructing irida controls graphs using plotly.
|
||||
"""
|
||||
from pprint import pformat
|
||||
|
||||
from plotly.graph_objs import FigureWidget, Scatter
|
||||
|
||||
from . import CustomFigure
|
||||
import plotly.express as px
|
||||
import pandas as pd
|
||||
@@ -13,30 +10,23 @@ import logging
|
||||
|
||||
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):
|
||||
|
||||
def __init__(self, df: pd.DataFrame, modes: list, ytitle: str | None = None, parent: QWidget | None = None,
|
||||
months: int = 6):
|
||||
super().__init__(df=df, modes=modes)
|
||||
logger.debug(f"DF: {self.df}")
|
||||
# logger.debug(f"DF: {self.df}")
|
||||
self.construct_chart(df=df)
|
||||
|
||||
def hello(self):
|
||||
print("hello")
|
||||
|
||||
def construct_chart(self, df: pd.DataFrame):
|
||||
logger.debug(f"PCR df:\n {df}")
|
||||
# logger.debug(f"PCR df:\n {df}")
|
||||
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"],
|
||||
color="target")
|
||||
except ValueError:
|
||||
express = px.scatter()
|
||||
scatter = FigureWidget([datum for datum in express.data])
|
||||
scatter = px.scatter()
|
||||
self.add_traces(scatter.data)
|
||||
self.update_traces(marker={'size': 15})
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Constructs main application.
|
||||
"""
|
||||
from pprint import pformat
|
||||
from PyQt6.QtCore import qInstallMessageHandler
|
||||
from PyQt6.QtWidgets import (
|
||||
QTabWidget, QWidget, QVBoxLayout,
|
||||
QHBoxLayout, QScrollArea, QMainWindow,
|
||||
@@ -11,6 +12,7 @@ from PyQt6.QtGui import QAction
|
||||
from pathlib import Path
|
||||
from markdown import markdown
|
||||
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 .functions import select_save_file, select_open_file
|
||||
from datetime import date
|
||||
@@ -32,12 +34,13 @@ class App(QMainWindow):
|
||||
def __init__(self, ctx: Settings = None):
|
||||
# logger.debug(f"Initializing main window...")
|
||||
super().__init__()
|
||||
qInstallMessageHandler(lambda x, y, z: None)
|
||||
self.ctx = ctx
|
||||
self.last_dir = ctx.directory_path
|
||||
self.report = Report()
|
||||
# NOTE: indicate version and connected database in title bar
|
||||
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):
|
||||
self.title = f"Submissions App"
|
||||
# NOTE: set initial app position and size
|
||||
|
||||
@@ -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,
|
||||
mode=self.mode,
|
||||
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):
|
||||
self.save_button.setEnabled(True)
|
||||
# logger.debug(f"Updating figure...")
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
'''
|
||||
Creates forms that the user can enter equipment info into.
|
||||
'''
|
||||
import time
|
||||
from pprint import pformat
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtCore import Qt, QSignalBlocker
|
||||
from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,
|
||||
QLabel, QWidget, QVBoxLayout, QDialogButtonBox, QGridLayout)
|
||||
from backend.db.models import Equipment, BasicSubmission, Process
|
||||
@@ -127,13 +128,14 @@ class RoleComboBox(QWidget):
|
||||
# logger.debug(f"Updating equipment: {equip}")
|
||||
equip2 = next((item for item in self.role.equipment if item.name == equip), self.role.equipment[0])
|
||||
# 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])
|
||||
|
||||
def update_tips(self):
|
||||
"""
|
||||
Changes what tips are available when process is changed
|
||||
"""
|
||||
"""
|
||||
process = self.process.currentText().strip()
|
||||
# logger.debug(f"Checking process: {process} for equipment {self.role.name}")
|
||||
process = Process.query(name=process)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Gel box for artic quality control
|
||||
"""
|
||||
from operator import itemgetter
|
||||
from PyQt6.QtWidgets import (QWidget, QDialog, QGridLayout,
|
||||
QLabel, QLineEdit, QDialogButtonBox,
|
||||
QTextEdit, QComboBox
|
||||
@@ -65,7 +66,8 @@ class GelBox(QDialog):
|
||||
layout.addWidget(self.imv, 0, 1, 20, 20)
|
||||
# NOTE: setting this widget as central widget of the main window
|
||||
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:
|
||||
control_info = None
|
||||
self.form = ControlsForm(parent=self, control_info=control_info)
|
||||
|
||||
@@ -99,19 +99,21 @@ class SubmissionsSheet(QTableView):
|
||||
proxyModel.setSourceModel(pandasModel(self.data))
|
||||
self.setModel(proxyModel)
|
||||
|
||||
def contextMenuEvent(self):
|
||||
def contextMenuEvent(self, event):
|
||||
"""
|
||||
Creates actions for right click menu events.
|
||||
|
||||
Args:
|
||||
event (_type_): the item of interest
|
||||
"""
|
||||
# logger.debug(event().__dict__)
|
||||
# logger.debug(event.__dict__)
|
||||
id = self.selectionModel().currentIndex()
|
||||
id = id.sibling(id.row(), 0).data()
|
||||
submission = BasicSubmission.query(id=id)
|
||||
# logger.debug(f"Event submission: {submission}")
|
||||
self.menu = QMenu(self)
|
||||
self.con_actions = submission.custom_context_events()
|
||||
# logger.debug(f"Menu options: {self.con_actions}")
|
||||
for k in self.con_actions.keys():
|
||||
# logger.debug(f"Adding {k}")
|
||||
action = QAction(k, self)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
'''
|
||||
Contains all submission related frontend functions
|
||||
'''
|
||||
import sys
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QPushButton, QVBoxLayout,
|
||||
QComboBox, QDateEdit, QLineEdit, QLabel
|
||||
@@ -190,7 +192,8 @@ class SubmissionFormWidget(QWidget):
|
||||
self.app = parent.app
|
||||
self.pyd = submission
|
||||
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'])
|
||||
self.recover = defaults['form_recover']
|
||||
self.ignore = defaults['form_ignore']
|
||||
@@ -215,7 +218,7 @@ class SubmissionFormWidget(QWidget):
|
||||
value = self.pyd.model_extra[k]
|
||||
except KeyError:
|
||||
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)
|
||||
if add_widget is not None:
|
||||
self.layout.addWidget(add_widget)
|
||||
@@ -224,7 +227,7 @@ class SubmissionFormWidget(QWidget):
|
||||
self.setStyleSheet(main_form_style)
|
||||
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,
|
||||
disable: bool = False) -> "self.InfoItem":
|
||||
"""
|
||||
@@ -240,6 +243,8 @@ class SubmissionFormWidget(QWidget):
|
||||
self.InfoItem: Form widget to hold name:value
|
||||
"""
|
||||
# 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:
|
||||
match value:
|
||||
case PydReagent():
|
||||
@@ -272,7 +277,7 @@ class SubmissionFormWidget(QWidget):
|
||||
"""
|
||||
extraction_kit = args[0]
|
||||
report = Report()
|
||||
# logger.debug(f"Extraction kit: {extraction_kit}")
|
||||
logger.debug(f"Extraction kit: {extraction_kit}")
|
||||
# NOTE: Remove previous reagent widgets
|
||||
try:
|
||||
old_reagents = self.find_widgets()
|
||||
@@ -284,7 +289,7 @@ class SubmissionFormWidget(QWidget):
|
||||
if isinstance(reagent, self.ReagentFormWidget) or isinstance(reagent, QPushButton):
|
||||
reagent.setParent(None)
|
||||
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:
|
||||
add_widget = self.ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.pyd.extraction_kit)
|
||||
self.layout.addWidget(add_widget)
|
||||
@@ -454,9 +459,11 @@ class SubmissionFormWidget(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:
|
||||
super().__init__(parent)
|
||||
if isinstance(submission_type, str):
|
||||
submission_type = SubmissionType.query(name=submission_type)
|
||||
layout = QVBoxLayout()
|
||||
self.label = self.ParsedQLabel(key=key, value=value)
|
||||
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 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:
|
||||
"""
|
||||
Creates form widget
|
||||
@@ -511,8 +518,10 @@ class SubmissionFormWidget(QWidget):
|
||||
Returns:
|
||||
QWidget: Form object
|
||||
"""
|
||||
if isinstance(submission_type, str):
|
||||
submission_type = SubmissionType.query(name=submission_type)
|
||||
if sub_obj is None:
|
||||
sub_obj = SubmissionType.query(name=submission_type).get_submission_class()
|
||||
sub_obj = submission_type.get_submission_class()
|
||||
try:
|
||||
value = value['value']
|
||||
except (TypeError, KeyError):
|
||||
@@ -544,7 +553,8 @@ class SubmissionFormWidget(QWidget):
|
||||
add_widget = MyQComboBox(scrollWidget=parent)
|
||||
# NOTE: lookup existing kits by 'submission_type' decided on by sheetparser
|
||||
# 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
|
||||
# logger.debug(f"Kits received for {submission_type}: {uses}")
|
||||
if check_not_nan(value):
|
||||
@@ -668,7 +678,7 @@ class SubmissionFormWidget(QWidget):
|
||||
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?")
|
||||
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,
|
||||
expiry=self.reagent.expiry,
|
||||
name=self.reagent.name)
|
||||
|
||||
@@ -42,7 +42,6 @@ class Summary(QWidget):
|
||||
self.setLayout(self.layout)
|
||||
self.get_report()
|
||||
|
||||
|
||||
def get_report(self):
|
||||
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():
|
||||
|
||||
@@ -17,13 +17,15 @@ from sqlalchemy import create_engine, text, MetaData
|
||||
from pydantic import field_validator, BaseModel, Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from typing import Any, Tuple, Literal, List
|
||||
print(inspect.stack()[1])
|
||||
# print(inspect.stack()[1])
|
||||
from __init__ import project_path
|
||||
from configparser import ConfigParser
|
||||
from tkinter import Tk # NOTE: This is for choosing database path before app is created.
|
||||
from tkinter.filedialog import askdirectory
|
||||
from sqlalchemy.exc import IntegrityError as sqlalcIntegrityError
|
||||
from pytz import timezone as tz
|
||||
|
||||
timezone = tz("America/Winnipeg")
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
@@ -386,16 +388,22 @@ class Settings(BaseSettings, extra="allow"):
|
||||
case "sqlite":
|
||||
value = f"/{values.data['database_path']}"
|
||||
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 _:
|
||||
# print(pprint.pprint(values.data))
|
||||
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'] }}")
|
||||
value = tmp.render(values=values.data)
|
||||
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)
|
||||
# print(f"Using {database_path} for database path")
|
||||
print(f"Using {database_path} for database path")
|
||||
engine = create_engine(database_path)
|
||||
session = Session(engine)
|
||||
return session
|
||||
@@ -939,8 +947,7 @@ def report_result(func):
|
||||
|
||||
"""
|
||||
def wrapper(*args, **kwargs):
|
||||
# logger.debug(f"Arguments: {args}")
|
||||
# logger.debug(f"Keyword arguments: {kwargs}")
|
||||
logger.debug(f"Report result being called by {func.__name__}")
|
||||
output = func(*args, **kwargs)
|
||||
match output:
|
||||
case Report():
|
||||
@@ -966,6 +973,12 @@ def report_result(func):
|
||||
except Exception as e:
|
||||
logger.error(f"Problem reporting due to {e}")
|
||||
logger.error(result.msg)
|
||||
# logger.debug(f"Returning: {output}")
|
||||
return output
|
||||
if 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
|
||||
|
||||
Reference in New Issue
Block a user