Mid-code cleanup

This commit is contained in:
lwark
2024-12-05 11:19:47 -06:00
parent 51cb5c41a4
commit 5fc02ffeec
8 changed files with 176 additions and 83 deletions

View File

@@ -24,8 +24,8 @@ def set_sqlite_pragma(dbapi_connection, connection_record):
if ctx.database_schema == "sqlite":
execution_phrase = "PRAGMA foreign_keys=ON"
# cursor.execute(execution_phrase)
elif ctx.database_schema == "mssql+pyodbc":
execution_phrase = "SET IDENTITY_INSERT dbo._wastewater ON;"
# elif ctx.database_schema == "mssql+pyodbc":
# execution_phrase = "SET IDENTITY_INSERT dbo._wastewater ON;"
else:
print("Nothing to execute, returning")
cursor.close()

View File

@@ -261,9 +261,9 @@ class KitType(BaseClass):
Returns:
dict: Dictionary containing relevant info for SubmissionType construction
"""
base_dict = dict(name=self.name)
base_dict['reagent roles'] = []
base_dict['equipment roles'] = []
base_dict = dict(name=self.name, reagent_roles=[], equipment_roles=[])
# base_dict['reagent roles'] = []
# base_dict['equipment roles'] = []
for k, v in self.construct_xl_map_for_use(submission_type=submission_type):
# logger.debug(f"Value: {v}")
try:
@@ -272,17 +272,17 @@ class KitType(BaseClass):
continue
for kk, vv in assoc.to_export_dict().items():
v[kk] = vv
base_dict['reagent roles'].append(v)
base_dict['reagent_roles'].append(v)
for k, v in submission_type.construct_field_map("equipment"):
try:
assoc = next(item for item in submission_type.submissiontype_equipmentrole_associations if
item.equipment_role.name == k)
except StopIteration:
continue
for kk, vv in assoc.to_export_dict(kit_type=self).items():
for kk, vv in assoc.to_export_dict(extraction_kit=self).items():
# logger.debug(f"{kk}:{vv}")
v[kk] = vv
base_dict['equipment roles'].append(v)
base_dict['equipment_roles'].append(v)
# logger.debug(f"KT returning {base_dict}")
return base_dict
@@ -360,12 +360,12 @@ class ReagentRole(BaseClass):
assert reagent.role
# logger.debug(f"Looking up reagent type for {type(kit_type)} {kit_type} and {type(reagent)} {reagent}")
# logger.debug(f"Kit reagent types: {kit_type.reagent_types}")
result = list(set(kit_type.reagent_roles).intersection(reagent.role))
result = set(kit_type.reagent_roles).intersection(reagent.role)
# logger.debug(f"Result: {result}")
try:
return result[0]
except IndexError:
return None
# try:
return next((item for item in result), None)
# except IndexError:
# return None
match name:
case str():
# logger.debug(f"Looking up reagent type by name str: {name}")
@@ -445,12 +445,8 @@ class Reagent(BaseClass, LogMixin):
if extraction_kit is not None:
# NOTE: Get the intersection of this reagent's ReagentType and all ReagentTypes in KitType
reagent_role = next((item for item in set(self.role).intersection(extraction_kit.reagent_roles)), self.role[0])
# try:
# reagent_role = list(set(self.role).intersection(extraction_kit.reagent_roles))[0]
# # NOTE: Most will be able to fall back to first ReagentType in itself because most will only have 1.
# except:
# reagent_role = self.role[0]
reagent_role = next((item for item in set(self.role).intersection(extraction_kit.reagent_roles)),
self.role[0])
else:
try:
reagent_role = self.role[0]
@@ -580,11 +576,14 @@ class Reagent(BaseClass, LogMixin):
for key, value in vars.items():
match key:
case "expiry":
if not isinstance(value, date):
field_value = datetime.strptime(value, "%Y-%m-%d").date
field_value.replace(tzinfo=timezone)
if isinstance(value, str):
field_value = datetime.strptime(value, "%Y-%m-%d")
# field_value.replace(tzinfo=timezone)
elif isinstance(value, date):
field_value = datetime.combine(value, datetime.min.time())
else:
field_value = value
field_value.replace(tzinfo=timezone)
case "role":
continue
case _:
@@ -792,6 +791,15 @@ class SubmissionType(BaseClass):
return self.sample_map
def construct_field_map(self, field: Literal['equipment', 'tip']) -> Generator[(str, dict), None, None]:
"""
Make a map of all locations for tips or equipment.
Args:
field (Literal['equipment', 'tip']): the field to construct a map for
Returns:
Generator[(str, dict), None, None]: Generator composing key, locations for each item in the map
"""
for item in self.__getattribute__(f"submissiontype_{field}role_associations"):
fmap = item.uses
if fmap is None:
@@ -799,6 +807,12 @@ class SubmissionType(BaseClass):
yield getattr(item, f"{field}_role").name, fmap
def get_default_kit(self) -> KitType | None:
"""
If only one kits exists for this Submission Type, return it.
Returns:
KitType | None:
"""
if len(self.kit_types) == 1:
return self.kit_types[0]
else:
@@ -1379,7 +1393,7 @@ class Equipment(BaseClass):
else:
return {k: v for k, v in self.__dict__.items()}
def get_processes(self, submission_type: SubmissionType, extraction_kit: str | KitType | None = None) -> List[str]:
def get_processes(self, submission_type: str | SubmissionType | None = None, extraction_kit: str | KitType | None = None) -> List[str]:
"""
Get all processes associated with this Equipment for a given SubmissionType
@@ -1390,23 +1404,35 @@ class Equipment(BaseClass):
Returns:
List[Process]: List of process names
"""
processes = [process for process in self.processes if submission_type in process.submission_types]
match extraction_kit:
case str():
# logger.debug(f"Filtering processes by extraction_kit str {extraction_kit}")
processes = [process for process in processes if
extraction_kit in [kit.name for kit in process.kit_types]]
case KitType():
# logger.debug(f"Filtering processes by extraction_kit KitType {extraction_kit}")
processes = [process for process in processes if extraction_kit in process.kit_types]
case _:
pass
# NOTE: Convert to strings
processes = [process.name for process in processes]
assert all([isinstance(process, str) for process in processes])
if len(processes) == 0:
processes = ['']
return processes
if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type)
if isinstance(extraction_kit, str):
extraction_kit = KitType.query(name=extraction_kit)
for process in self.processes:
if submission_type not in process.submission_types:
continue
if extraction_kit and extraction_kit not in process.kit_types:
continue
yield process
# processes = (process for process in self.processes if submission_type in process.submission_types)
# match extraction_kit:
# case str():
# # logger.debug(f"Filtering processes by extraction_kit str {extraction_kit}")
# processes = (process for process in processes if
# extraction_kit in [kit.name for kit in process.kit_types])
# case KitType():
# # logger.debug(f"Filtering processes by extraction_kit KitType {extraction_kit}")
# processes = (process for process in processes if extraction_kit in process.kit_types)
# case _:
# pass
# # NOTE: Convert to strings
# # processes = [process.name for process in processes]
# # assert all([isinstance(process, str) for process in processes])
# # if len(processes) == 0:
# # processes = ['']
# # return processes
# for process in processes:
# yield process.name
@classmethod
@setup_lookup
@@ -1452,8 +1478,7 @@ class Equipment(BaseClass):
pass
return cls.execute_query(query=query, limit=limit)
def to_pydantic(self, submission_type: SubmissionType,
extraction_kit: str | KitType | None = None,
def to_pydantic(self, submission_type: SubmissionType, extraction_kit: str | KitType | None = None,
role: str = None) -> "PydEquipment":
"""
Creates PydEquipment of this Equipment
@@ -1466,8 +1491,8 @@ class Equipment(BaseClass):
PydEquipment: pydantic equipment object
"""
from backend.validators.pydant import PydEquipment
return PydEquipment(
processes=self.get_processes(submission_type=submission_type, extraction_kit=extraction_kit), role=role,
processes = self.get_processes(submission_type=submission_type, extraction_kit=extraction_kit)
return PydEquipment(processes=processes, role=role,
**self.to_dict(processes=False))
@classmethod
@@ -1603,7 +1628,7 @@ class EquipmentRole(BaseClass):
return cls.execute_query(query=query, limit=limit)
def get_processes(self, submission_type: str | SubmissionType | None,
extraction_kit: str | KitType | None = None) -> List[Process]:
extraction_kit: str | KitType | None = None) -> Generator[Process, None, None]:
"""
Get processes used by this EquipmentRole
@@ -1612,30 +1637,38 @@ class EquipmentRole(BaseClass):
extraction_kit (str | KitType | None, optional): KitType of interest. Defaults to None.
Returns:
List[Process]: _description_
List[Process]: List of processes
"""
if isinstance(submission_type, str):
# logger.debug(f"Checking if str {submission_type} exists")
submission_type = SubmissionType.query(name=submission_type)
if submission_type is not None:
# logger.debug("Getting all processes for this EquipmentRole")
processes = [process for process in self.processes if submission_type in process.submission_types]
else:
processes = self.processes
match extraction_kit:
case str():
# logger.debug(f"Filtering processes by extraction_kit str {extraction_kit}")
processes = [item for item in processes if extraction_kit in [kit.name for kit in item.kit_types]]
case KitType():
# logger.debug(f"Filtering processes by extraction_kit KitType {extraction_kit}")
processes = [item for item in processes if extraction_kit in [kit for kit in item.kit_types]]
case _:
pass
output = [item.name for item in processes]
if len(output) == 0:
return ['']
else:
return output
if isinstance(extraction_kit, str):
extraction_kit = KitType.query(name=extraction_kit)
for process in self.processes:
if submission_type and submission_type not in process.submission_types:
continue
if extraction_kit and extraction_kit not in process.kit_types:
continue
yield process.name
# if submission_type is not None:
# # logger.debug("Getting all processes for this EquipmentRole")
# processes = [process for process in self.processes if submission_type in process.submission_types]
# else:
# processes = self.processes
# match extraction_kit:
# case str():
# # logger.debug(f"Filtering processes by extraction_kit str {extraction_kit}")
# processes = [item for item in processes if extraction_kit in [kit.name for kit in item.kit_types]]
# case KitType():
# # logger.debug(f"Filtering processes by extraction_kit KitType {extraction_kit}")
# processes = [item for item in processes if extraction_kit in [kit for kit in item.kit_types]]
# case _:
# pass
# output = [item.name for item in processes]
# if len(output) == 0:
# return ['']
# else:
# return output
def to_export_dict(self, submission_type: SubmissionType, kit_type: KitType):
"""
@@ -1644,8 +1677,8 @@ class EquipmentRole(BaseClass):
Returns:
dict: dictionary of Association and related reagent role
"""
return dict(role=self.name,
processes=self.get_processes(submission_type=submission_type, extraction_kit=kit_type))
processes = self.get_processes(submission_type=submission_type, extraction_kit=kit_type)
return dict(role=self.name, processes=[item for item in processes])
class SubmissionEquipmentAssociation(BaseClass):

View File

@@ -1074,6 +1074,7 @@ class BasicSubmission(BaseClass, LogMixin):
@setup_lookup
def query(cls,
submission_type: str | SubmissionType | None = None,
submission_type_name: str|None = None,
id: int | str | None = None,
rsl_plate_num: str | None = None,
start_date: date | str | int | None = None,
@@ -1173,6 +1174,11 @@ class BasicSubmission(BaseClass, LogMixin):
limit = 1
case _:
pass
match submission_type_name:
case str():
query = query.filter(model.submission_type_name == submission_type_name)
case _:
pass
# NOTE: by id (returns only a single value)
match id:
case int():
@@ -1389,13 +1395,23 @@ class BasicSubmission(BaseClass, LogMixin):
@classmethod
def calculate_turnaround(cls, start_date:date|None=None, end_date:date|None=None) -> Tuple[int|None, bool|None]:
if 'pytest' not in sys.modules:
from tools import ctx
else:
from test_settings import ctx
if not end_date:
return None, None
try:
delta = np.busday_count(start_date, end_date, holidays=create_holidays_for_year(start_date.year)) + 1
except ValueError:
return None, None
return delta, delta <= ctx.TaT_threshold
try:
tat = cls.get_default_info("turnaround_time")
except (AttributeError, KeyError):
tat = None
if not tat:
tat = ctx.TaT_threshold
return delta, delta <= tat
# Below are the custom submission types

View File

@@ -142,18 +142,18 @@ class ReportMaker(object):
class TurnaroundMaker(object):
def __init__(self, start_date: date, end_date: date):
def __init__(self, start_date: date, end_date: date, submission_type:str):
self.start_date = start_date
self.end_date = end_date
# NOTE: Set page size to zero to override limiting query size.
self.subs = BasicSubmission.query(start_date=start_date, end_date=end_date, page_size=0)
self.subs = BasicSubmission.query(start_date=start_date, end_date=end_date, submission_type_name=submission_type, page_size=0)
records = [self.build_record(sub) for sub in self.subs]
self.df = DataFrame.from_records(records)
@classmethod
def build_record(cls, sub):
days, tat_ok = sub.get_turnaround_time()
return dict(name=sub.rsl_plate_num, days=days, submitted_date=sub.submitted_date,
return dict(name=str(sub.rsl_plate_num), days=days, submitted_date=sub.submitted_date,
completed_date=sub.completed_date, acceptable=tat_ok)
def write_report(self, filename: Path | str, obj: QWidget | None = None):

View File

@@ -9,6 +9,7 @@ from datetime import date, datetime, timedelta
from dateutil.parser import parse
from dateutil.parser import ParserError
from typing import List, Tuple, Literal
from types import GeneratorType
from . import RSLNamer
from pathlib import Path
from tools import check_not_nan, convert_nans_to_nones, Report, Result, timezone
@@ -343,6 +344,8 @@ class PydEquipment(BaseModel, extra='ignore'):
@classmethod
def make_empty_list(cls, value):
# logger.debug(f"Pydantic value: {value}")
if isinstance(value, GeneratorType):
value = [item.name for item in value]
value = convert_nans_to_nones(value)
if not value:
value = ['']
@@ -380,6 +383,7 @@ class PydEquipment(BaseModel, extra='ignore'):
assoc = None
if assoc is None:
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=equipment)
# TODO: This seems precarious. What if there is more than one process?
process = Process.query(name=self.processes[0])
if process is None:
logger.error(f"Found unknown process: {process}.")
@@ -1122,6 +1126,13 @@ class PydEquipmentRole(BaseModel):
equipment: List[PydEquipment]
processes: List[str] | None
@field_validator("processes", mode="before")
@classmethod
def expand_processes(cls, value):
if isinstance(value, GeneratorType):
value = [item for item in value]
return value
def to_form(self, parent, used: list) -> "RoleComboBox":
"""
Creates a widget for user input into this class.

View File

@@ -11,25 +11,33 @@ logger = logging.getLogger(f"submissions.{__name__}")
class TurnaroundChart(CustomFigure):
def __init__(self, df: pd.DataFrame, modes: list, settings: dict, ytitle: str | None = None,
def __init__(self, df: pd.DataFrame, modes: list, settings: dict, threshold: float | None = None,
ytitle: str | None = None,
parent: QWidget | None = None,
months: int = 6):
super().__init__(df=df, modes=modes, settings=settings)
self.df = df
try:
months = int(settings['months'])
except KeyError:
months = 6
# logger.debug(f"DF: {self.df}")
self.construct_chart(df=df)
self.add_hline(y=3.5)
self.construct_chart()
if threshold:
self.add_hline(y=threshold)
# self.update_xaxes()
self.update_layout(showlegend=False)
def construct_chart(self, df: pd.DataFrame):
def construct_chart(self, df: pd.DataFrame | None = None):
if df:
self.df = df
# logger.debug(f"PCR df:\n {df}")
df = df.sort_values(by=['submitted_date', 'name'])
self.df = self.df[self.df.days.notnull()]
self.df = self.df.sort_values(['submitted_date', 'name'], ascending=[True, True]).reset_index(drop=True)
self.df = self.df.reset_index().rename(columns={"index": "idx"})
# logger.debug(f"DF: {self.df}")
try:
scatter = px.scatter(data_frame=df, x='name', y="days",
scatter = px.scatter(data_frame=self.df, x='idx', y="days",
hover_data=["name", "submitted_date", "completed_date", "days"],
color="acceptable", color_discrete_map={True: "green", False: "red"}
)
@@ -37,3 +45,12 @@ class TurnaroundChart(CustomFigure):
scatter = px.scatter()
self.add_traces(scatter.data)
self.update_traces(marker={'size': 15})
tickvals = self.df['idx'].tolist()
ticklabels = self.df['name'].tolist()
self.update_layout(
xaxis=dict(
tickmode='array',
tickvals=tickvals,
ticktext=ticklabels,
)
)

View File

@@ -8,7 +8,7 @@ from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtCore import Qt, pyqtSlot
from jinja2 import TemplateNotFound
from backend.db.models import BasicSubmission, BasicSample, Reagent, KitType
from tools import is_power_user, jinja_template_loading
from tools import is_power_user, jinja_template_loading, timezone
from .functions import select_save_file
from .misc import save_pdf
from pathlib import Path
@@ -176,7 +176,8 @@ class SubmissionDetails(QDialog):
if isinstance(submission, str):
submission = BasicSubmission.query(rsl_plate_num=submission)
submission.signed_by = getuser()
submission.completed_date = datetime.now().date()
submission.completed_date = datetime.now()
submission.completed_date.replace(tzinfo=timezone)
submission.save()
self.submission_details(submission=self.rsl_plate_num)

View File

@@ -1,10 +1,10 @@
from PyQt6.QtCore import QSignalBlocker
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QWidget, QGridLayout, QPushButton, QLabel
from PyQt6.QtWidgets import QWidget, QGridLayout, QPushButton, QLabel, QComboBox
from .info_tab import InfoPane
from backend.excel.reports import TurnaroundMaker
from pandas import DataFrame
from backend.db import BasicSubmission
from backend.db import BasicSubmission, SubmissionType
from frontend.visualizations.turnaround_chart import TurnaroundChart
import logging
@@ -17,6 +17,11 @@ class TurnaroundTime(InfoPane):
super().__init__(parent)
self.chart = None
self.report_object = None
self.submission_typer = QComboBox(self)
subs = ["Any"] + [item.name for item in SubmissionType.query()]
self.submission_typer.addItems(subs)
self.layout.addWidget(self.submission_typer, 1, 1, 1, 3)
self.submission_typer.currentTextChanged.connect(self.date_changed)
self.date_changed()
def date_changed(self):
@@ -31,6 +36,16 @@ class TurnaroundTime(InfoPane):
return
super().date_changed()
chart_settings = dict(start_date=self.start_date, end_date=self.end_date)
self.report_obj = TurnaroundMaker(start_date=self.start_date, end_date=self.end_date)
self.chart = TurnaroundChart(df=self.report_obj.df, settings=chart_settings, modes=[])
if self.submission_typer.currentText() == "Any":
submission_type = None
subtype_obj = None
else:
submission_type = self.submission_typer.currentText()
subtype_obj = SubmissionType.query(name = submission_type)
self.report_obj = TurnaroundMaker(start_date=self.start_date, end_date=self.end_date, submission_type=submission_type)
if subtype_obj:
threshold = subtype_obj.defaults['turnaround_time'] + 0.5
else:
threshold = None
self.chart = TurnaroundChart(df=self.report_obj.df, settings=chart_settings, modes=[], threshold=threshold)
self.webview.setHtml(self.chart.to_html())