post documentation and code clean-up.

This commit is contained in:
lwark
2024-07-03 15:13:55 -05:00
parent 12ca3157e5
commit c460e5eeca
21 changed files with 421 additions and 238 deletions

View File

@@ -9,6 +9,7 @@ def set_sqlite_pragma(dbapi_connection, connection_record):
""" """
*should* allow automatic creation of foreign keys in the database *should* allow automatic creation of foreign keys in the database
I have no idea how it actually works. I have no idea how it actually works.
Listens for connect and then turns on foreign keys?
Args: Args:
dbapi_connection (_type_): _description_ dbapi_connection (_type_): _description_

View File

@@ -1581,12 +1581,12 @@ class WastewaterArtic(BasicSubmission):
dict: dictionary used in submissions summary dict: dictionary used in submissions summary
""" """
output = super().to_dict(full_data=full_data, backup=backup, report=report) output = super().to_dict(full_data=full_data, backup=backup, report=report)
if report:
return output
if self.artic_technician in [None, "None"]: if self.artic_technician in [None, "None"]:
output['artic_technician'] = self.technician output['artic_technician'] = self.technician
else: else:
output['artic_technician'] = self.artic_technician output['artic_technician'] = self.artic_technician
if report:
return output
output['gel_info'] = self.gel_info output['gel_info'] = self.gel_info
output['gel_image_path'] = self.gel_image output['gel_image_path'] = self.gel_image
output['dna_core_submission_number'] = self.dna_core_submission_number output['dna_core_submission_number'] = self.dna_core_submission_number
@@ -2253,6 +2253,12 @@ class BasicSample(BaseClass):
@classmethod @classmethod
def get_searchables(cls): def get_searchables(cls):
"""
Delivers a list of fields that can be used in fuzzy search.
Returns:
List[str]: List of fields.
"""
return [dict(label="Submitter ID", field="submitter_id")] return [dict(label="Submitter ID", field="submitter_id")]
@classmethod @classmethod
@@ -2381,22 +2387,14 @@ class WastewaterSample(BasicSample):
output_dict["submitter_id"] = output_dict['ww_full_sample_id'] output_dict["submitter_id"] = output_dict['ww_full_sample_id']
return output_dict return output_dict
def get_previous_ww_submission(self, current_artic_submission: WastewaterArtic):
try:
plates = [item['plate'] for item in current_artic_submission.source_plates]
except TypeError as e:
logger.error(f"source_plates must not be present")
plates = [item.rsl_plate_num for item in
self.submissions[:self.submissions.index(current_artic_submission)]]
subs = [sub for sub in self.submissions if sub.rsl_plate_num in plates]
# logger.debug(f"Submissions: {subs}")
try:
return subs[-1]
except IndexError:
return None
@classmethod @classmethod
def get_searchables(cls): def get_searchables(cls) -> List[str]:
"""
Delivers a list of fields that can be used in fuzzy search. Extends parent.
Returns:
List[str]: List of fields.
"""
searchables = super().get_searchables() searchables = super().get_searchables()
for item in ["ww_processing_num", "ww_full_sample_id", "rsl_number"]: for item in ["ww_processing_num", "ww_full_sample_id", "rsl_number"]:
label = item.strip("ww_").replace("_", " ").replace("rsl", "RSL").title() label = item.strip("ww_").replace("_", " ").replace("rsl", "RSL").title()

View File

@@ -1,6 +1,7 @@
''' '''
Contains pandas convenience functions for interacting with excel workbooks Contains pandas and openpyxl convenience functions for interacting with excel workbooks
''' '''
from .reports import * from .reports import *
from .parser import * from .parser import *
from .writer import *

View File

@@ -1,5 +1,5 @@
''' '''
contains parser object for pulling values from client generated submission sheets. contains parser objects for pulling values from client generated submission sheets.
''' '''
import sys import sys
from copy import copy from copy import copy
@@ -78,7 +78,7 @@ class SheetParser(object):
def parse_reagents(self, extraction_kit: str | None = None): def parse_reagents(self, extraction_kit: str | None = None):
""" """
Pulls reagent info from the excel sheet Calls reagent parser class to pull info from the excel sheet
Args: Args:
extraction_kit (str | None, optional): Relevant extraction kit for reagent map. Defaults to None. extraction_kit (str | None, optional): Relevant extraction kit for reagent map. Defaults to None.
@@ -91,16 +91,22 @@ class SheetParser(object):
def parse_samples(self): def parse_samples(self):
""" """
Pulls sample info from the excel sheet Calls sample parser to pull info from the excel sheet
""" """
parser = SampleParser(xl=self.xl, submission_type=self.submission_type) parser = SampleParser(xl=self.xl, submission_type=self.submission_type)
self.sub['samples'] = parser.reconcile_samples() self.sub['samples'] = parser.reconcile_samples()
def parse_equipment(self): def parse_equipment(self):
"""
Calls equipment parser to pull info from the excel sheet
"""
parser = EquipmentParser(xl=self.xl, submission_type=self.submission_type) parser = EquipmentParser(xl=self.xl, submission_type=self.submission_type)
self.sub['equipment'] = parser.parse_equipment() self.sub['equipment'] = parser.parse_equipment()
def parse_tips(self): def parse_tips(self):
"""
Calls tips parser to pull info from the excel sheet
"""
parser = TipParser(xl=self.xl, submission_type=self.submission_type) parser = TipParser(xl=self.xl, submission_type=self.submission_type)
self.sub['tips'] = parser.parse_tips() self.sub['tips'] = parser.parse_tips()
@@ -160,8 +166,16 @@ class SheetParser(object):
class InfoParser(object): class InfoParser(object):
"""
Object to parse generic info from excel sheet.
"""
def __init__(self, xl: Workbook, submission_type: str|SubmissionType, sub_object: BasicSubmission|None=None): def __init__(self, xl: Workbook, submission_type: str|SubmissionType, sub_object: BasicSubmission|None=None):
"""
Args:
xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.)
sub_object (BasicSubmission | None, optional): Submission object holding methods. Defaults to None.
"""
logger.info(f"\n\nHello from InfoParser!\n\n") logger.info(f"\n\nHello from InfoParser!\n\n")
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
@@ -221,6 +235,7 @@ class InfoParser(object):
new['name'] = k new['name'] = k
relevant.append(new) relevant.append(new)
# logger.debug(f"relevant map for {sheet}: {pformat(relevant)}") # logger.debug(f"relevant map for {sheet}: {pformat(relevant)}")
# NOTE: make sure relevant is not an empty list.
if not relevant: if not relevant:
continue continue
for item in relevant: for item in relevant:
@@ -231,6 +246,7 @@ class InfoParser(object):
case "submission_type": case "submission_type":
value, missing = is_missing(value) value, missing = is_missing(value)
value = value.title() value = value.title()
# NOTE: is field a JSON?
case thing if thing in self.sub_object.jsons(): case thing if thing in self.sub_object.jsons():
value, missing = is_missing(value) value, missing = is_missing(value)
if missing: continue if missing: continue
@@ -248,12 +264,23 @@ class InfoParser(object):
dicto[item['name']] = dict(value=value, missing=missing) dicto[item['name']] = dict(value=value, missing=missing)
except (KeyError, IndexError): except (KeyError, IndexError):
continue continue
# Return after running the parser components held in submission object.
return self.sub_object.custom_info_parser(input_dict=dicto, xl=self.xl) return self.sub_object.custom_info_parser(input_dict=dicto, xl=self.xl)
class ReagentParser(object): class ReagentParser(object):
"""
Object to pull reagents from excel sheet.
"""
def __init__(self, xl: Workbook, submission_type: str, extraction_kit: str, sub_object:BasicSubmission|None=None): def __init__(self, xl: Workbook, submission_type: str, extraction_kit: str, sub_object:BasicSubmission|None=None):
"""
Args:
xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (str): Type of submission expected (Wastewater, Bacterial Culture, etc.)
extraction_kit (str): Extraction kit used.
sub_object (BasicSubmission | None, optional): Submission object holding methods. Defaults to None.
"""
# logger.debug("\n\nHello from ReagentParser!\n\n") # logger.debug("\n\nHello from ReagentParser!\n\n")
self.submission_type_obj = submission_type self.submission_type_obj = submission_type
self.sub_object = sub_object self.sub_object = sub_object
@@ -284,7 +311,7 @@ class ReagentParser(object):
pass pass
return reagent_map return reagent_map
def parse_reagents(self) -> List[PydReagent]: def parse_reagents(self) -> List[dict]:
""" """
Extracts reagent information from the excel form. Extracts reagent information from the excel form.
@@ -312,7 +339,7 @@ class ReagentParser(object):
comment = "" comment = ""
except (KeyError, IndexError): except (KeyError, IndexError):
listo.append( listo.append(
PydReagent(role=item.strip(), lot=None, expiry=None, name=None, comment="", missing=True)) dict(role=item.strip(), lot=None, expiry=None, name=None, comment="", missing=True))
continue continue
# NOTE: If the cell is blank tell the PydReagent # NOTE: If the cell is blank tell the PydReagent
if check_not_nan(lot): if check_not_nan(lot):
@@ -336,17 +363,17 @@ class ReagentParser(object):
class SampleParser(object): class SampleParser(object):
""" """
object to pull data for samples in excel sheet and construct individual sample objects Object to pull data for samples in excel sheet and construct individual sample objects
""" """
def __init__(self, xl: Workbook, submission_type: SubmissionType, sample_map: dict | None = None, sub_object:BasicSubmission|None=None) -> None: def __init__(self, xl: Workbook, submission_type: SubmissionType, sample_map: dict | None = None, sub_object:BasicSubmission|None=None) -> None:
""" """
convert sample sub-dataframe to dictionary of records
Args: Args:
df (pd.DataFrame): input sample dataframe xl (Workbook): Openpyxl workbook from submitted excel file.
elution_map (pd.DataFrame | None, optional): optional map of elution plate. Defaults to None. submission_type (SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.)
""" sample_map (dict | None, optional): Locations in database where samples are found. Defaults to None.
sub_object (BasicSubmission | None, optional): Submission object holding methods. Defaults to None.
"""
# logger.debug("\n\nHello from SampleParser!\n\n") # logger.debug("\n\nHello from SampleParser!\n\n")
self.samples = [] self.samples = []
self.xl = xl self.xl = xl
@@ -383,10 +410,13 @@ class SampleParser(object):
sample_info_map = sample_map sample_info_map = sample_map
return sample_info_map return sample_info_map
def parse_plate_map(self): def parse_plate_map(self) -> List[dict]:
""" """
Parse sample location/name from plate map Parse sample location/name from plate map
"""
Returns:
List[dict]: List of sample ids and locations.
"""
invalids = [0, "0", "EMPTY"] invalids = [0, "0", "EMPTY"]
smap = self.sample_info_map['plate_map'] smap = self.sample_info_map['plate_map']
ws = self.xl[smap['sheet']] ws = self.xl[smap['sheet']]
@@ -412,7 +442,11 @@ class SampleParser(object):
def parse_lookup_table(self) -> List[dict]: def parse_lookup_table(self) -> List[dict]:
""" """
Parse misc info from lookup table. Parse misc info from lookup table.
"""
Returns:
List[dict]: List of basic sample info.
"""
lmap = self.sample_info_map['lookup_table'] lmap = self.sample_info_map['lookup_table']
ws = self.xl[lmap['sheet']] ws = self.xl[lmap['sheet']]
lookup_samples = [] lookup_samples = []
@@ -460,7 +494,13 @@ class SampleParser(object):
new_samples.append(PydSample(**translated_dict)) new_samples.append(PydSample(**translated_dict))
return result, new_samples return result, new_samples
def reconcile_samples(self): def reconcile_samples(self) -> List[dict]:
"""
Merges sample info from lookup table and plate map.
Returns:
List[dict]: Reconciled samples
"""
# TODO: Move to pydantic validator? # TODO: Move to pydantic validator?
if self.plate_map_samples is None or self.lookup_samples is None: if self.plate_map_samples is None or self.lookup_samples is None:
self.samples = self.lookup_samples or self.plate_map_samples self.samples = self.lookup_samples or self.plate_map_samples
@@ -504,8 +544,15 @@ class SampleParser(object):
class EquipmentParser(object): class EquipmentParser(object):
"""
Object to pull data for equipment in excel sheet
"""
def __init__(self, xl: Workbook, submission_type: str|SubmissionType) -> None: def __init__(self, xl: Workbook, submission_type: str|SubmissionType) -> None:
"""
Args:
xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.)
"""
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
self.submission_type = submission_type self.submission_type = submission_type
@@ -582,8 +629,15 @@ class EquipmentParser(object):
class TipParser(object): class TipParser(object):
"""
Object to pull data for tips in excel sheet
"""
def __init__(self, xl: Workbook, submission_type: str|SubmissionType) -> None: def __init__(self, xl: Workbook, submission_type: str|SubmissionType) -> None:
"""
Args:
xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.)
"""
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
self.submission_type = submission_type self.submission_type = submission_type
@@ -644,8 +698,6 @@ class PCRParser(object):
def __init__(self, filepath: Path | None=None, submission: BasicSubmission | None=None) -> None: def __init__(self, filepath: Path | None=None, submission: BasicSubmission | None=None) -> None:
""" """
Initializes object.
Args: Args:
filepath (Path | None, optional): file to parse. Defaults to None. filepath (Path | None, optional): file to parse. Defaults to None.
""" """

View File

@@ -90,6 +90,13 @@ class ReportMaker(object):
return html return html
def write_report(self, filename: Path | str, obj: QWidget | None = None): def write_report(self, filename: Path | str, obj: QWidget | None = None):
"""
Writes info to files.
Args:
filename (Path | str): Basename of output file
obj (QWidget | None, optional): Parent object. Defaults to None.
"""
if isinstance(filename, str): if isinstance(filename, str):
filename = Path(filename) filename = Path(filename)
filename = filename.absolute() filename = filename.absolute()
@@ -108,6 +115,9 @@ class ReportMaker(object):
self.writer.close() self.writer.close()
def fix_up_xl(self): def fix_up_xl(self):
"""
Handles formatting of xl file.
"""
# logger.debug(f"Updating worksheet") # logger.debug(f"Updating worksheet")
worksheet: Worksheet = self.writer.sheets['Report'] worksheet: Worksheet = self.writer.sheets['Report']
for idx, col in enumerate(self.summary_df, start=1): # loop through all columns for idx, col in enumerate(self.summary_df, start=1): # loop through all columns

View File

@@ -1,3 +1,6 @@
'''
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 operator import itemgetter
@@ -27,8 +30,9 @@ class SheetWriter(object):
def __init__(self, submission: PydSubmission, missing_only: bool = False): def __init__(self, submission: PydSubmission, missing_only: bool = False):
""" """
Args: Args:
filepath (Path | None, optional): file path to excel sheet. Defaults to None. submission (PydSubmission): Object containing submission information.
""" missing_only (bool, optional): Whether to only fill in missing values. Defaults to False.
"""
self.sub = OrderedDict(submission.improved_dict()) self.sub = OrderedDict(submission.improved_dict())
for k, v in self.sub.items(): for k, v in self.sub.items():
match k: match k:
@@ -116,6 +120,13 @@ class InfoWriter(object):
def __init__(self, xl: Workbook, submission_type: SubmissionType | str, info_dict: dict, def __init__(self, xl: Workbook, submission_type: SubmissionType | str, info_dict: dict,
sub_object: BasicSubmission | None = None): sub_object: BasicSubmission | None = None):
"""
Args:
xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (SubmissionType | str): Type of submission expected (Wastewater, Bacterial Culture, etc.)
info_dict (dict): Dictionary of information to write.
sub_object (BasicSubmission | None, optional): Submission object containing methods. Defaults to None.
"""
logger.debug(f"Info_dict coming into InfoWriter: {pformat(info_dict)}") logger.debug(f"Info_dict coming into InfoWriter: {pformat(info_dict)}")
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
@@ -186,6 +197,13 @@ class ReagentWriter(object):
def __init__(self, xl: Workbook, submission_type: SubmissionType | str, extraction_kit: KitType | str, def __init__(self, xl: Workbook, submission_type: SubmissionType | str, extraction_kit: KitType | str,
reagent_list: list): reagent_list: list):
"""
Args:
xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (SubmissionType | str): Type of submission expected (Wastewater, Bacterial Culture, etc.)
extraction_kit (KitType | str): Extraction kit used.
reagent_list (list): List of reagent dicts to be written to excel.
"""
self.xl = xl self.xl = xl
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
@@ -245,8 +263,13 @@ class SampleWriter(object):
""" """
object to write sample data into excel file object to write sample data into excel file
""" """
def __init__(self, xl: Workbook, submission_type: SubmissionType | str, sample_list: list): def __init__(self, xl: Workbook, submission_type: SubmissionType | str, sample_list: list):
"""
Args:
xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (SubmissionType | str): Type of submission expected (Wastewater, Bacterial Culture, etc.)
sample_list (list): List of sample dictionaries to be written to excel file.
"""
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
self.submission_type = submission_type self.submission_type = submission_type
@@ -303,6 +326,12 @@ class EquipmentWriter(object):
""" """
def __init__(self, xl: Workbook, submission_type: SubmissionType | str, equipment_list: list): def __init__(self, xl: Workbook, submission_type: SubmissionType | str, equipment_list: list):
"""
Args:
xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (SubmissionType | str): Type of submission expected (Wastewater, Bacterial Culture, etc.)
equipment_list (list): List of equipment dictionaries to write to excel file.
"""
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
self.submission_type = submission_type self.submission_type = submission_type
@@ -385,6 +414,12 @@ class TipWriter(object):
""" """
def __init__(self, xl: Workbook, submission_type: SubmissionType | str, tips_list: list): def __init__(self, xl: Workbook, submission_type: SubmissionType | str, tips_list: list):
"""
Args:
xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (SubmissionType | str): Type of submission expected (Wastewater, Bacterial Culture, etc.)
tips_list (list): List of tip dictionaries to write to the excel file.
"""
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
self.submission_type = submission_type self.submission_type = submission_type
@@ -460,8 +495,15 @@ class TipWriter(object):
class DocxWriter(object): class DocxWriter(object):
"""
Object to render
"""
def __init__(self, base_dict: dict): def __init__(self, base_dict: dict):
"""
Args:
base_dict (dict): dictionary of info to be written to template.
"""
self.sub_obj = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=base_dict['submission_type']) self.sub_obj = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=base_dict['submission_type'])
env = jinja_template_loading() env = jinja_template_loading()
temp_name = f"{base_dict['submission_type'].replace(' ', '').lower()}_subdocument.docx" temp_name = f"{base_dict['submission_type'].replace(' ', '').lower()}_subdocument.docx"
@@ -506,7 +548,13 @@ class DocxWriter(object):
output.append(contents) output.append(contents)
return output return output
def create_merged_template(self, *args): def create_merged_template(self, *args) -> BytesIO:
"""
Appends submission specific information
Returns:
BytesIO: Merged docx template
"""
merged_document = Document() merged_document = Document()
output = BytesIO() output = BytesIO()
for index, file in enumerate(args): for index, file in enumerate(args):

View File

@@ -1,3 +1,6 @@
'''
Contains all validators
'''
import logging, re import logging, re
import sys import sys
from pathlib import Path from pathlib import Path

View File

@@ -3,21 +3,18 @@ Contains pydantic models and accompanying validators
''' '''
from __future__ import annotations from __future__ import annotations
import sys import sys
from operator import attrgetter import uuid, re, logging, csv
import uuid, re, logging from pydantic import BaseModel, field_validator, Field, model_validator
from pydantic import BaseModel, field_validator, Field, model_validator, PrivateAttr
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from dateutil.parser import parse from dateutil.parser import parse
from dateutil.parser import ParserError 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, row_map from tools import check_not_nan, convert_nans_to_nones, Report, Result
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
from openpyxl import load_workbook, Workbook
from io import BytesIO
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -106,20 +103,17 @@ class PydReagent(BaseModel):
return values.data['role'] return values.data['role']
def improved_dict(self) -> dict: def improved_dict(self) -> dict:
"""
Constructs a dictionary consisting of model.fields and model.extras
Returns:
dict: Information dictionary
"""
try: try:
extras = list(self.model_extra.keys()) extras = list(self.model_extra.keys())
except AttributeError: except AttributeError:
extras = [] extras = []
fields = list(self.model_fields.keys()) + extras fields = list(self.model_fields.keys()) + extras
# output = {}
# for k in fields:
# value = getattr(self, k)
# match value:
# case date():
# value = value.strftime("%Y-%m-%d")
# case _:
# pass
# output[k] = value
return {k: getattr(self, k) for k in fields} return {k: getattr(self, k) for k in fields}
def toSQL(self, submission: BasicSubmission | str = None) -> Tuple[Reagent, SubmissionReagentAssociation, Report]: def toSQL(self, submission: BasicSubmission | str = None) -> Tuple[Reagent, SubmissionReagentAssociation, Report]:
@@ -142,7 +136,7 @@ class PydReagent(BaseModel):
if isinstance(value, dict): if isinstance(value, dict):
value = value['value'] value = value['value']
# logger.debug(f"Reagent info item for {key}: {value}") # logger.debug(f"Reagent info item for {key}: {value}")
# set fields based on keys in dictionary # NOTE: set fields based on keys in dictionary
match key: match key:
case "lot": case "lot":
reagent.lot = value.upper() reagent.lot = value.upper()
@@ -177,11 +171,9 @@ class PydReagent(BaseModel):
assoc = None assoc = None
# add end-of-life extension from reagent type to expiry date # add end-of-life extension from reagent type to expiry date
# NOTE: this will now be done only in the reporting phase to account for potential changes in end-of-life extensions # NOTE: this will now be done only in the reporting phase to account for potential changes in end-of-life extensions
return reagent, assoc, report return reagent, assoc, report
class PydSample(BaseModel, extra='allow'): class PydSample(BaseModel, extra='allow'):
submitter_id: str submitter_id: str
sample_type: str sample_type: str
@@ -220,6 +212,12 @@ class PydSample(BaseModel, extra='allow'):
return str(value) return str(value)
def improved_dict(self) -> dict: def improved_dict(self) -> dict:
"""
Constructs a dictionary consisting of model.fields and model.extras
Returns:
dict: Information dictionary
"""
fields = list(self.model_fields.keys()) + list(self.model_extra.keys()) fields = list(self.model_fields.keys()) + list(self.model_extra.keys())
return {k: getattr(self, k) for k in fields} return {k: getattr(self, k) for k in fields}
@@ -249,7 +247,6 @@ class PydSample(BaseModel, extra='allow'):
if isinstance(submission, str): if isinstance(submission, str):
submission = BasicSubmission.query(rsl_plate_num=submission) submission = BasicSubmission.query(rsl_plate_num=submission)
assoc_type = submission.submission_type_name assoc_type = submission.submission_type_name
# assoc_type = self.sample_type.replace("Sample", "").strip()
for row, column, aid, submission_rank in zip(self.row, self.column, self.assoc_id, self.submission_rank): for row, column, aid, submission_rank in zip(self.row, self.column, self.assoc_id, self.submission_rank):
# logger.debug(f"Looking up association with identity: ({submission.submission_type_name} Association)") # logger.debug(f"Looking up association with identity: ({submission.submission_type_name} Association)")
# logger.debug(f"Looking up association with identity: ({assoc_type} Association)") # logger.debug(f"Looking up association with identity: ({assoc_type} Association)")
@@ -268,6 +265,12 @@ class PydSample(BaseModel, extra='allow'):
return instance, out_associations, report return instance, out_associations, report
def improved_dict(self) -> dict: def improved_dict(self) -> dict:
"""
Constructs a dictionary consisting of model.fields and model.extras
Returns:
dict: Information dictionary
"""
try: try:
extras = list(self.model_extra.keys()) extras = list(self.model_extra.keys())
except AttributeError: except AttributeError:
@@ -281,7 +284,16 @@ class PydTips(BaseModel):
lot: str|None = Field(default=None) lot: str|None = Field(default=None)
role: str role: str
def to_sql(self, submission:BasicSubmission): def to_sql(self, submission:BasicSubmission) -> SubmissionTipsAssociation:
"""
Con
Args:
submission (BasicSubmission): A submission object to associate tips represented here.
Returns:
SubmissionTipsAssociation: Association between queried tips and submission
"""
tips = Tips.query(name=self.name, lot=self.lot, limit=1) tips = Tips.query(name=self.name, lot=self.lot, limit=1)
assoc = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=self.role) assoc = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=self.role)
return assoc return assoc
@@ -348,6 +360,12 @@ class PydEquipment(BaseModel, extra='ignore'):
return equipment, assoc return equipment, assoc
def improved_dict(self) -> dict: def improved_dict(self) -> dict:
"""
Constructs a dictionary consisting of model.fields and model.extras
Returns:
dict: Information dictionary
"""
try: try:
extras = list(self.model_extra.keys()) extras = list(self.model_extra.keys())
except AttributeError: except AttributeError:
@@ -619,7 +637,14 @@ class PydSubmission(BaseModel, extra='allow'):
self.submission_object = BasicSubmission.find_polymorphic_subclass( self.submission_object = BasicSubmission.find_polymorphic_subclass(
polymorphic_identity=self.submission_type['value']) polymorphic_identity=self.submission_type['value'])
def set_attribute(self, key, value): def set_attribute(self, key:str, value):
"""
Better handling of attribute setting.
Args:
key (str): Name of field to set
value (_type_): Value to set field to.
"""
self.__setattr__(name=key, value=value) self.__setattr__(name=key, value=value)
def handle_duplicate_samples(self): def handle_duplicate_samples(self):
@@ -775,15 +800,14 @@ class PydSubmission(BaseModel, extra='allow'):
except AttributeError: except AttributeError:
instance.run_cost = 0 instance.run_cost = 0
# logger.debug(f"Calculated base run cost of: {instance.run_cost}") # logger.debug(f"Calculated base run cost of: {instance.run_cost}")
# Apply any discounts that are applicable for client and kit. # NOTE: Apply any discounts that are applicable for client and kit.
try: try:
# logger.debug("Checking and applying discounts...") # logger.debug("Checking and applying discounts...")
discounts = [item.amount for item in discounts = [item.amount for item in
Discount.query(kit_type=instance.extraction_kit, organization=instance.submitting_lab)] Discount.query(kit_type=instance.extraction_kit, organization=instance.submitting_lab)]
# logger.debug(f"We got discounts: {discounts}") # logger.debug(f"We got discounts: {discounts}")
if len(discounts) > 0: if len(discounts) > 0:
discounts = sum(discounts) instance.run_cost = instance.run_cost - sum(discounts)
instance.run_cost = instance.run_cost - discounts
except Exception as e: except Exception as e:
logger.error(f"An unknown exception occurred when calculating discounts: {e}") logger.error(f"An unknown exception occurred when calculating discounts: {e}")
# We need to make sure there's a proper rsl plate number # We need to make sure there's a proper rsl plate number
@@ -808,7 +832,13 @@ class PydSubmission(BaseModel, extra='allow'):
from frontend.widgets.submission_widget import SubmissionFormWidget from frontend.widgets.submission_widget import SubmissionFormWidget
return SubmissionFormWidget(parent=parent, submission=self) return SubmissionFormWidget(parent=parent, submission=self)
def to_writer(self): def to_writer(self) -> "SheetWriter":
"""
Sends data here to the sheet writer.
Returns:
SheetWriter: Sheetwriter object that will perform writing.
"""
from backend.excel.writer import SheetWriter from backend.excel.writer import SheetWriter
return SheetWriter(self) return SheetWriter(self)
@@ -866,6 +896,19 @@ class PydSubmission(BaseModel, extra='allow'):
status="Warning") status="Warning")
report.add_result(result) report.add_result(result)
return output_reagents, report return output_reagents, report
def export_csv(self, filename:Path|str):
try:
worksheet = self.csv
except AttributeError:
logger.error("No csv found.")
return
if isinstance(filename, str):
filename = Path(filename)
with open(filename, 'w', newline="") as f:
c = csv.writer(f)
for r in worksheet.rows:
c.writerow([cell.value for cell in r])
class PydContact(BaseModel): class PydContact(BaseModel):

View File

@@ -62,7 +62,7 @@ class App(QMainWindow):
# logger.debug(f"Creating menu bar...") # logger.debug(f"Creating menu bar...")
menuBar = self.menuBar() menuBar = self.menuBar()
fileMenu = menuBar.addMenu("&File") fileMenu = menuBar.addMenu("&File")
# Creating menus using a title # NOTE: Creating menus using a title
methodsMenu = menuBar.addMenu("&Methods") methodsMenu = menuBar.addMenu("&Methods")
reportMenu = menuBar.addMenu("&Reports") reportMenu = menuBar.addMenu("&Reports")
maintenanceMenu = menuBar.addMenu("&Monthly") maintenanceMenu = menuBar.addMenu("&Monthly")
@@ -70,7 +70,6 @@ class App(QMainWindow):
helpMenu.addAction(self.helpAction) helpMenu.addAction(self.helpAction)
helpMenu.addAction(self.docsAction) helpMenu.addAction(self.docsAction)
fileMenu.addAction(self.importAction) fileMenu.addAction(self.importAction)
# fileMenu.addAction(self.importPCRAction)
methodsMenu.addAction(self.searchLog) methodsMenu.addAction(self.searchLog)
methodsMenu.addAction(self.searchSample) methodsMenu.addAction(self.searchSample)
reportMenu.addAction(self.generateReportAction) reportMenu.addAction(self.generateReportAction)
@@ -94,7 +93,6 @@ class App(QMainWindow):
""" """
# logger.debug(f"Creating actions...") # logger.debug(f"Creating actions...")
self.importAction = QAction("&Import Submission", self) self.importAction = QAction("&Import Submission", self)
# self.importPCRAction = QAction("&Import PCR Results", self)
self.addReagentAction = QAction("Add Reagent", self) self.addReagentAction = QAction("Add Reagent", self)
self.generateReportAction = QAction("Make Report", self) self.generateReportAction = QAction("Make Report", self)
self.addKitAction = QAction("Import Kit", self) self.addKitAction = QAction("Import Kit", self)
@@ -112,7 +110,6 @@ class App(QMainWindow):
""" """
# logger.debug(f"Connecting actions...") # logger.debug(f"Connecting actions...")
self.importAction.triggered.connect(self.table_widget.formwidget.importSubmission) self.importAction.triggered.connect(self.table_widget.formwidget.importSubmission)
# self.importPCRAction.triggered.connect(self.table_widget.formwidget.import_pcr_results)
self.addReagentAction.triggered.connect(self.table_widget.formwidget.add_reagent) self.addReagentAction.triggered.connect(self.table_widget.formwidget.add_reagent)
self.generateReportAction.triggered.connect(self.table_widget.sub_wid.generate_report) self.generateReportAction.triggered.connect(self.table_widget.sub_wid.generate_report)
self.joinExtractionAction.triggered.connect(self.table_widget.sub_wid.link_extractions) self.joinExtractionAction.triggered.connect(self.table_widget.sub_wid.link_extractions)
@@ -166,12 +163,17 @@ class App(QMainWindow):
dlg.exec() dlg.exec()
def runSampleSearch(self): def runSampleSearch(self):
"""
Create a search for samples.
"""
dlg = SearchBox(self) dlg = SearchBox(self)
dlg.exec() dlg.exec()
def backup_database(self): def backup_database(self):
"""
Copies the database into the backup directory the first time it is opened every month.
"""
month = date.today().strftime("%Y-%m") month = date.today().strftime("%Y-%m")
# day = date.today().strftime("%Y-%m-%d")
# logger.debug(f"Here is the db directory: {self.ctx.database_path}") # logger.debug(f"Here is the db directory: {self.ctx.database_path}")
# logger.debug(f"Here is the backup directory: {self.ctx.backup_path}") # logger.debug(f"Here is the backup directory: {self.ctx.backup_path}")
current_month_bak = Path(self.ctx.backup_path).joinpath(f"submissions_backup-{month}").resolve().with_suffix(".db") current_month_bak = Path(self.ctx.backup_path).joinpath(f"submissions_backup-{month}").resolve().with_suffix(".db")

View File

@@ -1,3 +1,6 @@
'''
Handles display of control charts
'''
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QComboBox, QHBoxLayout, QWidget, QVBoxLayout, QComboBox, QHBoxLayout,
@@ -54,43 +57,37 @@ class ControlsViewer(QWidget):
""" """
self.controls_getter_function() self.controls_getter_function()
def chart_maker(self):
"""
Creates plotly charts for webview
"""
self.chart_maker_function()
def controls_getter_function(self): def controls_getter_function(self):
""" """
Get controls based on start/end dates Get controls based on start/end dates
""" """
report = Report() report = Report()
# subtype defaults to disabled # NOTE: subtype defaults to disabled
try: try:
self.sub_typer.disconnect() self.sub_typer.disconnect()
except TypeError: except TypeError:
pass pass
# correct start date being more recent than end date and rerun # NOTE: correct start date being more recent than end date and rerun
if self.datepicker.start_date.date() > self.datepicker.end_date.date(): if self.datepicker.start_date.date() > self.datepicker.end_date.date():
logger.warning("Start date after end date is not allowed!") logger.warning("Start date after end date is not allowed!")
threemonthsago = self.datepicker.end_date.date().addDays(-60) threemonthsago = self.datepicker.end_date.date().addDays(-60)
# block signal that will rerun controls getter and set start date # NOTE: block signal that will rerun controls getter and set start date
# Without triggering this function again # Without triggering this function again
with QSignalBlocker(self.datepicker.start_date) as blocker: with QSignalBlocker(self.datepicker.start_date) as blocker:
self.datepicker.start_date.setDate(threemonthsago) self.datepicker.start_date.setDate(threemonthsago)
self.controls_getter() self.controls_getter()
self.report.add_result(report) self.report.add_result(report)
return return
# convert to python useable date objects # NOTE: convert to python useable date objects
self.start_date = self.datepicker.start_date.date().toPyDate() self.start_date = self.datepicker.start_date.date().toPyDate()
self.end_date = self.datepicker.end_date.date().toPyDate() self.end_date = self.datepicker.end_date.date().toPyDate()
self.con_type = self.control_typer.currentText() self.con_type = self.control_typer.currentText()
self.mode = self.mode_typer.currentText() self.mode = self.mode_typer.currentText()
self.sub_typer.clear() self.sub_typer.clear()
# lookup subtypes # NOTE: lookup subtypes
sub_types = ControlType.query(name=self.con_type).get_subtypes(mode=self.mode) sub_types = ControlType.query(name=self.con_type).get_subtypes(mode=self.mode)
if sub_types != []: if sub_types != []:
# block signal that will rerun controls getter and update sub_typer # NOTE: block signal that will rerun controls getter and update sub_typer
with QSignalBlocker(self.sub_typer) as blocker: with QSignalBlocker(self.sub_typer) as blocker:
self.sub_typer.addItems(sub_types) self.sub_typer.addItems(sub_types)
self.sub_typer.setEnabled(True) self.sub_typer.setEnabled(True)
@@ -100,7 +97,13 @@ class ControlsViewer(QWidget):
self.sub_typer.setEnabled(False) self.sub_typer.setEnabled(False)
self.chart_maker() self.chart_maker()
self.report.add_result(report) self.report.add_result(report)
def chart_maker(self):
"""
Creates plotly charts for webview
"""
self.chart_maker_function()
def chart_maker_function(self): def chart_maker_function(self):
""" """
Create html chart for controls reporting Create html chart for controls reporting
@@ -119,7 +122,7 @@ class ControlsViewer(QWidget):
else: else:
self.subtype = self.sub_typer.currentText() self.subtype = self.sub_typer.currentText()
# logger.debug(f"Subtype: {self.subtype}") # logger.debug(f"Subtype: {self.subtype}")
# query all controls using the type/start and end dates from the gui # NOTE: query all controls using the type/start and end dates from the gui
controls = Control.query(control_type=self.con_type, start_date=self.start_date, end_date=self.end_date) controls = Control.query(control_type=self.con_type, start_date=self.start_date, end_date=self.end_date)
# NOTE: if no data found from query set fig to none for reporting in webview # NOTE: if no data found from query set fig to none for reporting in webview
if controls is None: if controls is None:
@@ -139,7 +142,7 @@ class ControlsViewer(QWidget):
title = self.mode title = self.mode
else: else:
title = f"{self.mode} - {self.subtype}" title = f"{self.mode} - {self.subtype}"
# send dataframe to chart maker # NOTE: send dataframe to chart maker
fig = create_charts(ctx=self.app.ctx, df=df, ytitle=title) fig = create_charts(ctx=self.app.ctx, df=df, ytitle=title)
# logger.debug(f"Updating figure...") # logger.debug(f"Updating figure...")
# NOTE: construct html for webview # NOTE: construct html for webview
@@ -157,7 +160,7 @@ class ControlsDatePicker(QWidget):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self.start_date = QDateEdit(calendarPopup=True) self.start_date = QDateEdit(calendarPopup=True)
# start date is two months prior to end date by default # NOTE: start date is two months prior to end date by default
twomonthsago = QDate.currentDate().addDays(-60) twomonthsago = QDate.currentDate().addDays(-60)
self.start_date.setDate(twomonthsago) self.start_date.setDate(twomonthsago)
self.end_date = QDateEdit(calendarPopup=True) self.end_date = QDateEdit(calendarPopup=True)

View File

@@ -1,4 +1,6 @@
import sys '''
Creates forms that the user can enter equipment info into.
'''
from pprint import pformat from pprint import pformat
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox, from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,
@@ -180,6 +182,9 @@ class RoleComboBox(QWidget):
logger.error(f"Could create PydEquipment due to: {e}") logger.error(f"Could create PydEquipment due to: {e}")
def toggle_checked(self): def toggle_checked(self):
"""
If this equipment is disabled, the input fields will be disabled.
"""
for widget in self.findChildren(QWidget): for widget in self.findChildren(QWidget):
match widget: match widget:
case QCheckBox(): case QCheckBox():

View File

@@ -23,32 +23,32 @@ class GelBox(QDialog):
def __init__(self, parent, img_path:str|Path, submission:WastewaterArtic): def __init__(self, parent, img_path:str|Path, submission:WastewaterArtic):
super().__init__(parent) super().__init__(parent)
# setting title # NOTE: setting title
self.setWindowTitle("PyQtGraph") self.setWindowTitle("PyQtGraph")
self.img_path = img_path self.img_path = img_path
self.submission = submission self.submission = submission
# setting geometry # NOTE: setting geometry
self.setGeometry(50, 50, 1200, 900) self.setGeometry(50, 50, 1200, 900)
# icon # NOTE: icon
icon = QIcon("skin.png") icon = QIcon("skin.png")
# setting icon to the window # NOTE: setting icon to the window
self.setWindowIcon(icon) self.setWindowIcon(icon)
# calling method # NOTE: calling method
self.UiComponents() self.UiComponents()
# showing all the widgets # NOTE: showing all the widgets
# method for components # method for components
def UiComponents(self): def UiComponents(self):
""" """
Create widgets in ui Create widgets in ui
""" """
# setting configuration options # NOTE: setting configuration options
pg.setConfigOptions(antialias=True) pg.setConfigOptions(antialias=True)
# creating image view object # NOTE: creating image view object
self.imv = pg.ImageView() self.imv = pg.ImageView()
# Create image. # NOTE: Create image.
# For some reason, ImageView wants to flip the image, so we have to rotate and flip the array first. # NOTE: For some reason, ImageView wants to flip the image, so we have to rotate and flip the array first.
# Using the Image.rotate function results in cropped image. # NOTE: Using the Image.rotate function results in cropped image, so using np.
img = np.flip(np.rot90(np.array(Image.open(self.img_path)),1),0) img = np.flip(np.rot90(np.array(Image.open(self.img_path)),1),0)
self.imv.setImage(img) self.imv.setImage(img)
layout = QGridLayout() layout = QGridLayout()
@@ -60,10 +60,10 @@ class GelBox(QDialog):
self.gel_barcode = QLineEdit() self.gel_barcode = QLineEdit()
self.gel_barcode.setText(self.submission.gel_barcode) self.gel_barcode.setText(self.submission.gel_barcode)
layout.addWidget(self.gel_barcode, 0, 4) layout.addWidget(self.gel_barcode, 0, 4)
# setting this layout to the widget # NOTE: setting this layout to the widget
# plot window goes on right side, spanning 3 rows # NOTE: plot window goes on right side, spanning 3 rows
layout.addWidget(self.imv, 1, 1,20,20) layout.addWidget(self.imv, 1, 1,20,20)
# 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'])
except KeyError: except KeyError:
@@ -123,16 +123,10 @@ class ControlsForm(QWidget):
widge.setText("Neg") widge.setText("Neg")
widge.setObjectName(f"{rows[iii]} : {columns[jjj]}") widge.setObjectName(f"{rows[iii]} : {columns[jjj]}")
self.layout.addWidget(widge, iii+1, jjj+2, 1, 1) self.layout.addWidget(widge, iii+1, jjj+2, 1, 1)
# try:
# for iii, item in enumerate(control_info, start=1):
# self.layout.addWidget(QLabel(f"{item['sample_id']} - {item['location']}"), iii+4, 1)
# except TypeError:
# pass
self.layout.addWidget(QLabel("Comments:"), 0,5,1,1) self.layout.addWidget(QLabel("Comments:"), 0,5,1,1)
self.comment_field = QTextEdit(self) self.comment_field = QTextEdit(self)
self.comment_field.setFixedHeight(50) self.comment_field.setFixedHeight(50)
self.layout.addWidget(self.comment_field, 1,5,4,1) self.layout.addWidget(self.comment_field, 1,5,4,1)
self.setLayout(self.layout) self.setLayout(self.layout)
def parse_form(self) -> List[dict]: def parse_form(self) -> List[dict]:

View File

@@ -30,27 +30,27 @@ class KitAdder(QWidget):
scrollContent = QWidget(scroll) scrollContent = QWidget(scroll)
self.grid = QGridLayout() self.grid = QGridLayout()
scrollContent.setLayout(self.grid) scrollContent.setLayout(self.grid)
# insert submit button at top # NOTE: insert submit button at top
self.submit_btn = QPushButton("Submit") self.submit_btn = QPushButton("Submit")
self.grid.addWidget(self.submit_btn,0,0,1,1) self.grid.addWidget(self.submit_btn,0,0,1,1)
self.grid.addWidget(QLabel("Kit Name:"),2,0) self.grid.addWidget(QLabel("Kit Name:"),2,0)
# widget to get kit name # NOTE: widget to get kit name
kit_name = QLineEdit() kit_name = QLineEdit()
kit_name.setObjectName("kit_name") kit_name.setObjectName("kit_name")
self.grid.addWidget(kit_name,2,1) self.grid.addWidget(kit_name,2,1)
self.grid.addWidget(QLabel("Used For Submission Type:"),3,0) self.grid.addWidget(QLabel("Used For Submission Type:"),3,0)
# widget to get uses of kit # NOTE: widget to get uses of kit
used_for = QComboBox() used_for = QComboBox()
used_for.setObjectName("used_for") used_for.setObjectName("used_for")
# Insert all existing sample types # NOTE: Insert all existing sample types
used_for.addItems([item.name for item in SubmissionType.query()]) used_for.addItems([item.name for item in SubmissionType.query()])
used_for.setEditable(True) used_for.setEditable(True)
self.grid.addWidget(used_for,3,1) self.grid.addWidget(used_for,3,1)
# Get all fields in SubmissionTypeKitTypeAssociation # NOTE: Get all fields in SubmissionTypeKitTypeAssociation
self.columns = [item for item in SubmissionTypeKitTypeAssociation.__table__.columns if len(item.foreign_keys) == 0] self.columns = [item for item in SubmissionTypeKitTypeAssociation.__table__.columns if len(item.foreign_keys) == 0]
for iii, column in enumerate(self.columns): for iii, column in enumerate(self.columns):
idx = iii + 4 idx = iii + 4
# convert field name to human readable. # NOTE: convert field name to human readable.
field_name = column.name.replace("_", " ").title() field_name = column.name.replace("_", " ").title()
self.grid.addWidget(QLabel(field_name),idx,0) self.grid.addWidget(QLabel(field_name),idx,0)
match column.type: match column.type:
@@ -79,7 +79,7 @@ class KitAdder(QWidget):
""" """
insert new reagent type row insert new reagent type row
""" """
# get bottommost row # NOTE: get bottommost row
maxrow = self.grid.rowCount() maxrow = self.grid.rowCount()
reg_form = ReagentRoleForm(parent=self) reg_form = ReagentRoleForm(parent=self)
reg_form.setObjectName(f"ReagentForm_{maxrow}") reg_form.setObjectName(f"ReagentForm_{maxrow}")
@@ -90,14 +90,14 @@ class KitAdder(QWidget):
send kit to database send kit to database
""" """
report = Report() report = Report()
# get form info # NOTE: get form info
info, reagents = self.parse_form() info, reagents = self.parse_form()
info = {k:v for k,v in info.items() if k in [column.name for column in self.columns] + ['kit_name', 'used_for']} info = {k:v for k,v in info.items() if k in [column.name for column in self.columns] + ['kit_name', 'used_for']}
# logger.debug(f"kit info: {pformat(info)}") # logger.debug(f"kit info: {pformat(info)}")
# logger.debug(f"kit reagents: {pformat(reagents)}") # logger.debug(f"kit reagents: {pformat(reagents)}")
info['reagent_roles'] = reagents info['reagent_roles'] = reagents
# logger.debug(pformat(info)) # logger.debug(pformat(info))
# send to kit constructor # NOTE: send to kit constructor
kit = PydKit(name=info['kit_name']) kit = PydKit(name=info['kit_name'])
for reagent in info['reagent_roles']: for reagent in info['reagent_roles']:
uses = { uses = {

View File

@@ -27,15 +27,12 @@ class AddReagentForm(QDialog):
super().__init__() super().__init__()
if reagent_lot is None: if reagent_lot is None:
reagent_lot = reagent_role reagent_lot = reagent_role
self.setWindowTitle("Add Reagent") self.setWindowTitle("Add Reagent")
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
# widget to get lot info # NOTE: widget to get lot info
self.name_input = QComboBox() self.name_input = QComboBox()
self.name_input.setObjectName("name") self.name_input.setObjectName("name")
self.name_input.setEditable(True) self.name_input.setEditable(True)
@@ -43,10 +40,10 @@ class AddReagentForm(QDialog):
self.lot_input = QLineEdit() self.lot_input = QLineEdit()
self.lot_input.setObjectName("lot") self.lot_input.setObjectName("lot")
self.lot_input.setText(reagent_lot) self.lot_input.setText(reagent_lot)
# widget to get expiry info # NOTE: widget to get expiry info
self.exp_input = QDateEdit(calendarPopup=True) self.exp_input = QDateEdit(calendarPopup=True)
self.exp_input.setObjectName('expiry') self.exp_input.setObjectName('expiry')
# if expiry is not passed in from gui, use today # NOTE: if expiry is not passed in from gui, use today
if expiry is None: if expiry is None:
self.exp_input.setDate(QDate.currentDate()) self.exp_input.setDate(QDate.currentDate())
else: else:
@@ -54,17 +51,17 @@ class AddReagentForm(QDialog):
self.exp_input.setDate(expiry) self.exp_input.setDate(expiry)
except TypeError: except TypeError:
self.exp_input.setDate(QDate.currentDate()) self.exp_input.setDate(QDate.currentDate())
# widget to get reagent type info # NOTE: widget to get reagent type info
self.type_input = QComboBox() self.type_input = QComboBox()
self.type_input.setObjectName('type') self.type_input.setObjectName('type')
self.type_input.addItems([item.name for item in ReagentRole.query()]) self.type_input.addItems([item.name for item in ReagentRole.query()])
# logger.debug(f"Trying to find index of {reagent_type}") # logger.debug(f"Trying to find index of {reagent_type}")
# convert input to user friendly string? # NOTE: convert input to user friendly string?
try: try:
reagent_role = reagent_role.replace("_", " ").title() reagent_role = reagent_role.replace("_", " ").title()
except AttributeError: except AttributeError:
reagent_role = None reagent_role = None
# set parsed reagent type to top of list # NOTE: set parsed reagent type to top of list
index = self.type_input.findText(reagent_role, Qt.MatchFlag.MatchEndsWith) index = self.type_input.findText(reagent_role, Qt.MatchFlag.MatchEndsWith)
if index >= 0: if index >= 0:
self.type_input.setCurrentIndex(index) self.type_input.setCurrentIndex(index)
@@ -110,12 +107,12 @@ class ReportDatePicker(QDialog):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self.setWindowTitle("Select Report Date Range") self.setWindowTitle("Select Report Date Range")
# make confirm/reject buttons # NOTE: make confirm/reject buttons
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
# widgets to ask for dates # NOTE: widgets to ask for dates
self.start_date = QDateEdit(calendarPopup=True) self.start_date = QDateEdit(calendarPopup=True)
self.start_date.setObjectName("start_date") self.start_date.setObjectName("start_date")
self.start_date.setDate(QDate.currentDate()) self.start_date.setDate(QDate.currentDate())
@@ -139,48 +136,6 @@ class ReportDatePicker(QDialog):
""" """
return dict(start_date=self.start_date.date().toPyDate(), end_date = self.end_date.date().toPyDate()) return dict(start_date=self.start_date.date().toPyDate(), end_date = self.end_date.date().toPyDate())
class FirstStrandSalvage(QDialog):
def __init__(self, ctx:Settings, submitter_id:str, rsl_plate_num:str|None=None) -> None:
super().__init__()
if rsl_plate_num is None:
rsl_plate_num = ""
self.setWindowTitle("Add Reagent")
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.submitter_id_input = QLineEdit()
self.submitter_id_input.setText(submitter_id)
self.rsl_plate_num = QLineEdit()
self.rsl_plate_num.setText(rsl_plate_num)
self.row_letter = QComboBox()
self.row_letter.addItems(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'])
self.row_letter.setEditable(False)
self.column_number = QComboBox()
self.column_number.addItems(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'])
self.column_number.setEditable(False)
self.layout = QFormLayout()
self.layout.addRow(self.tr("&Sample Number:"), self.submitter_id_input)
self.layout.addRow(self.tr("&Plate Number:"), self.rsl_plate_num)
self.layout.addRow(self.tr("&Source Row:"), self.row_letter)
self.layout.addRow(self.tr("&Source Column:"), self.column_number)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
def parse_form(self) -> dict:
"""
Pulls first strand info from form.
Returns:
dict: Output info
"""
return dict(plate=self.rsl_plate_num.text(), submitter_id=self.submitter_id_input.text(), well=f"{self.row_letter.currentText()}{self.column_number.currentText()}")
class LogParser(QDialog): class LogParser(QDialog):
def __init__(self, parent): def __init__(self, parent):

View File

@@ -22,13 +22,13 @@ class QuestionAsker(QDialog):
def __init__(self, title:str, message:str) -> QDialog: def __init__(self, title:str, message:str) -> QDialog:
super().__init__() super().__init__()
self.setWindowTitle(title) self.setWindowTitle(title)
# set yes/no buttons # NOTE: set yes/no buttons
QBtn = QDialogButtonBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No QBtn = QDialogButtonBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
self.layout = QVBoxLayout() self.layout = QVBoxLayout()
# Text for the yes/no question # NOTE: Text for the yes/no question
self.message = QLabel(message) self.message = QLabel(message)
self.layout.addWidget(self.message) self.layout.addWidget(self.message)
self.layout.addWidget(self.buttonBox) self.layout.addWidget(self.buttonBox)
@@ -41,7 +41,7 @@ class AlertPop(QMessageBox):
""" """
def __init__(self, message:str, status:Literal['Information', 'Question', 'Warning', 'Critical'], owner:str|None=None) -> QMessageBox: def __init__(self, message:str, status:Literal['Information', 'Question', 'Warning', 'Critical'], owner:str|None=None) -> QMessageBox:
super().__init__() super().__init__()
# select icon by string # NOTE: select icon by string
icon = getattr(QMessageBox.Icon, status) icon = getattr(QMessageBox.Icon, status)
self.setIcon(icon) self.setIcon(icon)
self.setInformativeText(message) self.setInformativeText(message)
@@ -61,13 +61,13 @@ class ObjectSelector(QDialog):
items = [item.name for item in obj_type.query()] items = [item.name for item in obj_type.query()]
self.widget.addItems(items) self.widget.addItems(items)
self.widget.setEditable(False) self.widget.setEditable(False)
# set yes/no buttons # NOTE: set yes/no buttons
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
self.layout = QVBoxLayout() self.layout = QVBoxLayout()
# Text for the yes/no question # NOTE: Text for the yes/no question
message = QLabel(message) message = QLabel(message)
self.layout.addWidget(message) self.layout.addWidget(message)
self.layout.addWidget(self.widget) self.layout.addWidget(self.widget)

View File

@@ -1,3 +1,6 @@
'''
Search box that performs fuzzy search for samples
'''
from pprint import pformat from pprint import pformat
from typing import Tuple from typing import Tuple
from pandas import DataFrame from pandas import DataFrame
@@ -33,6 +36,9 @@ class SearchBox(QDialog):
self.update_widgets() self.update_widgets()
def update_widgets(self): def update_widgets(self):
"""
Changes form inputs based on sample type
"""
deletes = [item for item in self.findChildren(FieldSearch)] deletes = [item for item in self.findChildren(FieldSearch)]
# logger.debug(deletes) # logger.debug(deletes)
for item in deletes: for item in deletes:
@@ -45,13 +51,21 @@ class SearchBox(QDialog):
widget = FieldSearch(parent=self, label=item['label'], field_name=item['field']) widget = FieldSearch(parent=self, label=item['label'], field_name=item['field'])
self.layout.addWidget(widget, start_row+iii, 0) self.layout.addWidget(widget, start_row+iii, 0)
def parse_form(self): def parse_form(self) -> dict:
"""
Converts form into dictionary.
Returns:
dict: Fields dictionary
"""
fields = [item.parse_form() for item in self.findChildren(FieldSearch)] fields = [item.parse_form() for item in self.findChildren(FieldSearch)]
return {item[0]:item[1] for item in fields if item[1] is not None} return {item[0]:item[1] for item in fields if item[1] is not None}
def update_data(self): def update_data(self):
"""
Shows dataframe of relevant samples.
"""
fields = self.parse_form() fields = self.parse_form()
# data = self.type.samples_to_df(sample_type=self.type, **fields)
data = self.type.fuzzy_search(sample_type=self.type, **fields) data = self.type.fuzzy_search(sample_type=self.type, **fields)
data = self.type.samples_to_df(sample_list=data) data = self.type.samples_to_df(sample_list=data)
# logger.debug(f"Data: {data}") # logger.debug(f"Data: {data}")
@@ -72,6 +86,9 @@ class FieldSearch(QWidget):
self.search_widget.returnPressed.connect(self.enter_pressed) self.search_widget.returnPressed.connect(self.enter_pressed)
def enter_pressed(self): def enter_pressed(self):
"""
Triggered when enter is pressed on this input field.
"""
self.parent().update_data() self.parent().update_data()
def parse_form(self) -> Tuple: def parse_form(self) -> Tuple:
@@ -99,4 +116,5 @@ class SearchResults(QTableView):
logger.error("Couldn't format id string.") logger.error("Couldn't format id string.")
proxy_model = QSortFilterProxyModel() proxy_model = QSortFilterProxyModel()
proxy_model.setSourceModel(pandasModel(self.data)) proxy_model.setSourceModel(pandasModel(self.data))
self.setModel(proxy_model) self.setModel(proxy_model)

View File

@@ -1,6 +1,7 @@
from PyQt6.QtGui import QPageSize '''
from PyQt6.QtPrintSupport import QPrinter Webview to show submission and sample details.
from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout, QMessageBox, '''
from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout,
QDialogButtonBox, QTextEdit) QDialogButtonBox, QTextEdit)
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebChannel import QWebChannel from PyQt6.QtWebChannel import QWebChannel
@@ -9,14 +10,12 @@ from PyQt6.QtCore import Qt, pyqtSlot
from backend.db.models import BasicSubmission, BasicSample from backend.db.models import BasicSubmission, BasicSample
from tools import is_power_user, html_to_pdf from tools import is_power_user, html_to_pdf
from .functions import select_save_file from .functions import select_save_file
from io import BytesIO
from pathlib import Path from pathlib import Path
import logging, base64 import logging
from getpass import getuser from getpass import getuser
from datetime import datetime from datetime import datetime
from pprint import pformat from pprint import pformat
from html2image import Html2Image
from PIL import Image
from typing import List from typing import List
from backend.excel.writer import DocxWriter from backend.excel.writer import DocxWriter
@@ -135,7 +134,7 @@ class SubmissionComment(QDialog):
pass pass
self.submission = submission self.submission = submission
self.setWindowTitle(f"{self.submission.rsl_plate_num} Submission Comment") self.setWindowTitle(f"{self.submission.rsl_plate_num} Submission Comment")
# create text field # NOTE: create text field
self.txt_editor = QTextEdit(self) self.txt_editor = QTextEdit(self)
self.txt_editor.setReadOnly(False) self.txt_editor.setReadOnly(False)
self.txt_editor.setText("Add Comment") self.txt_editor.setText("Add Comment")

View File

@@ -261,32 +261,32 @@ class SubmissionsSheet(QTableView):
html = make_report_html(df=summary_df, start_date=info['start_date'], end_date=info['end_date']) html = make_report_html(df=summary_df, start_date=info['start_date'], end_date=info['end_date'])
# NOTE: get save location of report # NOTE: get save location of report
fname = select_save_file(obj=self, default_name=f"Submissions_Report_{info['start_date']}-{info['end_date']}.docx", extension="docx") fname = select_save_file(obj=self, default_name=f"Submissions_Report_{info['start_date']}-{info['end_date']}.docx", extension="docx")
html_to_pdf(html=html, output_file=fname) # html_to_pdf(html=html, output_file=fname)
writer = pd.ExcelWriter(fname.with_suffix(".xlsx"), engine='openpyxl') # writer = pd.ExcelWriter(fname.with_suffix(".xlsx"), engine='openpyxl')
summary_df.to_excel(writer, sheet_name="Report") # summary_df.to_excel(writer, sheet_name="Report")
detailed_df.to_excel(writer, sheet_name="Details", index=False) # detailed_df.to_excel(writer, sheet_name="Details", index=False)
worksheet: Worksheet = writer.sheets['Report'] # worksheet: Worksheet = writer.sheets['Report']
for idx, col in enumerate(summary_df, start=1): # loop through all columns # for idx, col in enumerate(summary_df, start=1): # loop through all columns
series = summary_df[col] # series = summary_df[col]
max_len = max(( # max_len = max((
series.astype(str).map(len).max(), # len of largest item # series.astype(str).map(len).max(), # len of largest item
len(str(series.name)) # len of column name/header # len(str(series.name)) # len of column name/header
)) + 20 # adding a little extra space # )) + 20 # adding a little extra space
try: # try:
# NOTE: Convert idx to letter # # NOTE: Convert idx to letter
col_letter = chr(ord('@') + idx) # col_letter = chr(ord('@') + idx)
worksheet.column_dimensions[col_letter].width = max_len # worksheet.column_dimensions[col_letter].width = max_len
except ValueError: # except ValueError:
pass # pass
blank_row = get_first_blank_df_row(summary_df) + 1 # blank_row = get_first_blank_df_row(summary_df) + 1
# logger.debug(f"Blank row index = {blank_row}") # # logger.debug(f"Blank row index = {blank_row}")
for col in range(3,6): # for col in range(3,6):
col_letter = row_map[col] # col_letter = row_map[col]
worksheet.cell(row=blank_row, column=col, value=f"=SUM({col_letter}2:{col_letter}{str(blank_row-1)})") # worksheet.cell(row=blank_row, column=col, value=f"=SUM({col_letter}2:{col_letter}{str(blank_row-1)})")
for cell in worksheet['D']: # for cell in worksheet['D']:
if cell.row > 1: # if cell.row > 1:
cell.style = 'Currency' # cell.style = 'Currency'
writer.close() # writer.close()
# rp = ReportMaker(start_date=info['start_date'], end_date=info['end_date']) rp = ReportMaker(start_date=info['start_date'], end_date=info['end_date'])
# rp.write_report(filename=fname, obj=self) rp.write_report(filename=fname, obj=self)
self.report.add_result(report) self.report.add_result(report)

View File

@@ -114,4 +114,5 @@ class InfoWidget(QWidget):
sheets = self.sheet.text().split(","), sheets = self.sheet.text().split(","),
row = self.row.value(), row = self.row.value(),
column = self.column.value() column = self.column.value()
) )

View File

@@ -1,5 +1,7 @@
'''
Contains all submission related frontend functions
'''
import sys import sys
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QPushButton, QVBoxLayout, QWidget, QPushButton, QVBoxLayout,
QComboBox, QDateEdit, QLineEdit, QLabel QComboBox, QDateEdit, QLineEdit, QLabel
@@ -149,8 +151,6 @@ class SubmissionFormContainer(QWidget):
class SubmissionFormWidget(QWidget): class SubmissionFormWidget(QWidget):
def __init__(self, parent: QWidget, submission: PydSubmission) -> None: def __init__(self, parent: QWidget, submission: PydSubmission) -> None:
super().__init__(parent) super().__init__(parent)
# self.report = Report() # self.report = Report()
@@ -303,10 +303,10 @@ class SubmissionFormWidget(QWidget):
# logger.debug(f"Base submission: {base_submission.to_dict()}") # logger.debug(f"Base submission: {base_submission.to_dict()}")
# NOTE: check output message for issues # NOTE: check output message for issues
match result.code: match result.code:
# code 0: everything is fine. # NOTE: code 0: everything is fine.
case 0: case 0:
report.add_result(None) report.add_result(None)
# code 1: ask for overwrite # NOTE: code 1: ask for overwrite
case 1: case 1:
dlg = QuestionAsker(title=f"Review {base_submission.rsl_plate_num}?", message=result.msg) dlg = QuestionAsker(title=f"Review {base_submission.rsl_plate_num}?", message=result.msg)
if dlg.exec(): if dlg.exec():
@@ -319,7 +319,7 @@ class SubmissionFormWidget(QWidget):
self.app.report.add_result(report) self.app.report.add_result(report)
self.app.result_reporter() self.app.result_reporter()
return return
# code 2: No RSL plate number given # NOTE: code 2: No RSL plate number given
case 2: case 2:
report.add_result(result) report.add_result(result)
self.app.report.add_result(report) self.app.report.add_result(report)
@@ -351,9 +351,8 @@ class SubmissionFormWidget(QWidget):
if isinstance(fname, bool) or fname is None: if isinstance(fname, bool) or fname is None:
fname = select_save_file(obj=self, default_name=self.pyd.construct_filename(), extension="csv") fname = select_save_file(obj=self, default_name=self.pyd.construct_filename(), extension="csv")
try: try:
self.pyd.export_csv(fname)
# self.pyd.csv.to_csv(fname.__str__(), index=False) # workbook_2_csv(worksheet=self.pyd.csv, filename=fname)
workbook_2_csv(worksheet=self.pyd.csv, filename=fname)
except PermissionError: except PermissionError:
logger.warning(f"Could not get permissions to {fname}. Possibly the request was cancelled.") logger.warning(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
except AttributeError: except AttributeError:

View File

@@ -7,7 +7,6 @@ import json
import numpy as np import numpy as np
import logging, re, yaml, sys, os, stat, platform, getpass, inspect, csv import logging, re, yaml, sys, os, stat, platform, getpass, inspect, csv
import pandas as pd import pandas as pd
from PyQt6.QtWidgets import QWidget
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from logging import handlers from logging import handlers
from pathlib import Path from pathlib import Path
@@ -65,6 +64,17 @@ def get_unique_values_in_df_column(df: pd.DataFrame, column_name: str) -> list:
def check_key_or_attr(key: str, interest: dict | object, check_none: bool = False) -> bool: def check_key_or_attr(key: str, interest: dict | object, check_none: bool = False) -> bool:
"""
Checks if key exists in dict or object has attribute.
Args:
key (str): key or attribute name
interest (dict | object): Dictionary or object to be checked.
check_none (bool, optional): Return false if value exists, but is None. Defaults to False.
Returns:
bool: True if exists, else False
"""
match interest: match interest:
case dict(): case dict():
if key in interest.keys(): if key in interest.keys():
@@ -105,10 +115,9 @@ def check_not_nan(cell_contents) -> bool:
Returns: Returns:
bool: True if cell has value, else, false. bool: True if cell has value, else, false.
""" """
# check for nan as a string first # NOTE: check for nan as a string first
exclude = ['unnamed:', 'blank', 'void'] exclude = ['unnamed:', 'blank', 'void']
try: try:
# if "Unnamed:" in cell_contents or "blank" in cell_contents.lower():
if cell_contents.lower() in exclude: if cell_contents.lower() in exclude:
cell_contents = np.nan cell_contents = np.nan
cell_contents = cell_contents.lower() cell_contents = cell_contents.lower()
@@ -158,6 +167,15 @@ def convert_nans_to_nones(input_str) -> str | None:
def is_missing(value: Any) -> Tuple[Any, bool]: def is_missing(value: Any) -> Tuple[Any, bool]:
"""
Checks if a parsed value is missing.
Args:
value (Any): Incoming value
Returns:
Tuple[Any, bool]: Value, True if nan, else False
"""
if check_not_nan(value): if check_not_nan(value):
return value, False return value, False
else: else:
@@ -262,19 +280,19 @@ class Settings(BaseSettings, extra="allow"):
else: else:
database_path = values.data['database_path'] database_path = values.data['database_path']
if database_path is None: if database_path is None:
# check in user's .submissions directory for submissions.db # NOTE: check in user's .submissions directory for submissions.db
if Path.home().joinpath(".submissions", "submissions.db").exists(): if Path.home().joinpath(".submissions", "submissions.db").exists():
database_path = Path.home().joinpath(".submissions", "submissions.db") database_path = Path.home().joinpath(".submissions", "submissions.db")
# finally, look in the local dir # NOTE: finally, look in the local dir
else: else:
database_path = package_dir.joinpath("submissions.db") database_path = package_dir.joinpath("submissions.db")
else: else:
if database_path == ":memory:": if database_path == ":memory:":
pass pass
# check if user defined path is directory # NOTE: check if user defined path is directory
elif database_path.is_dir(): elif database_path.is_dir():
database_path = database_path.joinpath("submissions.db") database_path = database_path.joinpath("submissions.db")
# check if user defined path is a file # NOTE: check if user defined path is a file
elif database_path.is_file(): elif database_path.is_file():
database_path = database_path database_path = database_path
else: else:
@@ -282,7 +300,6 @@ class Settings(BaseSettings, extra="allow"):
logger.info(f"Using {database_path} for database file.") logger.info(f"Using {database_path} for database file.")
engine = create_engine(f"sqlite:///{database_path}") #, echo=True, future=True) engine = create_engine(f"sqlite:///{database_path}") #, echo=True, future=True)
session = Session(engine) session = Session(engine)
# metadata.session = session
return session return session
@field_validator('package', mode="before") @field_validator('package', mode="before")
@@ -403,7 +420,7 @@ class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler):
""" """
# Rotate the file first. # Rotate the file first.
handlers.RotatingFileHandler.doRollover(self) handlers.RotatingFileHandler.doRollover(self)
# Add group write to the current permissions. # NOTE: Add group write to the current permissions.
currMode = os.stat(self.baseFilename).st_mode currMode = os.stat(self.baseFilename).st_mode
os.chmod(self.baseFilename, currMode | stat.S_IWGRP) os.chmod(self.baseFilename, currMode | stat.S_IWGRP)
@@ -629,6 +646,12 @@ class Report(BaseModel):
return f"Report(result_count:{len(self.results)})" return f"Report(result_count:{len(self.results)})"
def add_result(self, result: Result | Report | None): def add_result(self, result: Result | Report | None):
"""
Takes a result object or all results in another report and adds them to this one.
Args:
result (Result | Report | None): Results to be added.
"""
match result: match result:
case Result(): case Result():
logger.info(f"Adding {result} to results.") logger.info(f"Adding {result} to results.")
@@ -644,15 +667,30 @@ class Report(BaseModel):
case _: case _:
logger.error(f"Unknown variable type: {type(result)} for <Result> entry into <Report>") logger.error(f"Unknown variable type: {type(result)} for <Result> entry into <Report>")
def is_empty(self):
return bool(self.results)
def rreplace(s:str, old:str, new:str) -> str:
"""
Removes rightmost occurence of a substring
def rreplace(s, old, new): Args:
s (str): input string
old (str): original substring
new (str): new substring
Returns:
str: updated string
"""
return (s[::-1].replace(old[::-1], new[::-1], 1))[::-1] return (s[::-1].replace(old[::-1], new[::-1], 1))[::-1]
def html_to_pdf(html, output_file: Path | str): def html_to_pdf(html:str, output_file: Path | str):
"""
Attempts to print an html string as a PDF. (currently not working)
Args:
html (str): Input html string.
output_file (Path | str): Output PDF file path.
"""
if isinstance(output_file, str): if isinstance(output_file, str):
output_file = Path(output_file) output_file = Path(output_file)
logger.debug(f"Printing PDF to {output_file}") logger.debug(f"Printing PDF to {output_file}")
@@ -688,6 +726,13 @@ def remove_key_from_list_of_dicts(input: list, key: str) -> list:
def workbook_2_csv(worksheet: Worksheet, filename: Path): def workbook_2_csv(worksheet: Worksheet, filename: Path):
"""
Export an excel worksheet (workbook is not correct) to csv file.
Args:
worksheet (Worksheet): Incoming worksheet
filename (Path): Output csv filepath.
"""
with open(filename, 'w', newline="") as f: with open(filename, 'w', newline="") as f:
c = csv.writer(f) c = csv.writer(f)
for r in worksheet.rows: for r in worksheet.rows:
@@ -698,6 +743,12 @@ ctx = get_config(None)
def is_power_user() -> bool: def is_power_user() -> bool:
"""
Checks if user is in list of power users
Returns:
bool: True if yes, False if no.
"""
try: try:
check = getpass.getuser() in ctx.power_users check = getpass.getuser() in ctx.power_users
except: except: