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
I have no idea how it actually works.
Listens for connect and then turns on foreign keys?
Args:
dbapi_connection (_type_): _description_

View File

@@ -1581,12 +1581,12 @@ class WastewaterArtic(BasicSubmission):
dict: dictionary used in submissions summary
"""
output = super().to_dict(full_data=full_data, backup=backup, report=report)
if report:
return output
if self.artic_technician in [None, "None"]:
output['artic_technician'] = self.technician
else:
output['artic_technician'] = self.artic_technician
if report:
return output
output['gel_info'] = self.gel_info
output['gel_image_path'] = self.gel_image
output['dna_core_submission_number'] = self.dna_core_submission_number
@@ -2253,6 +2253,12 @@ class BasicSample(BaseClass):
@classmethod
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")]
@classmethod
@@ -2381,22 +2387,14 @@ class WastewaterSample(BasicSample):
output_dict["submitter_id"] = output_dict['ww_full_sample_id']
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
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()
for item in ["ww_processing_num", "ww_full_sample_id", "rsl_number"]:
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 .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
from copy import copy
@@ -78,7 +78,7 @@ class SheetParser(object):
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:
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):
"""
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)
self.sub['samples'] = parser.reconcile_samples()
def parse_equipment(self):
"""
Calls equipment parser to pull info from the excel sheet
"""
parser = EquipmentParser(xl=self.xl, submission_type=self.submission_type)
self.sub['equipment'] = parser.parse_equipment()
def parse_tips(self):
"""
Calls tips parser to pull info from the excel sheet
"""
parser = TipParser(xl=self.xl, submission_type=self.submission_type)
self.sub['tips'] = parser.parse_tips()
@@ -160,8 +166,16 @@ class SheetParser(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):
"""
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")
if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type)
@@ -221,6 +235,7 @@ class InfoParser(object):
new['name'] = k
relevant.append(new)
# logger.debug(f"relevant map for {sheet}: {pformat(relevant)}")
# NOTE: make sure relevant is not an empty list.
if not relevant:
continue
for item in relevant:
@@ -231,6 +246,7 @@ class InfoParser(object):
case "submission_type":
value, missing = is_missing(value)
value = value.title()
# NOTE: is field a JSON?
case thing if thing in self.sub_object.jsons():
value, missing = is_missing(value)
if missing: continue
@@ -248,12 +264,23 @@ class InfoParser(object):
dicto[item['name']] = dict(value=value, missing=missing)
except (KeyError, IndexError):
continue
# Return after running the parser components held in submission object.
return self.sub_object.custom_info_parser(input_dict=dicto, xl=self.xl)
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):
"""
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")
self.submission_type_obj = submission_type
self.sub_object = sub_object
@@ -284,7 +311,7 @@ class ReagentParser(object):
pass
return reagent_map
def parse_reagents(self) -> List[PydReagent]:
def parse_reagents(self) -> List[dict]:
"""
Extracts reagent information from the excel form.
@@ -312,7 +339,7 @@ class ReagentParser(object):
comment = ""
except (KeyError, IndexError):
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
# NOTE: If the cell is blank tell the PydReagent
if check_not_nan(lot):
@@ -336,17 +363,17 @@ class ReagentParser(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:
"""
convert sample sub-dataframe to dictionary of records
Args:
df (pd.DataFrame): input sample dataframe
elution_map (pd.DataFrame | None, optional): optional map of elution plate. Defaults to None.
"""
xl (Workbook): Openpyxl workbook from submitted excel file.
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")
self.samples = []
self.xl = xl
@@ -383,10 +410,13 @@ class SampleParser(object):
sample_info_map = sample_map
return sample_info_map
def parse_plate_map(self):
def parse_plate_map(self) -> List[dict]:
"""
Parse sample location/name from plate map
"""
Returns:
List[dict]: List of sample ids and locations.
"""
invalids = [0, "0", "EMPTY"]
smap = self.sample_info_map['plate_map']
ws = self.xl[smap['sheet']]
@@ -412,7 +442,11 @@ class SampleParser(object):
def parse_lookup_table(self) -> List[dict]:
"""
Parse misc info from lookup table.
"""
Returns:
List[dict]: List of basic sample info.
"""
lmap = self.sample_info_map['lookup_table']
ws = self.xl[lmap['sheet']]
lookup_samples = []
@@ -460,7 +494,13 @@ class SampleParser(object):
new_samples.append(PydSample(**translated_dict))
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?
if self.plate_map_samples is None or self.lookup_samples is None:
self.samples = self.lookup_samples or self.plate_map_samples
@@ -504,8 +544,15 @@ class SampleParser(object):
class EquipmentParser(object):
"""
Object to pull data for equipment in excel sheet
"""
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):
submission_type = SubmissionType.query(name=submission_type)
self.submission_type = submission_type
@@ -582,8 +629,15 @@ class EquipmentParser(object):
class TipParser(object):
"""
Object to pull data for tips in excel sheet
"""
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):
submission_type = SubmissionType.query(name=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:
"""
Initializes object.
Args:
filepath (Path | None, optional): file to parse. Defaults to None.
"""

View File

@@ -90,6 +90,13 @@ class ReportMaker(object):
return html
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):
filename = Path(filename)
filename = filename.absolute()
@@ -108,6 +115,9 @@ class ReportMaker(object):
self.writer.close()
def fix_up_xl(self):
"""
Handles formatting of xl file.
"""
# logger.debug(f"Updating worksheet")
worksheet: Worksheet = self.writer.sheets['Report']
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
from copy import copy
from operator import itemgetter
@@ -27,8 +30,9 @@ class SheetWriter(object):
def __init__(self, submission: PydSubmission, missing_only: bool = False):
"""
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())
for k, v in self.sub.items():
match k:
@@ -116,6 +120,13 @@ class InfoWriter(object):
def __init__(self, xl: Workbook, submission_type: SubmissionType | str, info_dict: dict,
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)}")
if isinstance(submission_type, str):
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,
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
if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type)
@@ -245,8 +263,13 @@ class SampleWriter(object):
"""
object to write sample data into excel file
"""
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):
submission_type = SubmissionType.query(name=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):
"""
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):
submission_type = SubmissionType.query(name=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):
"""
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):
submission_type = SubmissionType.query(name=submission_type)
self.submission_type = submission_type
@@ -460,8 +495,15 @@ class TipWriter(object):
class DocxWriter(object):
"""
Object to render
"""
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'])
env = jinja_template_loading()
temp_name = f"{base_dict['submission_type'].replace(' ', '').lower()}_subdocument.docx"
@@ -506,7 +548,13 @@ class DocxWriter(object):
output.append(contents)
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()
output = BytesIO()
for index, file in enumerate(args):

View File

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

View File

@@ -3,21 +3,18 @@ Contains pydantic models and accompanying validators
'''
from __future__ import annotations
import sys
from operator import attrgetter
import uuid, re, logging
from pydantic import BaseModel, field_validator, Field, model_validator, PrivateAttr
import uuid, re, logging, csv
from pydantic import BaseModel, field_validator, Field, model_validator
from datetime import date, datetime, timedelta
from dateutil.parser import parse
from dateutil.parser import ParserError
from typing import List, Tuple, Literal
from . import RSLNamer
from pathlib import Path
from tools import check_not_nan, convert_nans_to_nones, Report, Result, row_map
from tools import check_not_nan, convert_nans_to_nones, Report, Result
from backend.db.models import *
from sqlalchemy.exc import StatementError, IntegrityError
from PyQt6.QtWidgets import QWidget
from openpyxl import load_workbook, Workbook
from io import BytesIO
logger = logging.getLogger(f"submissions.{__name__}")
@@ -106,20 +103,17 @@ class PydReagent(BaseModel):
return values.data['role']
def improved_dict(self) -> dict:
"""
Constructs a dictionary consisting of model.fields and model.extras
Returns:
dict: Information dictionary
"""
try:
extras = list(self.model_extra.keys())
except AttributeError:
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}
def toSQL(self, submission: BasicSubmission | str = None) -> Tuple[Reagent, SubmissionReagentAssociation, Report]:
@@ -142,7 +136,7 @@ class PydReagent(BaseModel):
if isinstance(value, dict):
value = value['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:
case "lot":
reagent.lot = value.upper()
@@ -177,11 +171,9 @@ class PydReagent(BaseModel):
assoc = None
# 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
return reagent, assoc, report
class PydSample(BaseModel, extra='allow'):
submitter_id: str
sample_type: str
@@ -220,6 +212,12 @@ class PydSample(BaseModel, extra='allow'):
return str(value)
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())
return {k: getattr(self, k) for k in fields}
@@ -249,7 +247,6 @@ class PydSample(BaseModel, extra='allow'):
if isinstance(submission, str):
submission = BasicSubmission.query(rsl_plate_num=submission)
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):
# logger.debug(f"Looking up association with identity: ({submission.submission_type_name} 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
def improved_dict(self) -> dict:
"""
Constructs a dictionary consisting of model.fields and model.extras
Returns:
dict: Information dictionary
"""
try:
extras = list(self.model_extra.keys())
except AttributeError:
@@ -281,7 +284,16 @@ class PydTips(BaseModel):
lot: str|None = Field(default=None)
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)
assoc = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=self.role)
return assoc
@@ -348,6 +360,12 @@ class PydEquipment(BaseModel, extra='ignore'):
return equipment, assoc
def improved_dict(self) -> dict:
"""
Constructs a dictionary consisting of model.fields and model.extras
Returns:
dict: Information dictionary
"""
try:
extras = list(self.model_extra.keys())
except AttributeError:
@@ -619,7 +637,14 @@ class PydSubmission(BaseModel, extra='allow'):
self.submission_object = BasicSubmission.find_polymorphic_subclass(
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)
def handle_duplicate_samples(self):
@@ -775,15 +800,14 @@ class PydSubmission(BaseModel, extra='allow'):
except AttributeError:
instance.run_cost = 0
# 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:
# logger.debug("Checking and applying discounts...")
discounts = [item.amount for item in
Discount.query(kit_type=instance.extraction_kit, organization=instance.submitting_lab)]
# logger.debug(f"We got discounts: {discounts}")
if len(discounts) > 0:
discounts = sum(discounts)
instance.run_cost = instance.run_cost - discounts
instance.run_cost = instance.run_cost - sum(discounts)
except Exception as e:
logger.error(f"An unknown exception occurred when calculating discounts: {e}")
# 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
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
return SheetWriter(self)
@@ -866,6 +896,19 @@ class PydSubmission(BaseModel, extra='allow'):
status="Warning")
report.add_result(result)
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):

View File

@@ -62,7 +62,7 @@ class App(QMainWindow):
# logger.debug(f"Creating menu bar...")
menuBar = self.menuBar()
fileMenu = menuBar.addMenu("&File")
# Creating menus using a title
# NOTE: Creating menus using a title
methodsMenu = menuBar.addMenu("&Methods")
reportMenu = menuBar.addMenu("&Reports")
maintenanceMenu = menuBar.addMenu("&Monthly")
@@ -70,7 +70,6 @@ class App(QMainWindow):
helpMenu.addAction(self.helpAction)
helpMenu.addAction(self.docsAction)
fileMenu.addAction(self.importAction)
# fileMenu.addAction(self.importPCRAction)
methodsMenu.addAction(self.searchLog)
methodsMenu.addAction(self.searchSample)
reportMenu.addAction(self.generateReportAction)
@@ -94,7 +93,6 @@ class App(QMainWindow):
"""
# logger.debug(f"Creating actions...")
self.importAction = QAction("&Import Submission", self)
# self.importPCRAction = QAction("&Import PCR Results", self)
self.addReagentAction = QAction("Add Reagent", self)
self.generateReportAction = QAction("Make Report", self)
self.addKitAction = QAction("Import Kit", self)
@@ -112,7 +110,6 @@ class App(QMainWindow):
"""
# logger.debug(f"Connecting actions...")
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.generateReportAction.triggered.connect(self.table_widget.sub_wid.generate_report)
self.joinExtractionAction.triggered.connect(self.table_widget.sub_wid.link_extractions)
@@ -166,12 +163,17 @@ class App(QMainWindow):
dlg.exec()
def runSampleSearch(self):
"""
Create a search for samples.
"""
dlg = SearchBox(self)
dlg.exec()
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")
# 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 backup directory: {self.ctx.backup_path}")
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.QtWidgets import (
QWidget, QVBoxLayout, QComboBox, QHBoxLayout,
@@ -54,43 +57,37 @@ class ControlsViewer(QWidget):
"""
self.controls_getter_function()
def chart_maker(self):
"""
Creates plotly charts for webview
"""
self.chart_maker_function()
def controls_getter_function(self):
"""
Get controls based on start/end dates
"""
report = Report()
# subtype defaults to disabled
# NOTE: subtype defaults to disabled
try:
self.sub_typer.disconnect()
except TypeError:
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():
logger.warning("Start date after end date is not allowed!")
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
with QSignalBlocker(self.datepicker.start_date) as blocker:
self.datepicker.start_date.setDate(threemonthsago)
self.controls_getter()
self.report.add_result(report)
return
# convert to python useable date objects
# NOTE: convert to python useable date objects
self.start_date = self.datepicker.start_date.date().toPyDate()
self.end_date = self.datepicker.end_date.date().toPyDate()
self.con_type = self.control_typer.currentText()
self.mode = self.mode_typer.currentText()
self.sub_typer.clear()
# lookup subtypes
# NOTE: lookup subtypes
sub_types = ControlType.query(name=self.con_type).get_subtypes(mode=self.mode)
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:
self.sub_typer.addItems(sub_types)
self.sub_typer.setEnabled(True)
@@ -100,7 +97,13 @@ class ControlsViewer(QWidget):
self.sub_typer.setEnabled(False)
self.chart_maker()
self.report.add_result(report)
def chart_maker(self):
"""
Creates plotly charts for webview
"""
self.chart_maker_function()
def chart_maker_function(self):
"""
Create html chart for controls reporting
@@ -119,7 +122,7 @@ class ControlsViewer(QWidget):
else:
self.subtype = self.sub_typer.currentText()
# 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)
# NOTE: if no data found from query set fig to none for reporting in webview
if controls is None:
@@ -139,7 +142,7 @@ class ControlsViewer(QWidget):
title = self.mode
else:
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)
# logger.debug(f"Updating figure...")
# NOTE: construct html for webview
@@ -157,7 +160,7 @@ class ControlsDatePicker(QWidget):
def __init__(self) -> None:
super().__init__()
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)
self.start_date.setDate(twomonthsago)
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 PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,
@@ -180,6 +182,9 @@ class RoleComboBox(QWidget):
logger.error(f"Could create PydEquipment due to: {e}")
def toggle_checked(self):
"""
If this equipment is disabled, the input fields will be disabled.
"""
for widget in self.findChildren(QWidget):
match widget:
case QCheckBox():

View File

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

View File

@@ -30,27 +30,27 @@ class KitAdder(QWidget):
scrollContent = QWidget(scroll)
self.grid = QGridLayout()
scrollContent.setLayout(self.grid)
# insert submit button at top
# NOTE: insert submit button at top
self.submit_btn = QPushButton("Submit")
self.grid.addWidget(self.submit_btn,0,0,1,1)
self.grid.addWidget(QLabel("Kit Name:"),2,0)
# widget to get kit name
# NOTE: widget to get kit name
kit_name = QLineEdit()
kit_name.setObjectName("kit_name")
self.grid.addWidget(kit_name,2,1)
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.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.setEditable(True)
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]
for iii, column in enumerate(self.columns):
idx = iii + 4
# convert field name to human readable.
# NOTE: convert field name to human readable.
field_name = column.name.replace("_", " ").title()
self.grid.addWidget(QLabel(field_name),idx,0)
match column.type:
@@ -79,7 +79,7 @@ class KitAdder(QWidget):
"""
insert new reagent type row
"""
# get bottommost row
# NOTE: get bottommost row
maxrow = self.grid.rowCount()
reg_form = ReagentRoleForm(parent=self)
reg_form.setObjectName(f"ReagentForm_{maxrow}")
@@ -90,14 +90,14 @@ class KitAdder(QWidget):
send kit to database
"""
report = Report()
# get form info
# NOTE: get form info
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']}
# logger.debug(f"kit info: {pformat(info)}")
# logger.debug(f"kit reagents: {pformat(reagents)}")
info['reagent_roles'] = reagents
# logger.debug(pformat(info))
# send to kit constructor
# NOTE: send to kit constructor
kit = PydKit(name=info['kit_name'])
for reagent in info['reagent_roles']:
uses = {

View File

@@ -27,15 +27,12 @@ class AddReagentForm(QDialog):
super().__init__()
if reagent_lot is None:
reagent_lot = reagent_role
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)
# widget to get lot info
# NOTE: widget to get lot info
self.name_input = QComboBox()
self.name_input.setObjectName("name")
self.name_input.setEditable(True)
@@ -43,10 +40,10 @@ class AddReagentForm(QDialog):
self.lot_input = QLineEdit()
self.lot_input.setObjectName("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.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:
self.exp_input.setDate(QDate.currentDate())
else:
@@ -54,17 +51,17 @@ class AddReagentForm(QDialog):
self.exp_input.setDate(expiry)
except TypeError:
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.setObjectName('type')
self.type_input.addItems([item.name for item in ReagentRole.query()])
# logger.debug(f"Trying to find index of {reagent_type}")
# convert input to user friendly string?
# NOTE: convert input to user friendly string?
try:
reagent_role = reagent_role.replace("_", " ").title()
except AttributeError:
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)
if index >= 0:
self.type_input.setCurrentIndex(index)
@@ -110,12 +107,12 @@ class ReportDatePicker(QDialog):
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("Select Report Date Range")
# make confirm/reject buttons
# NOTE: make confirm/reject buttons
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
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.setObjectName("start_date")
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())
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):
def __init__(self, parent):

View File

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

View File

@@ -1,3 +1,6 @@
'''
Search box that performs fuzzy search for samples
'''
from pprint import pformat
from typing import Tuple
from pandas import DataFrame
@@ -33,6 +36,9 @@ class SearchBox(QDialog):
self.update_widgets()
def update_widgets(self):
"""
Changes form inputs based on sample type
"""
deletes = [item for item in self.findChildren(FieldSearch)]
# logger.debug(deletes)
for item in deletes:
@@ -45,13 +51,21 @@ class SearchBox(QDialog):
widget = FieldSearch(parent=self, label=item['label'], field_name=item['field'])
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)]
return {item[0]:item[1] for item in fields if item[1] is not None}
def update_data(self):
"""
Shows dataframe of relevant samples.
"""
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.samples_to_df(sample_list=data)
# logger.debug(f"Data: {data}")
@@ -72,6 +86,9 @@ class FieldSearch(QWidget):
self.search_widget.returnPressed.connect(self.enter_pressed)
def enter_pressed(self):
"""
Triggered when enter is pressed on this input field.
"""
self.parent().update_data()
def parse_form(self) -> Tuple:
@@ -99,4 +116,5 @@ class SearchResults(QTableView):
logger.error("Couldn't format id string.")
proxy_model = QSortFilterProxyModel()
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
from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout, QMessageBox,
'''
Webview to show submission and sample details.
'''
from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout,
QDialogButtonBox, QTextEdit)
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebChannel import QWebChannel
@@ -9,14 +10,12 @@ from PyQt6.QtCore import Qt, pyqtSlot
from backend.db.models import BasicSubmission, BasicSample
from tools import is_power_user, html_to_pdf
from .functions import select_save_file
from io import BytesIO
from pathlib import Path
import logging, base64
import logging
from getpass import getuser
from datetime import datetime
from pprint import pformat
from html2image import Html2Image
from PIL import Image
from typing import List
from backend.excel.writer import DocxWriter
@@ -135,7 +134,7 @@ class SubmissionComment(QDialog):
pass
self.submission = submission
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.setReadOnly(False)
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'])
# 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")
html_to_pdf(html=html, output_file=fname)
writer = pd.ExcelWriter(fname.with_suffix(".xlsx"), engine='openpyxl')
summary_df.to_excel(writer, sheet_name="Report")
detailed_df.to_excel(writer, sheet_name="Details", index=False)
worksheet: Worksheet = writer.sheets['Report']
for idx, col in enumerate(summary_df, start=1): # loop through all columns
series = summary_df[col]
max_len = max((
series.astype(str).map(len).max(), # len of largest item
len(str(series.name)) # len of column name/header
)) + 20 # adding a little extra space
try:
# NOTE: Convert idx to letter
col_letter = chr(ord('@') + idx)
worksheet.column_dimensions[col_letter].width = max_len
except ValueError:
pass
blank_row = get_first_blank_df_row(summary_df) + 1
# logger.debug(f"Blank row index = {blank_row}")
for col in range(3,6):
col_letter = row_map[col]
worksheet.cell(row=blank_row, column=col, value=f"=SUM({col_letter}2:{col_letter}{str(blank_row-1)})")
for cell in worksheet['D']:
if cell.row > 1:
cell.style = 'Currency'
writer.close()
# rp = ReportMaker(start_date=info['start_date'], end_date=info['end_date'])
# rp.write_report(filename=fname, obj=self)
# html_to_pdf(html=html, output_file=fname)
# writer = pd.ExcelWriter(fname.with_suffix(".xlsx"), engine='openpyxl')
# summary_df.to_excel(writer, sheet_name="Report")
# detailed_df.to_excel(writer, sheet_name="Details", index=False)
# worksheet: Worksheet = writer.sheets['Report']
# for idx, col in enumerate(summary_df, start=1): # loop through all columns
# series = summary_df[col]
# max_len = max((
# series.astype(str).map(len).max(), # len of largest item
# len(str(series.name)) # len of column name/header
# )) + 20 # adding a little extra space
# try:
# # NOTE: Convert idx to letter
# col_letter = chr(ord('@') + idx)
# worksheet.column_dimensions[col_letter].width = max_len
# except ValueError:
# pass
# blank_row = get_first_blank_df_row(summary_df) + 1
# # logger.debug(f"Blank row index = {blank_row}")
# for col in range(3,6):
# col_letter = row_map[col]
# worksheet.cell(row=blank_row, column=col, value=f"=SUM({col_letter}2:{col_letter}{str(blank_row-1)})")
# for cell in worksheet['D']:
# if cell.row > 1:
# cell.style = 'Currency'
# writer.close()
rp = ReportMaker(start_date=info['start_date'], end_date=info['end_date'])
rp.write_report(filename=fname, obj=self)
self.report.add_result(report)

View File

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

View File

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

View File

@@ -7,7 +7,6 @@ import json
import numpy as np
import logging, re, yaml, sys, os, stat, platform, getpass, inspect, csv
import pandas as pd
from PyQt6.QtWidgets import QWidget
from jinja2 import Environment, FileSystemLoader
from logging import handlers
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:
"""
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:
case dict():
if key in interest.keys():
@@ -105,10 +115,9 @@ def check_not_nan(cell_contents) -> bool:
Returns:
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']
try:
# if "Unnamed:" in cell_contents or "blank" in cell_contents.lower():
if cell_contents.lower() in exclude:
cell_contents = np.nan
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]:
"""
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):
return value, False
else:
@@ -262,19 +280,19 @@ class Settings(BaseSettings, extra="allow"):
else:
database_path = values.data['database_path']
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():
database_path = Path.home().joinpath(".submissions", "submissions.db")
# finally, look in the local dir
# NOTE: finally, look in the local dir
else:
database_path = package_dir.joinpath("submissions.db")
else:
if database_path == ":memory:":
pass
# check if user defined path is directory
# NOTE: check if user defined path is directory
elif database_path.is_dir():
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():
database_path = database_path
else:
@@ -282,7 +300,6 @@ class Settings(BaseSettings, extra="allow"):
logger.info(f"Using {database_path} for database file.")
engine = create_engine(f"sqlite:///{database_path}") #, echo=True, future=True)
session = Session(engine)
# metadata.session = session
return session
@field_validator('package', mode="before")
@@ -403,7 +420,7 @@ class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler):
"""
# Rotate the file first.
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
os.chmod(self.baseFilename, currMode | stat.S_IWGRP)
@@ -629,6 +646,12 @@ class Report(BaseModel):
return f"Report(result_count:{len(self.results)})"
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:
case Result():
logger.info(f"Adding {result} to results.")
@@ -644,15 +667,30 @@ class Report(BaseModel):
case _:
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]
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):
output_file = Path(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):
"""
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:
c = csv.writer(f)
for r in worksheet.rows:
@@ -698,6 +743,12 @@ ctx = get_config(None)
def is_power_user() -> bool:
"""
Checks if user is in list of power users
Returns:
bool: True if yes, False if no.
"""
try:
check = getpass.getuser() in ctx.power_users
except: