From e047d1a9ee110873ebb1b93e949dca0bb8fe4285 Mon Sep 17 00:00:00 2001 From: lwark Date: Fri, 24 May 2024 13:02:46 -0500 Subject: [PATCH] Code cleanup, dependency update, various bug fixes --- CHANGELOG.md | 3 +- requirements.txt | Bin 4824 -> 0 bytes src/submissions/backend/db/models/__init__.py | 4 +- src/submissions/backend/db/models/controls.py | 15 +- src/submissions/backend/db/models/kits.py | 62 +++--- .../backend/db/models/submissions.py | 83 ++++--- src/submissions/backend/excel/parser.py | 77 +------ src/submissions/backend/excel/reports.py | 34 +-- src/submissions/backend/excel/writer.py | 17 +- .../backend/validators/__init__.py | 18 +- src/submissions/backend/validators/pydant.py | 82 +++---- .../frontend/visualizations/control_charts.py | 2 +- src/submissions/frontend/widgets/app.py | 26 +-- .../frontend/widgets/controls_chart.py | 12 +- .../frontend/widgets/equipment_usage.py | 8 +- .../frontend/widgets/gel_checker.py | 2 +- .../frontend/widgets/kit_creator.py | 18 +- src/submissions/frontend/widgets/misc.py | 6 +- .../frontend/widgets/submission_details.py | 38 ++-- .../frontend/widgets/submission_table.py | 111 ++-------- .../widgets/submission_type_creator.py | 6 +- .../frontend/widgets/submission_widget.py | 136 ++++-------- .../templates/basicsubmission_details.html | 2 +- src/submissions/tools.py | 202 ++++++++++-------- 24 files changed, 403 insertions(+), 561 deletions(-) delete mode 100644 requirements.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index ad0d678..439cf92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,6 @@ ## 202405.04 -## 202405.04 - +- Improved Webview of submission details. - Fixed Reagents not being updated on edit. - Fixed data resorting after submitting new run. diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index b9a2fefb50c1634a666968d7b7b76596012af754..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4824 zcmai&TW?xd5QX=1rT&yt1?=1&^3Y0^sw$CM<)P}6g@M>O_<#Xle|+2Z&Dz60U?-JD zAk6HUHES+=4*&i$Da$e`=X!0+uFT7(e9+&k@>6-M*QmTHKb9XPT}pN>y+xUZ#wesG z@o#GE%7xBP%DPXqzO3YD624}o)BB_J=klI@-^k{xs*g4M>|`0O%dnp~ z@ekJ2-^%_-2sdGMCtp|v>ubG6)hdx9`znil^*omRbfAOAW-r4J8us%`pqh<n>XS z&RPnWQ`w&CeOYzD*v_BJ@1)$y!=ilA`%->ZdSA;57L5b_lR$hfxh0MjTX24^GYg%b z#VMjR7GzWT*i;XTkdK9a9JbN2g{l01k&R@Dd}ox?;CdtLbJ;-y4qFr{--f)4%x+X8 zM7=0a;h$4*Xa0-}Df}RNCmFckMm$rou+)pJV+Rc)h7Z@_Fte@J9z*lAVk6h~v=wil zqjsl?VZ66UZ97{;hhKbB6Rv+(OIFM*s${L2+KH2u{s%IcfGt!&vvQ|TFflvu>-;tSojT+}rw_uhIy=i1c&XE%7z;5mTvR)*250D4lWFpxb4GJmd(LKBeH~1k?~$2NxasAK z^Pq8!H2+&+p~^Cz*1hPgBbU2s`ApwX#godnt$M;Gj|+LDGRZGJamb^qWg)wVRX_#1 ztE_P)KQo;MmHB_a&;RfGzAslo;YuVba%tw4HPf8TPBRmqbmFUUWu%<*86+FYm>r+> z`Ag*^>&Trz#&*)6w~~=Auuy@0J!7=V^R*<*$=kg=b8VZwB(Ut>^X#D;Jn2`GP*2Do z^G0;!%J@J>j&%&QS+hRmv~ z0B)P5Tk&u)V13BOMjprsQ-fK?H2o@chaG)dYiC{OL8tgUjmQDmha5hNgJd#qUCW(E zGRzF?cjJ&!u2M+W?ar^lXWlXn4_T%vwa`^1L%D0f$n6KFmdW8+ZULtAU1iv3hM1p1((39^hX1*I(2s1nbTTzbXJ6{`b(}U= z-@3TQKkjQ?GJE}zQBuYbH(;J7nCfiwOn4Y&hhvxUo@kWW0lCFvk(t`2m2ZmA;tYQJ>JO|;7~ePB zfwYqk-|Xy__rY!3_R~8IyV@N`v(D4fPMmNLTn6Tbg;TDJp+`0(Dz6utOEH#wqRmWj zy$?@#Q_4Bi3GZ5e+?IG;>NEMs8SyT5^6#C;8`EB3~D>>}B z$h@9Ok0%&rB|XiI^9(_MRBPMVpVTo-LbG-}Id^(eVYl;d0${_hcMmpfeg|;Gtz!AU z{YufEbzp%DLUK; F{{!{p$Xoyb diff --git a/src/submissions/backend/db/models/__init__.py b/src/submissions/backend/db/models/__init__.py index 37cc85f..1f8ad0d 100644 --- a/src/submissions/backend/db/models/__init__.py +++ b/src/submissions/backend/db/models/__init__.py @@ -102,7 +102,7 @@ class BaseClass(Base): @classmethod def query(cls, **kwargs) -> Any | List[Any]: """ - Default query function for models + Default query function for models. Overridden in most models. Returns: Any | List[Any]: Result of query execution. @@ -128,7 +128,7 @@ class BaseClass(Base): query: Query = cls.__database_session__.query(model) # logger.debug(f"Grabbing singles using {model.get_default_info}") singles = model.get_default_info('singles') - logger.debug(f"Querying: {model}, with kwargs: {kwargs}") + logger.info(f"Querying: {model}, with kwargs: {kwargs}") for k, v in kwargs.items(): # logger.debug(f"Using key: {k} with value: {v}") try: diff --git a/src/submissions/backend/db/models/controls.py b/src/submissions/backend/db/models/controls.py index 6b2a491..23fdcf2 100644 --- a/src/submissions/backend/db/models/controls.py +++ b/src/submissions/backend/db/models/controls.py @@ -63,16 +63,16 @@ class ControlType(BaseClass): Returns: List[str]: list of subtypes available """ - # Get first instance since all should have same subtypes - # Get mode of instance + # NOTE: Get first instance since all should have same subtypes + # NOTE: Get mode of instance jsoner = getattr(self.instances[0], mode) # logger.debug(f"JSON out: {jsoner.keys()}") try: - # Pick genera (all should have same subtypes) + # NOTE: Pick genera (all should have same subtypes) genera = list(jsoner.keys())[0] except IndexError: return [] - # remove items that don't have relevant data + # NOTE: remove items that don't have relevant data subtypes = [item for item in jsoner[genera] if "_hashes" not in item and "_ratio" not in item] return subtypes @@ -135,7 +135,6 @@ class Control(BaseClass): """ # logger.debug("loading json string into dict") try: - # kraken = json.loads(self.kraken) kraken = self.kraken except TypeError: kraken = {} @@ -178,7 +177,7 @@ class Control(BaseClass): data = self.__getattribute__(mode) except TypeError: data = {} - logger.debug(f"Length of data: {len(data)}") + # logger.debug(f"Length of data: {len(data)}") # logger.debug("dict keys are genera of bacteria, e.g. 'Streptococcus'") for genus in data: _dict = dict( @@ -236,7 +235,7 @@ class Control(BaseClass): models.Control|List[models.Control]: Control object of interest. """ query: Query = cls.__database_session__.query(cls) - # by control type + # NOTE: by control type match control_type: case ControlType(): # logger.debug(f"Looking up control by control type: {control_type}") @@ -246,7 +245,7 @@ class Control(BaseClass): query = query.join(ControlType).filter(ControlType.name == control_type) case _: pass - # by date range + # NOTE: by date range if start_date is not None and end_date is None: logger.warning(f"Start date with no end date, using today.") end_date = date.today() diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index 97a31bb..b2ebaa2 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -120,8 +120,8 @@ class KitType(BaseClass): submission_type (str | Submissiontype | None, optional): Submission type to narrow results. Defaults to None. Returns: - list: List of reagent types - """ + List[ReagentType]: List of reagents linked to this kit. + """ match submission_type: case SubmissionType(): # logger.debug(f"Getting reagents by SubmissionType {submission_type}") @@ -152,17 +152,15 @@ class KitType(BaseClass): dict: Dictionary containing information locations. """ info_map = {} - # Account for submission_type variable type. + # NOTE: Account for submission_type variable type. match submission_type: case str(): # logger.debug(f"Constructing xl map with str {submission_type}") assocs = [item for item in self.kit_reagenttype_associations if item.submission_type.name == submission_type] - # st_assoc = [item for item in self.used_for if submission_type == item.name][0] case SubmissionType(): # logger.debug(f"Constructing xl map with SubmissionType {submission_type}") assocs = [item for item in self.kit_reagenttype_associations if item.submission_type == submission_type] - # st_assoc = submission_type case _: raise ValueError(f"Wrong variable type: {type(submission_type)} used!") # logger.debug("Get all KitTypeReagentTypeAssociation for SubmissionType") @@ -371,10 +369,10 @@ class Reagent(BaseClass): dict: representation of the reagent's attributes """ if extraction_kit is not None: - # Get the intersection of this reagent's ReagentType and all ReagentTypes in KitType + # NOTE: Get the intersection of this reagent's ReagentType and all ReagentTypes in KitType try: reagent_role = list(set(self.type).intersection(extraction_kit.reagent_types))[0] - # Most will be able to fall back to first ReagentType in itself because most will only have 1. + # NOTE: Most will be able to fall back to first ReagentType in itself because most will only have 1. except: reagent_role = self.type[0] else: @@ -383,7 +381,7 @@ class Reagent(BaseClass): rtype = reagent_role.name.replace("_", " ") except AttributeError: rtype = "Unknown" - # Calculate expiry with EOL from ReagentType + # NOTE: Calculate expiry with EOL from ReagentType try: place_holder = self.expiry + reagent_role.eol_ext except (TypeError, AttributeError) as e: @@ -467,7 +465,7 @@ class Reagent(BaseClass): match name: case str(): # logger.debug(f"Looking up reagent by name str: {name}") - # Not limited due to multiple reagents having same name. + # NOTE: Not limited due to multiple reagents having same name. query = query.filter(cls.name == name) case _: pass @@ -475,7 +473,7 @@ class Reagent(BaseClass): case str(): # logger.debug(f"Looking up reagent by lot number str: {lot_number}") query = query.filter(cls.lot == lot_number) - # In this case limit number returned. + # NOTE: In this case limit number returned. limit = 1 case _: pass @@ -516,10 +514,6 @@ class Discount(BaseClass): organization (models.Organization | str | int): Organization receiving discount. kit_type (models.KitType | str | int): Kit discount received on. - Raises: - ValueError: Invalid Organization - ValueError: Invalid kit. - Returns: models.Discount|List[models.Discount]: Discount(s) of interest. """ @@ -535,7 +529,6 @@ class Discount(BaseClass): # logger.debug(f"Looking up discount with organization id: {organization}") query = query.join(Organization).filter(Organization.id == organization) case _: - # raise ValueError(f"Invalid value for organization: {organization}") pass match kit_type: case KitType(): @@ -548,7 +541,6 @@ class Discount(BaseClass): # logger.debug(f"Looking up discount with kit type id: {kit_type}") query = query.join(KitType).filter(KitType.id == kit_type) case _: - # raise ValueError(f"Invalid value for kit type: {kit_type}") pass return cls.execute_query(query=query) @@ -634,11 +626,18 @@ class SubmissionType(BaseClass): self.save() def construct_info_map(self, mode: Literal['read', 'write']) -> dict: + """ + Make of map of where all fields are located in excel sheet + + Args: + mode (Literal["read", "write"]): Which mode to get locations for + + Returns: + dict: Map of locations + """ info = self.info_map # logger.debug(f"Info map: {info}") output = {} - # for k,v in info.items(): - # info[k]['write'] += info[k]['read'] match mode: case "read": output = {k: v[mode] for k, v in info.items() if v[mode]} @@ -647,7 +646,13 @@ class SubmissionType(BaseClass): output = {k: v for k, v in output.items() if all([isinstance(item, dict) for item in v])} return output - def construct_sample_map(self): + def construct_sample_map(self) -> dict: + """ + Returns sample map + + Returns: + dict: sample location map + """ return self.sample_map def construct_equipment_map(self) -> dict: @@ -655,7 +660,7 @@ class SubmissionType(BaseClass): Constructs map of equipment to excel cells. Returns: - List[dict]: List of equipment locations in excel sheet + dict: Map equipment locations in excel sheet """ output = {} # logger.debug("Iterating through equipment roles") @@ -671,7 +676,7 @@ class SubmissionType(BaseClass): Returns PydEquipmentRole of all equipment associated with this SubmissionType Returns: - List['PydEquipmentRole']: List of equipment roles + List[PydEquipmentRole]: List of equipment roles """ return [item.to_pydantic(submission_type=self, extraction_kit=extraction_kit) for item in self.equipment] @@ -702,7 +707,13 @@ class SubmissionType(BaseClass): raise TypeError(f"Type {type(equipment_role)} is not allowed") return list(set([item for items in relevant for item in items if item != None])) - def get_submission_class(self): + def get_submission_class(self) -> "BasicSubmission": + """ + Gets submission class associated with this submission type. + + Returns: + BasicSubmission: Submission class + """ from .submissions import BasicSubmission return BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.name) @@ -1063,7 +1074,7 @@ class Equipment(BaseClass): processes (bool, optional): Whether to include processes. Defaults to False. Returns: - dict: _description_ + dict: Dictionary representation of this equipment """ if not processes: return {k: v for k, v in self.__dict__.items() if k != 'processes'} @@ -1152,7 +1163,7 @@ class Equipment(BaseClass): extraction_kit (str | KitType | None, optional): Relevant KitType. Defaults to None. Returns: - PydEquipment: _description_ + PydEquipment: pydantic equipment object """ from backend.validators.pydant import PydEquipment return PydEquipment( @@ -1179,7 +1190,6 @@ class Equipment(BaseClass): class EquipmentRole(BaseClass): """ Abstract roles for equipment - """ id = Column(INTEGER, primary_key=True) #: Role id, primary key @@ -1331,7 +1341,7 @@ class SubmissionEquipmentAssociation(BaseClass): equipment = relationship(Equipment, back_populates="equipment_submission_associations") #: associated equipment def __repr__(self): - return f"" + return f"" def __init__(self, submission, equipment, role: str = "None"): self.submission = submission diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 8afcefc..bbab71e 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -107,6 +107,12 @@ class BasicSubmission(BaseClass): @classmethod def jsons(cls) -> List[str]: + """ + Get list of JSON db columns + + Returns: + List[str]: List of column names + """ output = [item.name for item in cls.__table__.columns if isinstance(item.type, JSON)] if issubclass(cls, BasicSubmission) and not cls.__name__ == "BasicSubmission": output += BasicSubmission.jsons() @@ -114,6 +120,12 @@ class BasicSubmission(BaseClass): @classmethod def timestamps(cls) -> List[str]: + """ + Get list of TIMESTAMP columns + + Returns: + List[str]: List of column names + """ output = [item.name for item in cls.__table__.columns if isinstance(item.type, TIMESTAMP)] if issubclass(cls, BasicSubmission) and not cls.__name__ == "BasicSubmission": output += BasicSubmission.timestamps() @@ -122,7 +134,7 @@ class BasicSubmission(BaseClass): # TODO: Beef up this to include info_map from DB @classmethod def get_default_info(cls, *args): - # Create defaults for all submission_types + # NOTE: Create defaults for all submission_types parent_defs = super().get_default_info() recover = ['filepath', 'samples', 'csv', 'comment', 'equipment'] dicto = dict( @@ -132,9 +144,7 @@ class BasicSubmission(BaseClass): # NOTE: Fields not placed in ui form form_ignore=['reagents', 'ctx', 'id', 'cost', 'extraction_info', 'signed_by', 'comment'] + recover, # NOTE: Fields not placed in ui form to be moved to pydantic - form_recover=recover, - # parser_ignore=['samples', 'signed_by'] + [item for item in cls.jsons() if item != "comment"], - # excel_ignore=[], + form_recover=recover ) # logger.debug(dicto['singles']) # NOTE: Singles tells the query which fields to set limit to 1 @@ -151,7 +161,6 @@ class BasicSubmission(BaseClass): st = cls.get_submission_type() if st is None: logger.error("No default info for BasicSubmission.") - # return output else: output['submission_type'] = st.name for k, v in st.defaults.items(): @@ -169,16 +178,37 @@ class BasicSubmission(BaseClass): return output @classmethod - def get_submission_type(cls): + def get_submission_type(cls) -> SubmissionType: + """ + Gets the SubmissionType associated with this class + + Returns: + SubmissionType: SubmissionType with name equal to this polymorphic identity + """ name = cls.__mapper_args__['polymorphic_identity'] return SubmissionType.query(name=name) @classmethod - def construct_info_map(cls, mode:Literal['read', 'write']): + def construct_info_map(cls, mode:Literal["read", "write"]) -> dict: + """ + Method to call submission type's construct info map. + + Args: + mode (Literal["read", "write"]): Which map to construct. + + Returns: + dict: Map of info locations. + """ return cls.get_submission_type().construct_info_map(mode=mode) @classmethod - def construct_sample_map(cls): + def construct_sample_map(cls) -> dict: + """ + Method to call submission type's construct_sample_map + + Returns: + dict: sample location map + """ return cls.get_submission_type().construct_sample_map() def to_dict(self, full_data: bool = False, backup: bool = False, report: bool = False) -> dict: @@ -192,7 +222,7 @@ class BasicSubmission(BaseClass): Returns: dict: dictionary used in submissions summary and details """ - # get lab from nested organization object + # NOTE: get lab from nested organization object # logger.debug(f"Converting {self.rsl_plate_num} to dict...") try: sub_lab = self.submitting_lab.name @@ -202,12 +232,12 @@ class BasicSubmission(BaseClass): sub_lab = sub_lab.replace("_", " ").title() except AttributeError: pass - # get extraction kit name from nested kit object + # NOTE: get extraction kit name from nested kit object try: ext_kit = self.extraction_kit.name except AttributeError: ext_kit = None - # load scraped extraction info + # NOTE: load scraped extraction info try: ext_info = self.extraction_info except TypeError: @@ -324,7 +354,7 @@ class BasicSubmission(BaseClass): def make_plate_map(self, plate_rows: int = 8, plate_columns=12) -> str: """ - Constructs an html based plate map. + Constructs an html based plate map for submission details. Args: sample_list (list): List of submission samples @@ -386,7 +416,7 @@ class BasicSubmission(BaseClass): subs = [item.to_dict() for item in cls.query(submission_type=submission_type, limit=limit, chronologic=chronologic)] # logger.debug(f"Got {len(subs)} submissions.") df = pd.DataFrame.from_records(subs) - # Exclude sub information + # NOTE: Exclude sub information for item in ['controls', 'extraction_info', 'pcr_info', 'comment', 'comments', 'samples', 'reagents', 'equipment', 'gel_info', 'gel_image', 'dna_core_submission_number', 'gel_controls']: try: @@ -414,9 +444,6 @@ class BasicSubmission(BaseClass): # logger.debug(f"Looking up organization: {value}") field_value = Organization.query(name=value) # logger.debug(f"Got {field_value} for organization {value}") - # case "submitter_plate_num": - # # logger.debug(f"Submitter plate id: {value}") - # field_value = value case "samples": for sample in value: # logger.debug(f"Parsing {sample} to sql.") @@ -436,17 +463,6 @@ class BasicSubmission(BaseClass): field_value = value case "ctx" | "csv" | "filepath" | "equipment": return - # case "comment": - # if value == "" or value == None or value == 'null': - # field_value = None - # else: - # field_value = dict(name=getuser(), text=value, time=datetime.now()) - # # if self.comment is None: - # # self.comment = [field_value] - # # else: - # # self.comment.append(field_value) - # self.update_json(field=key, value=field_value) - # return case item if item in self.jsons(): logger.debug(f"Setting JSON attribute.") existing = self.__getattribute__(key) @@ -1852,13 +1868,6 @@ class WastewaterArtic(BasicSubmission): set_plate = None for assoc in self.submission_sample_associations: dicto = assoc.to_sub_dict() - # old_sub = assoc.sample.get_previous_ww_submission(current_artic_submission=self) - # try: - # dicto['plate_name'] = old_sub.rsl_plate_num - # except AttributeError: - # dicto['plate_name'] = "" - # old_assoc = WastewaterAssociation.query(submission=old_sub, sample=assoc.sample, limit=1) - # dicto['well'] = f"{row_map[old_assoc.row]}{old_assoc.column}" for item in self.source_plates: old_plate = WastewaterAssociation.query(submission=item['plate'], sample=assoc.sample, limit=1) if old_plate is not None: @@ -1879,6 +1888,12 @@ class WastewaterArtic(BasicSubmission): events['Gel Box'] = self.gel_box return events + def set_attribute(self, key: str, value): + super().set_attribute(key=key, value=value) + if key == 'gel_info': + if len(self.gel_info) > 3: + self.gel_info = self.gel_info[-3:] + def gel_box(self, obj): """ Creates widget to perform gel viewing operations diff --git a/src/submissions/backend/excel/parser.py b/src/submissions/backend/excel/parser.py index d0cc73b..fca7e8e 100644 --- a/src/submissions/backend/excel/parser.py +++ b/src/submissions/backend/excel/parser.py @@ -543,7 +543,6 @@ class EquipmentParser(object): def __init__(self, xl: Workbook, submission_type: str|SubmissionType) -> None: if isinstance(submission_type, str): submission_type = SubmissionType.query(name=submission_type) - self.submission_type = submission_type self.xl = xl self.map = self.fetch_equipment_map() @@ -555,7 +554,6 @@ class EquipmentParser(object): Returns: List[dict]: List of locations """ - # submission_type = SubmissionType.query(name=self.submission_type) return self.submission_type.construct_equipment_map() def get_asset_number(self, input: str) -> str: @@ -569,7 +567,7 @@ class EquipmentParser(object): str: asset number """ regex = Equipment.get_regex() - logger.debug(f"Using equipment regex: {regex} on {input}") + # logger.debug(f"Using equipment regex: {regex} on {input}") try: return regex.search(input).group().strip("-") except AttributeError: @@ -582,11 +580,10 @@ class EquipmentParser(object): Returns: List[PydEquipment]: list of equipment """ - logger.debug(f"Equipment parser going into parsing: {pformat(self.__dict__)}") + # logger.debug(f"Equipment parser going into parsing: {pformat(self.__dict__)}") output = [] # logger.debug(f"Sheets: {sheets}") for sheet in self.xl.sheetnames: - # df = self.xl.parse(sheet, header=None, dtype=object) ws = self.xl[sheet] try: relevant = [item for item in self.map if item['sheet'] == sheet] @@ -595,7 +592,6 @@ class EquipmentParser(object): # logger.debug(f"Relevant equipment: {pformat(relevant)}") previous_asset = "" for equipment in relevant: - # asset = df.iat[equipment['name']['row']-1, equipment['name']['column']-1] asset = ws.cell(equipment['name']['row'], equipment['name']['column']) if not check_not_nan(asset): asset = previous_asset @@ -603,7 +599,6 @@ class EquipmentParser(object): previous_asset = asset asset = self.get_asset_number(input=asset) eq = Equipment.query(asset_number=asset) - # process = df.iat[equipment['process']['row']-1, equipment['process']['column']-1] process = ws.cell(row=equipment['process']['row'], column=equipment['process']['column']) try: output.append( @@ -614,72 +609,6 @@ class EquipmentParser(object): # logger.debug(f"Here is the output so far: {pformat(output)}") return output - -# class PCRParser(object): -# """ -# Object to pull data from Design and Analysis PCR export file. -# """ -# -# def __init__(self, filepath: Path | None = None) -> None: -# """ -# Initializes object. -# -# Args: -# filepath (Path | None, optional): file to parse. Defaults to None. -# """ -# logger.debug(f"Parsing {filepath.__str__()}") -# if filepath == None: -# logger.error(f"No filepath given.") -# self.xl = None -# else: -# try: -# self.xl = pd.ExcelFile(filepath.__str__()) -# except ValueError as e: -# logger.error(f"Incorrect value: {e}") -# self.xl = None -# except PermissionError: -# logger.error(f"Couldn't get permissions for {filepath.__str__()}. Operation might have been cancelled.") -# return -# self.parse_general(sheet_name="Results") -# namer = RSLNamer(filename=filepath.__str__()) -# self.plate_num = namer.parsed_name -# self.submission_type = namer.submission_type -# logger.debug(f"Set plate number to {self.plate_num} and type to {self.submission_type}") -# parser = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type) -# self.samples = parser.parse_pcr(xl=self.xl, rsl_number=self.plate_num) -# -# def parse_general(self, sheet_name: str): -# """ -# Parse general info rows for all types of PCR results -# -# Args: -# sheet_name (str): Name of sheet in excel workbook that holds info. -# """ -# self.pcr = {} -# df = self.xl.parse(sheet_name=sheet_name, dtype=object).fillna("") -# self.pcr['comment'] = df.iloc[0][1] -# self.pcr['operator'] = df.iloc[1][1] -# self.pcr['barcode'] = df.iloc[2][1] -# self.pcr['instrument'] = df.iloc[3][1] -# self.pcr['block_type'] = df.iloc[4][1] -# self.pcr['instrument_name'] = df.iloc[5][1] -# self.pcr['instrument_serial'] = df.iloc[6][1] -# self.pcr['heated_cover_serial'] = df.iloc[7][1] -# self.pcr['block_serial'] = df.iloc[8][1] -# self.pcr['run-start'] = df.iloc[9][1] -# self.pcr['run_end'] = df.iloc[10][1] -# self.pcr['run_duration'] = df.iloc[11][1] -# self.pcr['sample_volume'] = df.iloc[12][1] -# self.pcr['cover_temp'] = df.iloc[13][1] -# self.pcr['passive_ref'] = df.iloc[14][1] -# self.pcr['pcr_step'] = df.iloc[15][1] -# self.pcr['quant_cycle_method'] = df.iloc[16][1] -# self.pcr['analysis_time'] = df.iloc[17][1] -# self.pcr['software'] = df.iloc[18][1] -# self.pcr['plugin'] = df.iloc[19][1] -# self.pcr['exported_on'] = df.iloc[20][1] -# self.pcr['imported_by'] = getuser() - class PCRParser(object): """Object to pull data from Design and Analysis PCR export file.""" @@ -690,7 +619,7 @@ class PCRParser(object): Args: filepath (Path | None, optional): file to parse. Defaults to None. """ - logger.debug(f'Parsing {filepath.__str__()}') + # logger.debug(f'Parsing {filepath.__str__()}') if filepath is None: logger.error('No filepath given.') self.xl = None diff --git a/src/submissions/backend/excel/reports.py b/src/submissions/backend/excel/reports.py index 22ddba0..2f9497c 100644 --- a/src/submissions/backend/excel/reports.py +++ b/src/submissions/backend/excel/reports.py @@ -27,7 +27,7 @@ def make_report_xlsx(records:list[dict]) -> Tuple[DataFrame, DataFrame]: # aggregate cost and sample count columns df2 = df.groupby(["Submitting Lab", "Extraction Kit"]).agg({'Extraction Kit':'count', 'Cost': 'sum', 'Sample Count':'sum'}) df2 = df2.rename(columns={"Extraction Kit": 'Run Count'}) - logger.debug(f"Output daftaframe for xlsx: {df2.columns}") + # logger.debug(f"Output daftaframe for xlsx: {df2.columns}") df = df.drop('id', axis=1) df = df.sort_values(['Submitting Lab', "Submitted Date"]) return df, df2 @@ -47,13 +47,13 @@ def make_report_html(df:DataFrame, start_date:date, end_date:date) -> str: """ old_lab = "" output = [] - logger.debug(f"Report DataFrame: {df}") + # logger.debug(f"Report DataFrame: {df}") for ii, row in enumerate(df.iterrows()): - logger.debug(f"Row {ii}: {row}") + # logger.debug(f"Row {ii}: {row}") lab = row[0][0] - logger.debug(type(row)) - logger.debug(f"Old lab: {old_lab}, Current lab: {lab}") - logger.debug(f"Name: {row[0][1]}") + # logger.debug(type(row)) + # logger.debug(f"Old lab: {old_lab}, Current lab: {lab}") + # logger.debug(f"Name: {row[0][1]}") data = [item for item in row[1]] kit = dict(name=row[0][1], cost=data[1], run_count=int(data[0]), sample_count=int(data[2])) # if this is the same lab as before add together @@ -67,7 +67,7 @@ def make_report_html(df:DataFrame, start_date:date, end_date:date) -> str: adder = dict(lab=lab, kits=[kit], total_cost=kit['cost'], total_samples=kit['sample_count'], total_runs=kit['run_count']) output.append(adder) old_lab = lab - logger.debug(output) + # logger.debug(output) dicto = {'start_date':start_date, 'end_date':end_date, 'labs':output}#, "table":table} temp = env.get_template('summary_report.html') html = temp.render(input=dicto) @@ -91,14 +91,14 @@ def convert_data_list_to_df(input:list[dict], subtype:str|None=None) -> DataFram for column in df.columns: if "percent" in column: count_col = [item for item in df.columns if "count" in item][0] - # The actual percentage from kraken was off due to exclusion of NaN, recalculating. + # NOTE: The actual percentage from kraken was off due to exclusion of NaN, recalculating. df[column] = 100 * df[count_col] / df.groupby('name')[count_col].transform('sum') if column not in safe: if subtype != None and column != subtype: del df[column] - # move date of sample submitted on same date as previous ahead one. + # NOTE: move date of sample submitted on same date as previous ahead one. df = displace_date(df) - # ad hoc method to make data labels more accurate. + # NOTE: ad hoc method to make data labels more accurate. df = df_column_renamer(df=df) return df @@ -131,8 +131,8 @@ def displace_date(df:DataFrame) -> DataFrame: Returns: DataFrame: output dataframe with dates incremented. """ - logger.debug(f"Unique items: {df['name'].unique()}") - # get submitted dates for each control + # logger.debug(f"Unique items: {df['name'].unique()}") + # NOTE: get submitted dates for each control dict_list = [dict(name=item, date=df[df.name == item].iloc[0]['submitted_date']) for item in sorted(df['name'].unique())] previous_dates = [] for _, item in enumerate(dict_list): @@ -157,10 +157,10 @@ def check_date(df:DataFrame, item:dict, previous_dates:list) -> Tuple[DataFrame, check = False previous_dates.append(item['date']) if check: - logger.debug(f"We found one! Increment date!\n\t{item['date']} to {item['date'] + timedelta(days=1)}") - # get df locations where name == item name + # logger.debug(f"We found one! Increment date!\n\t{item['date']} to {item['date'] + timedelta(days=1)}") + # NOTE: get df locations where name == item name mask = df['name'] == item['name'] - # increment date in dataframe + # NOTE: increment date in dataframe df.loc[mask, 'submitted_date'] = df.loc[mask, 'submitted_date'].apply(lambda x: x + timedelta(days=1)) item['date'] += timedelta(days=1) passed = False @@ -170,9 +170,9 @@ def check_date(df:DataFrame, item:dict, previous_dates:list) -> Tuple[DataFrame, # logger.debug(f"DF: {type(df)}, previous_dates: {type(previous_dates)}") # if run didn't lead to changed date, return values if passed: - logger.debug(f"Date check passed, returning.") + # logger.debug(f"Date check passed, returning.") return df, previous_dates - # if date was changed, rerun with new date + # NOTE: if date was changed, rerun with new date else: logger.warning(f"Date check failed, running recursion") df, previous_dates = check_date(df, item, previous_dates) diff --git a/src/submissions/backend/excel/writer.py b/src/submissions/backend/excel/writer.py index 8dc935f..4a6568f 100644 --- a/src/submissions/backend/excel/writer.py +++ b/src/submissions/backend/excel/writer.py @@ -31,7 +31,6 @@ class SheetWriter(object): case 'filepath': self.__setattr__(k, v) case 'submission_type': - # self.__setattr__('submission_type', submission.submission_type['value']) self.sub[k] = v['value'] self.submission_type = SubmissionType.query(name=v['value']) self.sub_object = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type) @@ -40,7 +39,7 @@ class SheetWriter(object): self.sub[k] = v['value'] else: self.sub[k] = v - logger.debug(f"\n\nWriting to {submission.filepath.__str__()}\n\n") + # logger.debug(f"\n\nWriting to {submission.filepath.__str__()}\n\n") if self.filepath.stem.startswith("tmp"): template = self.submission_type.template_file @@ -95,7 +94,7 @@ class InfoWriter(object): self.xl = xl map = submission_type.construct_info_map(mode='write') self.info = self.reconcile_map(info_dict, map) - logger.debug(pformat(self.info)) + # logger.debug(pformat(self.info)) def reconcile_map(self, info_dict: dict, map: dict) -> dict: output = {} @@ -121,8 +120,7 @@ class InfoWriter(object): logger.error(f"No locations for {k}, skipping") continue for loc in locations: - - logger.debug(f"Writing {k} to {loc['sheet']}, row: {loc['row']}, column: {loc['column']}") + # logger.debug(f"Writing {k} to {loc['sheet']}, row: {loc['row']}, column: {loc['column']}") sheet = self.xl[loc['sheet']] sheet.cell(row=loc['row'], column=loc['column'], value=v['value']) return self.sub_object.custom_info_writer(self.xl, info=self.info) @@ -152,7 +150,7 @@ class ReagentWriter(object): try: dicto = dict(value=v, row=mp_info[k]['row'], column=mp_info[k]['column']) except KeyError as e: - # logger.error(f"Keyerror: {e}") + logger.error(f"KeyError: {e}") dicto = v placeholder[k] = dicto placeholder['sheet'] = mp_info['sheet'] @@ -197,7 +195,6 @@ class SampleWriter(object): def write_samples(self): sheet = self.xl[self.map['sheet']] columns = self.map['sample_columns'] - # rows = range(self.map['start_row'], self.map['end_row']+1) for ii, sample in enumerate(self.samples): row = self.map['start_row'] + (sample['submission_rank'] - 1) for k, v in sample.items(): @@ -229,8 +226,6 @@ class EquipmentWriter(object): for jj, (k, v) in enumerate(equipment.items(), start=1): dicto = dict(value=v, row=ii, column=jj) placeholder[k] = dicto - - # output.append(placeholder) else: for jj, (k, v) in enumerate(equipment.items(), start=1): try: @@ -258,8 +253,8 @@ class EquipmentWriter(object): for k, v in equipment.items(): if not isinstance(v, dict): continue - logger.debug( - f"Writing {k}: {v['value']} to {equipment['sheet']}, row: {v['row']}, column: {v['column']}") + # logger.debug( + # f"Writing {k}: {v['value']} to {equipment['sheet']}, row: {v['row']}, column: {v['column']}") if isinstance(v['value'], list): v['value'] = v['value'][0] try: diff --git a/src/submissions/backend/validators/__init__.py b/src/submissions/backend/validators/__init__.py index 7dfb19d..de2590b 100644 --- a/src/submissions/backend/validators/__init__.py +++ b/src/submissions/backend/validators/__init__.py @@ -23,7 +23,7 @@ class RSLNamer(object): if self.submission_type is None: # logger.debug("Creating submission type because none exists") self.submission_type = self.retrieve_submission_type(filename=filename) - logger.debug(f"got submission type: {self.submission_type}") + # logger.debug(f"got submission type: {self.submission_type}") if self.submission_type is not None: # logger.debug("Retrieving BasicSubmission subclass") self.sub_object = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type) @@ -47,7 +47,7 @@ class RSLNamer(object): """ match filename: case Path(): - logger.debug(f"Using path method for {filename}.") + # logger.debug(f"Using path method for {filename}.") if filename.exists(): wb = load_workbook(filename) try: @@ -67,7 +67,7 @@ class RSLNamer(object): submission_type = cls.retrieve_submission_type(filename=filename.stem.__str__()) case str(): regex = BasicSubmission.construct_regex() - logger.debug(f"Using string method for {filename}.") + # logger.debug(f"Using string method for {filename}.") m = regex.search(filename) try: submission_type = m.lastgroup @@ -100,17 +100,17 @@ class RSLNamer(object): regex (str): string to construct pattern filename (str): string to be parsed """ - logger.debug(f"Input string to be parsed: {filename}") + # logger.debug(f"Input string to be parsed: {filename}") if regex is None: regex = BasicSubmission.construct_regex() else: regex = re.compile(rf'{regex}', re.IGNORECASE | re.VERBOSE) - logger.debug(f"Using regex: {regex}") + # logger.debug(f"Using regex: {regex}") match filename: case Path(): m = regex.search(filename.stem) case str(): - logger.debug(f"Using string method.") + # logger.debug(f"Using string method.") m = regex.search(filename) case _: m = None @@ -121,7 +121,7 @@ class RSLNamer(object): parsed_name = None else: parsed_name = None - logger.debug(f"Got parsed submission name: {parsed_name}") + # logger.debug(f"Got parsed submission name: {parsed_name}") return parsed_name @classmethod @@ -167,8 +167,8 @@ class RSLNamer(object): Returns: str: output file name. """ - logger.debug(f"Kwargs: {kwargs}") - logger.debug(f"Template: {template}") + # logger.debug(f"Kwargs: {kwargs}") + # logger.debug(f"Template: {template}") environment = jinja_template_loading() template = environment.from_string(template) return template.render(**kwargs) diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 96ddc74..bf0236a 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -134,15 +134,15 @@ class PydReagent(BaseModel): # logger.debug("Adding extra fields.") if self.model_extra != None: self.__dict__.update(self.model_extra) - logger.debug(f"Reagent SQL constructor is looking up type: {self.type}, lot: {self.lot}") + # logger.debug(f"Reagent SQL constructor is looking up type: {self.type}, lot: {self.lot}") reagent = Reagent.query(lot_number=self.lot, name=self.name) - logger.debug(f"Result: {reagent}") + # logger.debug(f"Result: {reagent}") if reagent is None: reagent = Reagent() for key, value in self.__dict__.items(): if isinstance(value, dict): 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 match key: case "lot": @@ -191,7 +191,7 @@ class PydSample(BaseModel, extra='allow'): @model_validator(mode='after') @classmethod def validate_model(cls, data): - logger.debug(f"Data for pydsample: {data}") + # logger.debug(f"Data for pydsample: {data}") model = BasicSample.find_polymorphic_subclass(polymorphic_identity=data.sample_type) for k, v in data.model_extra.items(): print(k, v) @@ -200,7 +200,7 @@ class PydSample(BaseModel, extra='allow'): v = datetime.strptime(v, "%Y-%m-%d") data.__setattr__(k, v) # print(dir(data)) - logger.debug(f"Data coming out of validation: {pformat(data)}") + # logger.debug(f"Data coming out of validation: {pformat(data)}") return data @field_validator("row", "column", "assoc_id", "submission_rank") @@ -233,7 +233,7 @@ class PydSample(BaseModel, extra='allow'): """ report = None self.__dict__.update(self.model_extra) - logger.debug(f"Here is the incoming sample dict: \n{self.__dict__}") + # logger.debug(f"Here is the incoming sample dict: \n{self.__dict__}") instance = BasicSample.query_or_create(sample_type=self.sample_type, submitter_id=self.submitter_id) for key, value in self.__dict__.items(): match key: @@ -246,8 +246,8 @@ class PydSample(BaseModel, extra='allow'): if submission is not None: 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)") + # logger.debug(f"Looking up association with identity: ({submission.submission_type_name} Association)") + # logger.debug(f"Looking up association with identity: ({assoc_type} Association)") association = SubmissionSampleAssociation.query_or_create(association_type=f"{assoc_type} Association", submission=submission, sample=instance, @@ -357,7 +357,7 @@ class PydSubmission(BaseModel, extra='allow'): @field_validator('equipment', mode='before') @classmethod def convert_equipment_dict(cls, value): - logger.debug(f"Equipment: {value}") + # logger.debug(f"Equipment: {value}") if isinstance(value, dict): return value['value'] return value @@ -381,7 +381,7 @@ class PydSubmission(BaseModel, extra='allow'): @field_validator("submitted_date", mode="before") @classmethod def rescue_date(cls, value): - logger.debug(f"\n\nDate coming into pydantic: {value}\n\n") + # logger.debug(f"\n\nDate coming into pydantic: {value}\n\n") try: check = value['value'] == None except TypeError: @@ -426,7 +426,7 @@ class PydSubmission(BaseModel, extra='allow'): @classmethod def lookup_submitting_lab(cls, value): if isinstance(value['value'], str): - logger.debug(f"Looking up organization {value['value']}") + # logger.debug(f"Looking up organization {value['value']}") try: value['value'] = Organization.query(name=value['value']).name except AttributeError: @@ -457,12 +457,12 @@ class PydSubmission(BaseModel, extra='allow'): @field_validator("rsl_plate_num") @classmethod def rsl_from_file(cls, value, values): - logger.debug(f"RSL-plate initial value: {value['value']} and other values: {values.data}") + # logger.debug(f"RSL-plate initial value: {value['value']} and other values: {values.data}") sub_type = values.data['submission_type']['value'] if check_not_nan(value['value']): return value else: - logger.debug("Constructing plate name.") + # logger.debug("Constructing plate name.") output = RSLNamer(filename=values.data['filepath'].__str__(), sub_type=sub_type, data=values.data).parsed_name return dict(value=output, missing=True) @@ -649,32 +649,32 @@ class PydSubmission(BaseModel, extra='allow'): rsl_plate_num=self.rsl_plate_num['value']) result = Result(msg=msg, code=code) self.handle_duplicate_samples() - logger.debug(f"Here's our list of duplicate removed samples: {self.samples}") + # logger.debug(f"Here's our list of duplicate removed samples: {self.samples}") # for key, value in self.__dict__.items(): for key, value in dicto.items(): if isinstance(value, dict): value = value['value'] - logger.debug(f"Setting {key} to {value}") + # logger.debug(f"Setting {key} to {value}") match key: case "reagents": if code == 1: instance.submission_reagent_associations = [] - logger.debug(f"Looking through {self.reagents}") + # logger.debug(f"Looking through {self.reagents}") for reagent in self.reagents: reagent, assoc = reagent.toSQL(submission=instance) - logger.debug(f"Association: {assoc}") + # logger.debug(f"Association: {assoc}") if assoc is not None:# and assoc not in instance.submission_reagent_associations: instance.submission_reagent_associations.append(assoc) # instance.reagents.append(reagent) case "samples": for sample in self.samples: sample, associations, _ = sample.toSQL(submission=instance) - logger.debug(f"Sample SQL object to be added to submission: {sample.__dict__}") + # logger.debug(f"Sample SQL object to be added to submission: {sample.__dict__}") for assoc in associations: if assoc is not None and assoc not in instance.submission_sample_associations: instance.submission_sample_associations.append(assoc) case "equipment": - logger.debug(f"Equipment: {pformat(self.equipment)}") + # logger.debug(f"Equipment: {pformat(self.equipment)}") try: if equip is None: continue @@ -684,11 +684,11 @@ class PydSubmission(BaseModel, extra='allow'): equip, association = equip.toSQL(submission=instance) if association is not None: association.save() - logger.debug( - f"Equipment association SQL object to be added to submission: {association.__dict__}") + # logger.debug( + # f"Equipment association SQL object to be added to submission: {association.__dict__}") instance.submission_equipment_associations.append(association) case item if item in instance.jsons(): - logger.debug(f"{item} is a json.") + # logger.debug(f"{item} is a json.") try: ii = value.items() except AttributeError: @@ -701,38 +701,38 @@ class PydSubmission(BaseModel, extra='allow'): try: instance.set_attribute(key=key, value=value) except AttributeError as e: - logger.debug(f"Could not set attribute: {key} to {value} due to: \n\n {e}") + logger.error(f"Could not set attribute: {key} to {value} due to: \n\n {e}") continue except KeyError: continue try: - logger.debug(f"Calculating costs for procedure...") + # logger.debug(f"Calculating costs for procedure...") instance.calculate_base_cost() except (TypeError, AttributeError) as e: - logger.debug(f"Looks like that kit doesn't have cost breakdown yet due to: {e}, using full plate cost.") + # logger.debug(f"Looks like that kit doesn't have cost breakdown yet due to: {e}, using full plate cost.") try: instance.run_cost = instance.extraction_kit.cost_per_run except AttributeError: 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. try: - logger.debug("Checking and applying discounts...") + # 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}") + # logger.debug(f"We got discounts: {discounts}") if len(discounts) > 0: discounts = sum(discounts) instance.run_cost = instance.run_cost - 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 - logger.debug(f"We've got a total cost of {instance.run_cost}") - try: - logger.debug(f"Constructed instance: {instance}") - except AttributeError as e: - logger.debug(f"Something went wrong constructing instance {self.rsl_plate_num}: {e}") - logger.debug(f"Constructed submissions message: {msg}") + # logger.debug(f"We've got a total cost of {instance.run_cost}") + # try: + # logger.debug(f"Constructed instance: {instance}") + # except AttributeError as e: + # logger.debug(f"Something went wrong constructing instance {self.rsl_plate_num}: {e}") + # logger.debug(f"Constructed submissions message: {msg}") return instance, result def to_form(self, parent: QWidget): @@ -777,26 +777,26 @@ class PydSubmission(BaseModel, extra='allow'): Report: Result object containing a message and any missing components. """ report = Report() - logger.debug(f"Extraction kit: {extraction_kit}. Is it a string? {isinstance(extraction_kit, str)}") + # logger.debug(f"Extraction kit: {extraction_kit}. Is it a string? {isinstance(extraction_kit, str)}") if isinstance(extraction_kit, str): extraction_kit = dict(value=extraction_kit) if extraction_kit is not None and extraction_kit != self.extraction_kit['value']: self.extraction_kit['value'] = extraction_kit['value'] - logger.debug(f"Looking up {self.extraction_kit['value']}") + # logger.debug(f"Looking up {self.extraction_kit['value']}") ext_kit = KitType.query(name=self.extraction_kit['value']) ext_kit_rtypes = [item.to_pydantic() for item in ext_kit.get_reagents(required=True, submission_type=self.submission_type['value'])] - logger.debug(f"Kit reagents: {ext_kit_rtypes}") - logger.debug(f"Submission reagents: {self.reagents}") + # logger.debug(f"Kit reagents: {ext_kit_rtypes}") + # logger.debug(f"Submission reagents: {self.reagents}") # Exclude any reagenttype found in this pyd not expected in kit. expected_check = [item.type for item in ext_kit_rtypes] output_reagents = [rt for rt in self.reagents if rt.type in expected_check] - logger.debug(f"Already have these reagent types: {output_reagents}") + # logger.debug(f"Already have these reagent types: {output_reagents}") missing_check = [item.type for item in output_reagents] missing_reagents = [rt for rt in ext_kit_rtypes if rt.type not in missing_check] missing_reagents += [rt for rt in output_reagents if rt.missing] output_reagents += [rt for rt in missing_reagents if rt not in output_reagents] - logger.debug(f"Missing reagents types: {missing_reagents}") + # logger.debug(f"Missing reagents types: {missing_reagents}") # if lists are equal return no problem if len(missing_reagents) == 0: result = None @@ -873,7 +873,7 @@ class PydReagentType(BaseModel): instance: ReagentType = ReagentType.query(name=self.name) if instance == None: instance = ReagentType(name=self.name, eol_ext=self.eol_ext) - logger.debug(f"This is the reagent type instance: {instance.__dict__}") + # logger.debug(f"This is the reagent type instance: {instance.__dict__}") try: assoc = KitTypeReagentTypeAssociation.query(reagent_type=instance, kit_type=kit) except StatementError: diff --git a/src/submissions/frontend/visualizations/control_charts.py b/src/submissions/frontend/visualizations/control_charts.py index 7be3e05..85b0e73 100644 --- a/src/submissions/frontend/visualizations/control_charts.py +++ b/src/submissions/frontend/visualizations/control_charts.py @@ -48,7 +48,7 @@ def create_charts(ctx:Settings, df:pd.DataFrame, ytitle:str|None=None) -> Figure # Set descending for any columns that have "{mode}" in the header. ascending = [False if item == "target" else True for item in sorts] df = df.sort_values(by=sorts, ascending=ascending) - logger.debug(df[df.isna().any(axis=1)]) + # logger.debug(df[df.isna().any(axis=1)]) # actual chart construction is done by fig = construct_chart(df=df, modes=modes, ytitle=ytitle) return fig diff --git a/src/submissions/frontend/widgets/app.py b/src/submissions/frontend/widgets/app.py index 17118d4..1f4e5a3 100644 --- a/src/submissions/frontend/widgets/app.py +++ b/src/submissions/frontend/widgets/app.py @@ -25,7 +25,7 @@ logger.info("Hello, I am a logger") class App(QMainWindow): def __init__(self, ctx: Settings = None): - logger.debug(f"Initializing main window...") + # logger.debug(f"Initializing main window...") super().__init__() self.ctx = ctx self.last_dir = ctx.directory_path @@ -58,7 +58,7 @@ class App(QMainWindow): """ adds items to menu bar """ - logger.debug(f"Creating menu bar...") + # logger.debug(f"Creating menu bar...") menuBar = self.menuBar() fileMenu = menuBar.addMenu("&File") # Creating menus using a title @@ -79,7 +79,7 @@ class App(QMainWindow): """ adds items to toolbar """ - logger.debug(f"Creating toolbar...") + # logger.debug(f"Creating toolbar...") toolbar = QToolBar("My main toolbar") self.addToolBar(toolbar) toolbar.addAction(self.addReagentAction) @@ -90,7 +90,7 @@ class App(QMainWindow): """ creates actions """ - logger.debug(f"Creating actions...") + # logger.debug(f"Creating actions...") self.importAction = QAction("&Import Submission", self) # self.importPCRAction = QAction("&Import PCR Results", self) self.addReagentAction = QAction("Add Reagent", self) @@ -107,7 +107,7 @@ class App(QMainWindow): """ connect menu and tool bar item to functions """ - logger.debug(f"Connecting actions...") + # 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) @@ -134,7 +134,7 @@ class App(QMainWindow): url = Path(sys._MEIPASS).joinpath("files", "docs", "index.html") else: url = Path("docs\\build\\index.html").absolute() - logger.debug(f"Attempting to open {url}") + # logger.debug(f"Attempting to open {url}") webbrowser.get('windows-default').open(f"file://{url.__str__()}") def result_reporter(self): @@ -144,11 +144,11 @@ class App(QMainWindow): Args: result (dict | None, optional): The result from a function. Defaults to None. """ - logger.debug(f"Running results reporter for: {self.report.results}") + # logger.debug(f"Running results reporter for: {self.report.results}") if len(self.report.results) > 0: - logger.debug(f"We've got some results!") + # logger.debug(f"We've got some results!") for result in self.report.results: - logger.debug(f"Showing result: {result}") + # logger.debug(f"Showing result: {result}") if result != None: alert = result.report() if alert.exec(): @@ -164,17 +164,17 @@ class App(QMainWindow): def backup_database(self): 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}") + # 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") if not current_month_bak.exists() and "demo" not in self.ctx.database_path.__str__(): - logger.debug("No backup found for this month, backing up database.") + logger.info("No backup found for this month, backing up database.") shutil.copyfile(self.ctx.database_path, current_month_bak) class AddSubForm(QWidget): def __init__(self, parent:QWidget): - logger.debug(f"Initializating subform...") + # logger.debug(f"Initializating subform...") super(QWidget, self).__init__(parent) self.layout = QVBoxLayout(self) # Initialize tab screen diff --git a/src/submissions/frontend/widgets/controls_chart.py b/src/submissions/frontend/widgets/controls_chart.py index 9f4d0c3..a73bd55 100644 --- a/src/submissions/frontend/widgets/controls_chart.py +++ b/src/submissions/frontend/widgets/controls_chart.py @@ -112,13 +112,13 @@ class ControlsViewer(QWidget): Tuple[QMainWindow, dict]: Collection of new main app window and result dict """ report = Report() - logger.debug(f"Control getter context: \n\tControl type: {self.con_type}\n\tMode: {self.mode}\n\tStart Date: {self.start_date}\n\tEnd Date: {self.end_date}") + # logger.debug(f"Control getter context: \n\tControl type: {self.con_type}\n\tMode: {self.mode}\n\tStart Date: {self.start_date}\n\tEnd Date: {self.end_date}") # set the subtype for kraken if self.sub_typer.currentText() == "": self.subtype = None else: 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 controls = Control.query(control_type=self.con_type, start_date=self.start_date, end_date=self.end_date) # if no data found from query set fig to none for reporting in webview @@ -129,7 +129,7 @@ class ControlsViewer(QWidget): data = [control.convert_by_mode(mode=self.mode) for control in controls] # flatten data to one dimensional list data = [item for sublist in data for item in sublist] - logger.debug(f"Control objects going into df conversion: {type(data)}") + # logger.debug(f"Control objects going into df conversion: {type(data)}") if data == []: self.report.add_result(Result(status="Critical", msg="No data found for controls in given date range.")) return @@ -141,13 +141,13 @@ class ControlsViewer(QWidget): title = f"{self.mode} - {self.subtype}" # send dataframe to chart maker fig = create_charts(ctx=self.app.ctx, df=df, ytitle=title) - logger.debug(f"Updating figure...") + # logger.debug(f"Updating figure...") # construct html for webview html = construct_html(figure=fig) - logger.debug(f"The length of html code is: {len(html)}") + # logger.debug(f"The length of html code is: {len(html)}") self.webengineview.setHtml(html) self.webengineview.update() - logger.debug("Figure updated... I hope.") + # logger.debug("Figure updated... I hope.") self.report.add_result(report) class ControlsDatePicker(QWidget): diff --git a/src/submissions/frontend/widgets/equipment_usage.py b/src/submissions/frontend/widgets/equipment_usage.py index 4d66edc..d793d4e 100644 --- a/src/submissions/frontend/widgets/equipment_usage.py +++ b/src/submissions/frontend/widgets/equipment_usage.py @@ -17,9 +17,9 @@ class EquipmentUsage(QDialog): self.setWindowTitle("Equipment Checklist") self.used_equipment = self.submission.get_used_equipment() self.kit = self.submission.extraction_kit - logger.debug(f"Existing equipment: {self.used_equipment}") + # logger.debug(f"Existing equipment: {self.used_equipment}") self.opt_equipment = submission.submission_type.get_equipment() - logger.debug(f"EquipmentRoles: {self.opt_equipment}") + # logger.debug(f"EquipmentRoles: {self.opt_equipment}") self.layout = QVBoxLayout() self.setLayout(self.layout) self.populate_form() @@ -115,9 +115,9 @@ class RoleComboBox(QWidget): Changes processes when equipment is changed """ equip = self.box.currentText() - logger.debug(f"Updating equipment: {equip}") + # logger.debug(f"Updating equipment: {equip}") equip2 = [item for item in self.role.equipment if item.name==equip][0] - logger.debug(f"Using: {equip2}") + # logger.debug(f"Using: {equip2}") self.process.clear() self.process.addItems([item for item in equip2.processes if item in self.role.processes]) diff --git a/src/submissions/frontend/widgets/gel_checker.py b/src/submissions/frontend/widgets/gel_checker.py index ccf180d..7c32f2a 100644 --- a/src/submissions/frontend/widgets/gel_checker.py +++ b/src/submissions/frontend/widgets/gel_checker.py @@ -145,5 +145,5 @@ class ControlsForm(QWidget): dicto['values'].append(dict(name=label[1], value=le.text())) if label[0] not in [item['name'] for item in output]: output.append(dicto) - logger.debug(pformat(output)) + # logger.debug(pformat(output)) return output, self.comment_field.toPlainText() diff --git a/src/submissions/frontend/widgets/kit_creator.py b/src/submissions/frontend/widgets/kit_creator.py index 98a0ad4..8f43444 100644 --- a/src/submissions/frontend/widgets/kit_creator.py +++ b/src/submissions/frontend/widgets/kit_creator.py @@ -93,10 +93,10 @@ class KitAdder(QWidget): # 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)}") + # logger.debug(f"kit info: {pformat(info)}") + # logger.debug(f"kit reagents: {pformat(reagents)}") info['reagent_types'] = reagents - logger.debug(pformat(info)) + # logger.debug(pformat(info)) # send to kit constructor kit = PydKit(name=info['kit_name']) for reagent in info['reagent_types']: @@ -108,7 +108,7 @@ class KitAdder(QWidget): 'expiry':reagent['expiry'] }} kit.reagent_types.append(PydReagentType(name=reagent['rtname'], eol_ext=reagent['eol'], uses=uses)) - logger.debug(f"Output pyd object: {kit.__dict__}") + # logger.debug(f"Output pyd object: {kit.__dict__}") sqlobj, result = kit.toSQL(self.ctx) report.add_result(result=result) sqlobj.save() @@ -122,7 +122,7 @@ class KitAdder(QWidget): Returns: Tuple[dict, list]: dict=info, list=reagents """ - logger.debug(f"Hello from {self.__class__} parser!") + # logger.debug(f"Hello from {self.__class__} parser!") info = {} reagents = [] widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore and not isinstance(widget.parent(), ReagentTypeForm)] @@ -153,7 +153,7 @@ class ReagentTypeForm(QWidget): self.reagent_getter.setObjectName("rtname") # lookup all reagent type names from db lookup = ReagentType.query() - logger.debug(f"Looked up ReagentType names: {lookup}") + # logger.debug(f"Looked up ReagentType names: {lookup}") self.reagent_getter.addItems([item.name for item in lookup]) self.reagent_getter.setEditable(True) grid.addWidget(self.reagent_getter,0,1) @@ -205,14 +205,14 @@ class ReagentTypeForm(QWidget): Returns: dict: _description_ """ - logger.debug(f"Hello from {self.__class__} parser!") + # logger.debug(f"Hello from {self.__class__} parser!") info = {} info['eol'] = self.eol.value() info['sheet'] = self.location_sheet_name.text() info['rtname'] = self.reagent_getter.currentText() widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore] for widget in widgets: - logger.debug(f"Parsed widget: {widget.objectName()} of type {type(widget)} with parent {widget.parent()}") + # logger.debug(f"Parsed widget: {widget.objectName()} of type {type(widget)} with parent {widget.parent()}") match widget: case QLineEdit(): info[widget.objectName()] = widget.text() @@ -225,7 +225,7 @@ class ReagentTypeForm(QWidget): key, sub_key = widget.objectName().split("_") if key not in info.keys(): info[key] = {} - logger.debug(f"Adding key {key}, {sub_key} and value {widget.value()} to {info}") + # logger.debug(f"Adding key {key}, {sub_key} and value {widget.value()} to {info}") info[key][sub_key] = widget.value() return info diff --git a/src/submissions/frontend/widgets/misc.py b/src/submissions/frontend/widgets/misc.py index ddd4362..3d6ae79 100644 --- a/src/submissions/frontend/widgets/misc.py +++ b/src/submissions/frontend/widgets/misc.py @@ -58,7 +58,7 @@ class AddReagentForm(QDialog): self.type_input = QComboBox() self.type_input.setObjectName('type') self.type_input.addItems([item.name for item in ReagentType.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? try: reagent_type = reagent_type.replace("_", " ").title() @@ -97,7 +97,7 @@ class AddReagentForm(QDialog): """ Updates reagent names form field with examples from reagent type """ - logger.debug(self.type_input.currentText()) + # logger.debug(self.type_input.currentText()) self.name_input.clear() lookup = Reagent.query(reagent_type=self.type_input.currentText()) self.name_input.addItems(list(set([item.name for item in lookup]))) @@ -210,7 +210,7 @@ class LogParser(QDialog): """ count: int = 0 total: int = 0 - logger.debug(f"Current search term: {self.phrase_looker.currentText()}") + # logger.debug(f"Current search term: {self.phrase_looker.currentText()}") try: with open(self.fname, "r") as f: for chunk in readInChunks(fileObj=f): diff --git a/src/submissions/frontend/widgets/submission_details.py b/src/submissions/frontend/widgets/submission_details.py index 6b0ccea..c708ea2 100644 --- a/src/submissions/frontend/widgets/submission_details.py +++ b/src/submissions/frontend/widgets/submission_details.py @@ -1,4 +1,4 @@ -from PyQt6.QtWidgets import (QDialog, QScrollArea, QPushButton, QVBoxLayout, QMessageBox, +from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout, QMessageBox, QDialogButtonBox, QTextEdit) from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebChannel import QWebChannel @@ -10,7 +10,6 @@ from .functions import select_save_file from io import BytesIO from tempfile import TemporaryFile, TemporaryDirectory from pathlib import Path -# from xhtml2pdf import pisa import logging, base64 from getpass import getuser from datetime import datetime @@ -33,24 +32,19 @@ class SubmissionDetails(QDialog): self.app = parent.parent().parent().parent().parent().parent().parent() except AttributeError: self.app = None - # self.setWindowTitle(f"Submission Details - {sub.rsl_plate_num}") - # create scrollable interior - interior = QScrollArea() - interior.setParent(self) self.webview = QWebEngineView(parent=self) self.webview.setMinimumSize(900, 500) self.webview.setMaximumSize(900, 500) - # self.webview.setHtml(self.html) self.layout = QVBoxLayout() - interior.resize(900, 500) - interior.setWidget(self.webview) self.setFixedSize(900, 500) - # button to export a pdf version + # NOTE: button to export a pdf version btn = QPushButton("Export PDF") - btn.setParent(self) - btn.setFixedWidth(900) + btn.setFixedWidth(875) btn.clicked.connect(self.export) - # setup channel + self.layout.addWidget(btn) + self.layout.addWidget(self.webview) + self.setLayout(self.layout) + # NOTE: setup channel self.channel = QWebChannel() self.channel.registerObject('backend', self) self.submission_details(submission=sub) @@ -80,31 +74,25 @@ class SubmissionDetails(QDialog): Args: submission (str | BasicSubmission): Submission of interest. """ - logger.debug(f"Details for: {submission}") + # logger.debug(f"Details for: {submission}") if isinstance(submission, str): - # submission = BasicSubmission.query(rsl_number=submission) submission = BasicSubmission.query(rsl_plate_num=submission) self.base_dict = submission.to_dict(full_data=True) - logger.debug(f"Submission details data:\n{pformat({k:v for k,v in self.base_dict.items() if k != 'samples'})}") - # don't want id + # logger.debug(f"Submission details data:\n{pformat({k:v for k,v in self.base_dict.items() if k != 'samples'})}") + # NOTE: don't want id del self.base_dict['id'] # logger.debug(f"Creating barcode.") - # if not check_if_app(): - # self.base_dict['barcode'] = base64.b64encode(submission.make_plate_barcode(width=120, height=30)).decode('utf-8') - logger.debug(f"Making platemap...") + # logger.debug(f"Making platemap...") self.base_dict['platemap'] = submission.make_plate_map() self.base_dict, self.template = submission.get_details_template(base_dict=self.base_dict) self.html = self.template.render(sub=self.base_dict, signing_permission=is_power_user()) self.webview.setHtml(self.html) self.setWindowTitle(f"Submission Details - {submission.rsl_plate_num}") - # with open("details.html", "w") as f: - # f.write(self.html) @pyqtSlot(str) def sign_off(self, submission:str|BasicSubmission): - logger.debug(f"Signing off on {submission} - ({getuser()})") + # logger.debug(f"Signing off on {submission} - ({getuser()})") if isinstance(submission, str): - # submission = BasicSubmission.query(rsl_number=submission) submission = BasicSubmission.query(rsl_plate_num=submission) submission.signed_by = getuser() submission.save() @@ -177,6 +165,6 @@ class SubmissionComment(QDialog): comment = self.txt_editor.toPlainText() dt = datetime.strftime(datetime.now(), "%Y-%m-%d %H:%M:%S") full_comment = {"name":commenter, "time": dt, "text": comment} - logger.debug(f"Full comment: {full_comment}") + # logger.debug(f"Full comment: {full_comment}") return full_comment diff --git a/src/submissions/frontend/widgets/submission_table.py b/src/submissions/frontend/widgets/submission_table.py index 0c45b6e..084f989 100644 --- a/src/submissions/frontend/widgets/submission_table.py +++ b/src/submissions/frontend/widgets/submission_table.py @@ -110,7 +110,7 @@ class SubmissionsSheet(QTableView): self.menu = QMenu(self) self.con_actions = submission.custom_context_events() for k in self.con_actions.keys(): - logger.debug(f"Adding {k}") + # logger.debug(f"Adding {k}") action = QAction(k, self) action.triggered.connect(lambda _, action_name=k: self.triggered_action(action_name=action_name)) self.menu.addAction(action) @@ -124,8 +124,8 @@ class SubmissionsSheet(QTableView): Args: action_name (str): name of the action from the menu """ - logger.debug(f"Action: {action_name}") - logger.debug(f"Responding with {self.con_actions[action_name]}") + # logger.debug(f"Action: {action_name}") + # logger.debug(f"Responding with {self.con_actions[action_name]}") func = self.con_actions[action_name] func(obj=self) @@ -162,50 +162,20 @@ class SubmissionsSheet(QTableView): experiment_name=run[4].strip(), end_time=run[5].strip() ) - # elution columns are item 6 in the comma split list to the end + # NOTE: elution columns are item 6 in the comma split list to the end for ii in range(6, len(run)): new_run[f"column{str(ii-5)}_vol"] = run[ii] - # Lookup imported submissions - # sub = BasicSubmission.query(rsl_number=new_run['rsl_plate_num']) + # NOTE: Lookup imported submissions sub = BasicSubmission.query(rsl_plate_num=new_run['rsl_plate_num']) - # If no such submission exists, move onto the next run + # NOTE: If no such submission exists, move onto the next run if sub == None: continue try: - logger.debug(f"Found submission: {sub.rsl_plate_num}") + # logger.debug(f"Found submission: {sub.rsl_plate_num}") count += 1 except AttributeError: continue sub.set_attribute('extraction_info', new_run) - # if sub.extraction_info != None: - # # existing = json.loads(sub.extraction_info) - # existing = sub.extraction_info - # else: - # existing = None - # # Check if the new info already exists in the imported submission - # try: - # # if json.dumps(new_run) in sub.extraction_info: - # if new_run in sub.extraction_info: - # logger.debug(f"Looks like we already have that info.") - # continue - # except TypeError: - # pass - # # Update or create the extraction info - # if existing != None: - # try: - # logger.debug(f"Updating {type(existing)}: {existing} with {type(new_run)}: {new_run}") - # existing.append(new_run) - # logger.debug(f"Setting: {existing}") - # # sub.extraction_info = json.dumps(existing) - # sub.extraction_info = existing - # except TypeError: - # logger.error(f"Error updating!") - # # sub.extraction_info = json.dumps([new_run]) - # sub.extraction_info = [new_run] - # logger.debug(f"Final ext info for {sub.rsl_plate_num}: {sub.extraction_info}") - # else: - # # sub.extraction_info = json.dumps([new_run]) - # sub.extraction_info = [new_run] sub.save() self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information')) @@ -230,7 +200,7 @@ class SubmissionsSheet(QTableView): """ fname = select_open_file(self, file_extension="csv") with open(fname.__str__(), 'r') as f: - # split csv rows on comma + # NOTE: split csv rows on comma runs = [col.strip().split(",") for col in f.readlines()] count = 0 for run in runs: @@ -242,49 +212,17 @@ class SubmissionsSheet(QTableView): experiment_name=run[4].strip(), end_time=run[5].strip() ) - # lookup imported submission - # sub = lookup_submission_by_rsl_num(ctx=obj.ctx, rsl_num=new_run['rsl_plate_num']) - # sub = lookup_submissions(ctx=obj.ctx, rsl_number=new_run['rsl_plate_num']) + # NOTE: lookup imported submission sub = BasicSubmission.query(rsl_number=new_run['rsl_plate_num']) - # if imported submission doesn't exist move on to next run + # NOTE: if imported submission doesn't exist move on to next run if sub == None: continue - try: - logger.debug(f"Found submission: {sub.rsl_plate_num}") - except AttributeError: - continue - sub.set_attribute('pcr_info', new_run) - # # check if pcr_info already exists - # if hasattr(sub, 'pcr_info') and sub.pcr_info != None: - # # existing = json.loads(sub.pcr_info) - # existing = sub.pcr_info - # else: - # existing = None - # # check if this entry already exists in imported submission # try: - # # if json.dumps(new_run) in sub.pcr_info: - # if new_run in sub.pcr_info: - # logger.debug(f"Looks like we already have that info.") - # continue - # else: - # count += 1 - # except TypeError: - # logger.error(f"No json to dump") - # if existing is not None: - # try: - # logger.debug(f"Updating {type(existing)}: {existing} with {type(new_run)}: {new_run}") - # existing.append(new_run) - # logger.debug(f"Setting: {existing}") - # # sub.pcr_info = json.dumps(existing) - # sub.pcr_info = existing - # except TypeError: - # logger.error(f"Error updating!") - # # sub.pcr_info = json.dumps([new_run]) - # sub.pcr_info = [new_run] - # logger.debug(f"Final ext info for {sub.rsl_plate_num}: {sub.pcr_info}") - # else: - # # sub.pcr_info = json.dumps([new_run]) - # sub.pcr_info = [new_run] + # logger.debug(f"Found submission: {sub.rsl_plate_num}") + # except AttributeError: + # continue + sub.set_attribute('pcr_info', new_run) + # NOTE: check if pcr_info already exists sub.save() self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information')) @@ -308,23 +246,21 @@ class SubmissionsSheet(QTableView): Tuple[QMainWindow, dict]: Collection of new main app window and result dict """ report = Report() - # ask for date ranges + # NOTE: ask for date ranges dlg = ReportDatePicker() if dlg.exec(): info = dlg.parse_form() - logger.debug(f"Report info: {info}") - # find submissions based on date range + # logger.debug(f"Report info: {info}") + # NOTE: find submissions based on date range subs = BasicSubmission.query(start_date=info['start_date'], end_date=info['end_date']) - # convert each object to dict + # NOTE: convert each object to dict records = [item.to_dict(report=True) for item in subs] logger.debug(f"Records: {pformat(records)}") - # make dataframe from record dictionaries + # NOTE: make dataframe from record dictionaries detailed_df, summary_df = make_report_xlsx(records=records) html = make_report_html(df=summary_df, start_date=info['start_date'], end_date=info['end_date']) - # 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']}.pdf", extension="pdf") - # with open(fname, "w+b") as f: - # pisa.CreatePDF(html, dest=f) 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") @@ -337,14 +273,13 @@ class SubmissionsSheet(QTableView): len(str(series.name)) # len of column name/header )) + 20 # adding a little extra space try: - # worksheet.column_dimensions[get_column_letter(idx=idx)].width = max_len - # Convert idx to letter + # 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}") + # 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)})") diff --git a/src/submissions/frontend/widgets/submission_type_creator.py b/src/submissions/frontend/widgets/submission_type_creator.py index faecf42..4c10b18 100644 --- a/src/submissions/frontend/widgets/submission_type_creator.py +++ b/src/submissions/frontend/widgets/submission_type_creator.py @@ -26,11 +26,11 @@ class SubmissionTypeAdder(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("Submission Type Name:"),2,0) - # widget to get kit name + # NOTE: widget to get kit name self.st_name = QLineEdit() self.st_name.setObjectName("submission_type_name") self.grid.addWidget(self.st_name,2,1,1,2) @@ -39,7 +39,7 @@ class SubmissionTypeAdder(QWidget): self.grid.addWidget(template_selector,3,1) self.template_label = QLabel("None") self.grid.addWidget(self.template_label,3,2) - # widget to get uses of kit + # NOTE: widget to get uses of kit exclude = ['id', 'submitting_lab_id', 'extraction_kit_id', 'reagents_id', 'extraction_info', 'pcr_info', 'run_cost'] self.columns = {key:value for key, value in BasicSubmission.__dict__.items() if isinstance(value, InstrumentedAttribute)} self.columns = {key:value for key, value in self.columns.items() if hasattr(value, "type") and key not in exclude} diff --git a/src/submissions/frontend/widgets/submission_widget.py b/src/submissions/frontend/widgets/submission_widget.py index e0a0431..ef77964 100644 --- a/src/submissions/frontend/widgets/submission_widget.py +++ b/src/submissions/frontend/widgets/submission_widget.py @@ -30,7 +30,7 @@ class SubmissionFormContainer(QWidget): import_drag = pyqtSignal(Path) def __init__(self, parent: QWidget) -> None: - logger.debug(f"Setting form widget...") + # logger.debug(f"Setting form widget...") super().__init__(parent) self.app = self.parent().parent() self.report = Report() @@ -52,7 +52,7 @@ class SubmissionFormContainer(QWidget): Sets filename when file dropped """ fname = Path([u.toLocalFile() for u in event.mimeData().urls()][0]) - logger.debug(f"App: {self.app}") + # logger.debug(f"App: {self.app}") self.app.last_dir = fname.parent self.import_drag.emit(fname) @@ -63,7 +63,7 @@ class SubmissionFormContainer(QWidget): self.app.raise_() self.app.activateWindow() self.import_submission_function(fname) - logger.debug(f"Result from result reporter: {self.report.results}") + # logger.debug(f"Result from result reporter: {self.report.results}") self.app.report.add_result(self.report) self.report = Report() self.app.result_reporter() @@ -78,7 +78,7 @@ class SubmissionFormContainer(QWidget): Returns: Tuple[QMainWindow, dict|None]: Collection of new main app window and result dict """ - logger.debug(f"\n\nStarting Import...\n\n") + logger.info(f"\n\nStarting Import...\n\n") report = Report() try: self.form.setParent(None) @@ -90,7 +90,7 @@ class SubmissionFormContainer(QWidget): # set file dialog if isinstance(fname, bool) or fname == None: fname = select_open_file(self, file_extension="xlsx") - logger.debug(f"Attempting to parse file: {fname}") + # logger.debug(f"Attempting to parse file: {fname}") if not fname.exists(): report.add_result(Result(msg=f"File {fname.__str__()} not found.", status="critical")) self.report.add_result(report) @@ -103,16 +103,16 @@ class SubmissionFormContainer(QWidget): return except AttributeError: self.prsr = SheetParser(filepath=fname) - logger.debug(f"Submission dictionary:\n{pformat(self.prsr.sub)}") + # logger.debug(f"Submission dictionary:\n{pformat(self.prsr.sub)}") self.pyd = self.prsr.to_pydantic() - logger.debug(f"Pydantic result: \n\n{pformat(self.pyd)}\n\n") + # logger.debug(f"Pydantic result: \n\n{pformat(self.pyd)}\n\n") self.form = self.pyd.to_form(parent=self) self.layout().addWidget(self.form) # if self.prsr.sample_result != None: # report.add_result(msg=self.prsr.sample_result, status="Warning") self.report.add_result(report) - logger.debug(f"Outgoing report: {self.report.results}") - logger.debug(f"All attributes of submission container:\n{pformat(self.__dict__)}") + # logger.debug(f"Outgoing report: {self.report.results}") + # logger.debug(f"All attributes of submission container:\n{pformat(self.__dict__)}") def add_reagent(self, reagent_lot:str|None=None, reagent_type:str|None=None, expiry:date|None=None, name:str|None=None): """ @@ -135,7 +135,7 @@ class SubmissionFormContainer(QWidget): if dlg.exec(): # extract form info info = dlg.parse_form() - logger.debug(f"Reagent info: {info}") + # logger.debug(f"Reagent info: {info}") # create reagent object reagent = PydReagent(ctx=self.app.ctx, **info, missing=False) # send reagent to db @@ -216,10 +216,10 @@ class SubmissionFormWidget(QWidget): caller = inspect.stack()[1].function.__repr__().replace("'", "") # self.reagents = [] # logger.debug(f"Self.reagents: {self.reagents}") - logger.debug(f"\n\n{pformat(caller)}\n\n") + # logger.debug(f"\n\n{pformat(caller)}\n\n") # logger.debug(f"SubmissionType: {self.submission_type}") report = Report() - logger.debug(f"Extraction kit: {extraction_kit}") + # logger.debug(f"Extraction kit: {extraction_kit}") # Remove previous reagent widgets try: old_reagents = self.find_widgets() @@ -249,7 +249,7 @@ class SubmissionFormWidget(QWidget): add_widget = self.ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.pyd.extraction_kit) self.layout.addWidget(add_widget) report.add_result(integrity_report) - logger.debug(f"Outgoing report: {report.results}") + # logger.debug(f"Outgoing report: {report.results}") if hasattr(self.pyd, "csv"): export_csv_btn = QPushButton("Export CSV") export_csv_btn.setObjectName("export_csv_btn") @@ -263,46 +263,6 @@ class SubmissionFormWidget(QWidget): self.app.report.add_result(report) self.app.result_reporter() - # def kit_integrity_completion_function(self, extraction_kit:str|None=None): - # """ - # Compare kit contents to parsed contents and creates widgets. - # - # Args: - # obj (QMainWindow): The original app window - # - # Returns: - # Tuple[QMainWindow, dict]: Collection of new main app window and result dict - # """ - # report = Report() - # missing_reagents = [] - # # logger.debug(inspect.currentframe().f_back.f_code.co_name) - # # find the widget that contains kit info - # if extraction_kit is None: - # kit_widget = self.find_widgets(object_name="extraction_kit")[0].input - # logger.debug(f"Kit selector: {kit_widget}") - # # get current kit being used - # self.ext_kit = kit_widget.currentText() - # else: - # self.ext_kit = extraction_kit - # for reagent in self.reagents: - # logger.debug(f"Creating widget for {reagent}") - # add_widget = self.ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.ext_kit) - # # self.form.layout().addWidget(add_widget) - # self.layout.addWidget(add_widget) - # if reagent.missing: - # missing_reagents.append(reagent) - # logger.debug(f"Checking integrity of {self.ext_kit}") - # # TODO: put check_kit_integrity here instead of what's here? - # # see if there are any missing reagents - # if len(missing_reagents) > 0: - # result = Result(msg=f"""The submission you are importing is missing some reagents expected by the kit.\n\n - # It looks like you are missing: {[item.type.upper() for item in missing_reagents]}\n\n - # Alternatively, you may have set the wrong extraction kit.\n\nThe program will populate lists using existing reagents. - # \n\nPlease make sure you check the lots carefully!""".replace(" ", ""), status="Warning") - # report.add_result(result) - # self.report.add_result(report) - # logger.debug(f"Outgoing report: {self.report.results}") - def clear_form(self): """ Removes all form widgets @@ -335,24 +295,23 @@ class SubmissionFormWidget(QWidget): Returns: Tuple[QMainWindow, dict]: Collection of new main app window and result dict """ - logger.debug(f"\n\nBeginning Submission\n\n") + logger.info(f"\n\nBeginning Submission\n\n") report = Report() - # self.pyd: PydSubmission = self.parse_form() result = self.parse_form() report.add_result(result) - logger.debug(f"Submission: {pformat(self.pyd)}") - logger.debug("Checking kit integrity...") + # logger.debug(f"Submission: {pformat(self.pyd)}") + # logger.debug("Checking kit integrity...") _, result = self.pyd.check_kit_integrity() report.add_result(result) if len(result.results) > 0: self.app.report.add_result(report) self.app.result_reporter() return - logger.debug(f"PYD before transformation into SQL:\n\n{self.pyd}\n\n") + # logger.debug(f"PYD before transformation into SQL:\n\n{self.pyd}\n\n") base_submission, result = self.pyd.to_sql() - logger.debug(f"SQL object: {pformat(base_submission.__dict__)}") + # logger.debug(f"SQL object: {pformat(base_submission.__dict__)}") # logger.debug(f"Base submission: {base_submission.to_dict()}") - # check output message for issues + # NOTE: check output message for issues match result.code: # code 0: everything is fine. case 0: @@ -379,11 +338,10 @@ class SubmissionFormWidget(QWidget): # logger.debug(f"Updating: {reagent} with {reagent.lot}") reagent.update_last_used(kit=base_submission.extraction_kit) # logger.debug(f"Final reagents: {pformat(base_submission.reagents)}") - # sys.exit("Programmed stop submission_widget.py, line 381") base_submission.save() - # update summary sheet + # NOTE: update summary sheet self.app.table_widget.sub_wid.setData() - # reset form + # NOTE: reset form self.setParent(None) # logger.debug(f"All attributes of obj: {pformat(self.__dict__)}") self.app.report.add_result(report) @@ -396,16 +354,14 @@ class SubmissionFormWidget(QWidget): Args: fname (Path | None, optional): Input filename. Defaults to None. """ - # self.parse_form() if isinstance(fname, bool) or fname == None: fname = select_save_file(obj=self, default_name=self.pyd.construct_filename(), extension="csv") try: - # logger.debug(f'') # self.pyd.csv.to_csv(fname.__str__(), index=False) workbook_2_csv(worksheet=self.pyd.csv, filename=fname) except PermissionError: - logger.debug(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: logger.error(f"No csv file found in the submission at this point.") @@ -417,7 +373,7 @@ class SubmissionFormWidget(QWidget): Report: Report on status of parse. """ report = Report() - logger.debug(f"Hello from form parser!") + logger.info(f"Hello from form parser!") info = {} reagents = [] for widget in self.findChildren(QWidget): @@ -431,21 +387,19 @@ class SubmissionFormWidget(QWidget): field, value = widget.parse_form() if field is not None: info[field] = value - logger.debug(f"Info: {pformat(info)}") - logger.debug(f"Reagents going into pyd: {pformat(reagents)}") + # logger.debug(f"Info: {pformat(info)}") + # logger.debug(f"Reagents going into pyd: {pformat(reagents)}") self.pyd.reagents = reagents - # logger.debug(f"Attrs not in info: {[k for k, v in self.__dict__.items() if k not in info.keys()]}") for item in self.recover: - logger.debug(f"Attempting to recover: {item}") + # logger.debug(f"Attempting to recover: {item}") if hasattr(self, item): value = getattr(self, item) - logger.debug(f"Setting {item}") + # logger.debug(f"Setting {item}") info[item] = value - # submission = PydSubmission(reagents=reagents, **info) for k,v in info.items(): self.pyd.set_attribute(key=k, value=v) - # return submission + # NOTE: return submission report.add_result(report) return report @@ -510,7 +464,7 @@ class SubmissionFormWidget(QWidget): except (TypeError, KeyError): pass obj = parent.parent().parent() - logger.debug(f"Creating widget for: {key}") + # logger.debug(f"Creating widget for: {key}") match key: case 'submitting_lab': add_widget = QComboBox() @@ -531,12 +485,12 @@ class SubmissionFormWidget(QWidget): # create combobox to hold looked up kits add_widget = QComboBox() # lookup existing kits by 'submission_type' decided on by sheetparser - logger.debug(f"Looking up kits used for {submission_type}") + # logger.debug(f"Looking up kits used for {submission_type}") uses = [item.name for item in KitType.query(used_for=submission_type)] obj.uses = uses - logger.debug(f"Kits received for {submission_type}: {uses}") + # logger.debug(f"Kits received for {submission_type}: {uses}") if check_not_nan(value): - logger.debug(f"The extraction kit in parser was: {value}") + # logger.debug(f"The extraction kit in parser was: {value}") uses.insert(0, uses.pop(uses.index(value))) obj.ext_kit = value else: @@ -565,7 +519,7 @@ class SubmissionFormWidget(QWidget): case _: # anything else gets added in as a line edit add_widget = QLineEdit() - logger.debug(f"Setting widget text to {str(value).replace('_', ' ')}") + # logger.debug(f"Setting widget text to {str(value).replace('_', ' ')}") add_widget.setText(str(value).replace("_", " ")) if add_widget != None: add_widget.setObjectName(key) @@ -639,14 +593,6 @@ class SubmissionFormWidget(QWidget): # If changed set self.missing to True and update self.label self.lot.currentTextChanged.connect(self.updated) - # def check_uncheck(self): - # if self.check_box.isChecked(): - # self.lot.setCurrentIndex(0) - # self.lot.setEnabled(True) - # else: - # self.lot.setCurrentText("Not Applicable") - # self.lot.setEnabled(False) - def parse_form(self) -> Tuple[PydReagent, dict]: """ Pulls form info into PydReagent @@ -657,17 +603,17 @@ class SubmissionFormWidget(QWidget): # if not self.check_box.isChecked(): # return None, None lot = self.lot.currentText() - logger.debug(f"Using this lot for the reagent {self.reagent}: {lot}") + # logger.debug(f"Using this lot for the reagent {self.reagent}: {lot}") wanted_reagent = Reagent.query(lot_number=lot, reagent_type=self.reagent.type) - # if reagent doesn't exist in database, offer to add it (uses App.add_reagent) + # NOTE: if reagent doesn't exist in database, offer to add it (uses App.add_reagent) if wanted_reagent == None: dlg = QuestionAsker(title=f"Add {lot}?", message=f"Couldn't find reagent type {self.reagent.type}: {lot} in the database.\n\nWould you like to add it?") if dlg.exec(): wanted_reagent = self.parent().parent().add_reagent(reagent_lot=lot, reagent_type=self.reagent.type, expiry=self.reagent.expiry, name=self.reagent.name) return wanted_reagent, None else: - # In this case we will have an empty reagent and the submission will fail kit integrity check - logger.debug("Will not add reagent.") + # NOTE: In this case we will have an empty reagent and the submission will fail kit integrity check + # logger.debug("Will not add reagent.") return None, Result(msg="Failed integrity check", status="Critical") else: # Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name @@ -712,8 +658,8 @@ class SubmissionFormWidget(QWidget): def __init__(self, reagent, extraction_kit:str) -> None: super().__init__() self.setEditable(True) - logger.debug(f"Attempting lookup of reagents by type: {reagent.type}") - # below was lookup_reagent_by_type_name_and_kit_name, but I couldn't get it to work. + # logger.debug(f"Attempting lookup of reagents by type: {reagent.type}") + # NOTE: below was lookup_reagent_by_type_name_and_kit_name, but I couldn't get it to work. lookup = Reagent.query(reagent_type=reagent.type) relevant_reagents = [str(item.lot) for item in lookup] output_reg = [] @@ -725,8 +671,8 @@ class SubmissionFormWidget(QWidget): elif isinstance(rel_reagent, str): output_reg.append(rel_reagent) relevant_reagents = output_reg - # if reagent in sheet is not found insert it into the front of relevant reagents so it shows - logger.debug(f"Relevant reagents for {reagent.lot}: {relevant_reagents}") + # NOTE: if reagent in sheet is not found insert it into the front of relevant reagents so it shows + # logger.debug(f"Relevant reagents for {reagent.lot}: {relevant_reagents}") if str(reagent.lot) not in relevant_reagents: if check_not_nan(reagent.lot): relevant_reagents.insert(0, str(reagent.lot)) diff --git a/src/submissions/templates/basicsubmission_details.html b/src/submissions/templates/basicsubmission_details.html index ceae5bf..1275457 100644 --- a/src/submissions/templates/basicsubmission_details.html +++ b/src/submissions/templates/basicsubmission_details.html @@ -27,12 +27,12 @@ left: 50%; margin-left: -60px; } - /* Show the tooltip text when you mouse over the tooltip container */ .tooltip:hover .tooltiptext { visibility: visible; font-size: large; } + Submission Details for {{ sub['Plate Number'] }} diff --git a/src/submissions/tools.py b/src/submissions/tools.py index d774daf..0415cab 100644 --- a/src/submissions/tools.py +++ b/src/submissions/tools.py @@ -40,8 +40,9 @@ main_aux_dir = Path.home().joinpath(f"{os_config_dir}/submissions") CONFIGDIR = main_aux_dir.joinpath("config") LOGDIR = main_aux_dir.joinpath("logs") -row_map = {1:"A", 2:"B", 3:"C", 4:"D", 5:"E", 6:"F", 7:"G", 8:"H"} -row_keys = {v:k for k,v in row_map.items()} +row_map = {1: "A", 2: "B", 3: "C", 4: "D", 5: "E", 6: "F", 7: "G", 8: "H"} +row_keys = {v: k for k, v in row_map.items()} + def check_not_nan(cell_contents) -> bool: """ @@ -52,7 +53,7 @@ def check_not_nan(cell_contents) -> bool: Returns: bool: True if cell has value, else, false. - """ + """ # check for nan as a string first exclude = ['unnamed:', 'blank', 'void'] try: @@ -88,7 +89,8 @@ def check_not_nan(cell_contents) -> bool: logger.debug(f"Check encountered unknown error: {type(e).__name__} - {e}") return False -def convert_nans_to_nones(input_str) -> str|None: + +def convert_nans_to_nones(input_str) -> str | None: """ Get rid of various "nan", "NAN", "NaN", etc/ @@ -97,19 +99,21 @@ def convert_nans_to_nones(input_str) -> str|None: Returns: str: _description_ - """ + """ # logger.debug(f"Input value of: {input_str}") if check_not_nan(input_str): return input_str return None -def is_missing(value:Any) -> Tuple[Any, bool]: + +def is_missing(value: Any) -> Tuple[Any, bool]: if check_not_nan(value): return value, False else: return convert_nans_to_nones(value), True -def check_regex_match(pattern:str, check:str) -> bool: + +def check_regex_match(pattern: str, check: str) -> bool: """ Determines if a pattern matches a str @@ -119,13 +123,14 @@ def check_regex_match(pattern:str, check:str) -> bool: Returns: bool: match found? - """ + """ try: return bool(re.match(fr"{pattern}", check)) except TypeError: return False -def get_first_blank_df_row(df:pd.DataFrame) -> int: + +def get_first_blank_df_row(df: pd.DataFrame) -> int: """ For some reason I need a whole function for this. @@ -134,9 +139,10 @@ def get_first_blank_df_row(df:pd.DataFrame) -> int: Returns: int: Index of the row after the last used row. - """ + """ return df.shape[0] + 1 + # Settings class Settings(BaseSettings, extra="allow"): @@ -146,16 +152,16 @@ class Settings(BaseSettings, extra="allow"): Raises: FileNotFoundError: Error if database not found. - """ + """ directory_path: Path - database_path: Path|str|None = None - backup_path: Path|str|None = None + database_path: Path | str | None = None + backup_path: Path | str | None = None # super_users: list|None = None # power_users: list|None = None # rerun_regex: str - submission_types: dict|None = None - database_session: Session|None = None - package: Any|None = None + submission_types: dict | None = None + database_session: Session | None = None + package: Any | None = None model_config = SettingsConfigDict(env_file_encoding='utf-8') @@ -178,10 +184,10 @@ class Settings(BaseSettings, extra="allow"): if isinstance(value, str): value = Path(value) if not value.exists(): - value = Path().home() - # metadata.directory_path = value + value = Path().home() + # metadata.directory_path = value return value - + @field_validator('database_path', mode="before") @classmethod def ensure_database_exists(cls, value, values): @@ -196,7 +202,7 @@ class Settings(BaseSettings, extra="allow"): return value else: raise FileNotFoundError(f"Couldn't find database at {value}") - + @field_validator('database_session', mode="before") @classmethod def create_database_session(cls, value, values): @@ -223,7 +229,7 @@ class Settings(BaseSettings, extra="allow"): else: raise FileNotFoundError("No database file found. Exiting program.") logger.debug(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) # metadata.session = session return session @@ -240,19 +246,20 @@ class Settings(BaseSettings, extra="allow"): super().__init__(*args, **kwargs) self.set_from_db(db_path=kwargs['database_path']) - def set_from_db(self, db_path:Path): + def set_from_db(self, db_path: Path): if 'pytest' in sys.modules: config_items = dict(power_users=['lwark', 'styson', 'ruwang']) else: session = Session(create_engine(f"sqlite:///{db_path}")) config_items = session.execute(text("SELECT * FROM _configitem")).all() session.close() - config_items = {item[1]:json.loads(item[2]) for item in config_items} + config_items = {item[1]: json.loads(item[2]) for item in config_items} for k, v in config_items.items(): if not hasattr(self, k): self.__setattr__(k, v) -def get_config(settings_path: Path|str|None=None) -> Settings: + +def get_config(settings_path: Path | str | None = None) -> Settings: """ Get configuration settings from path or default if blank. @@ -262,36 +269,38 @@ def get_config(settings_path: Path|str|None=None) -> Settings: Returns: Settings: Pydantic settings object - """ - logger.debug(f"Creating settings...") + """ + # logger.debug(f"Creating settings...") if isinstance(settings_path, str): settings_path = Path(settings_path) + # custom pyyaml constructor to join fields def join(loader, node): seq = loader.construct_sequence(node) return ''.join([str(i) for i in seq]) + # register the tag handler yaml.add_constructor('!join', join) - logger.debug(f"Making directory: {CONFIGDIR.__str__()}") + # make directories try: CONFIGDIR.mkdir(parents=True) except FileExistsError: - pass - logger.debug(f"Making directory: {LOGDIR.__str__()}") + logger.warning(f"Config directory {CONFIGDIR} already exists.") + try: LOGDIR.mkdir(parents=True) except FileExistsError: - pass - # if user hasn't defined config path in cli args + logger.warning(f"Logging directory {LOGDIR} already exists.") + # NOTE: if user hasn't defined config path in cli args if settings_path == None: - # Check user .config/submissions directory + # NOTE: Check user .config/submissions directory if CONFIGDIR.joinpath("config.yml").exists(): settings_path = CONFIGDIR.joinpath("config.yml") - # Check user .submissions directory + # NOTE: Check user .submissions directory elif Path.home().joinpath(".submissions", "config.yml").exists(): settings_path = Path.home().joinpath(".submissions", "config.yml") - # finally look in the local config + # NOTE: finally look in the local config else: if check_if_app(): settings_path = Path(sys._MEIPASS).joinpath("files", "config.yml") @@ -299,14 +308,14 @@ def get_config(settings_path: Path|str|None=None) -> Settings: settings_path = package_dir.joinpath('config.yml') with open(settings_path, "r") as dset: default_settings = yaml.load(dset, Loader=yaml.Loader) - # Tell program we need to copy the config.yml to the user directory - # copy settings to config directory + # NOTE: Tell program we need to copy the config.yml to the user directory + # NOTE: copy settings to config directory return Settings(**copy_settings(settings_path=CONFIGDIR.joinpath("config.yml"), settings=default_settings)) else: - # check if user defined path is directory + # NOTE: check if user defined path is directory if settings_path.is_dir(): settings_path = settings_path.joinpath("config.yml") - # check if user defined path is file + # NOTE: check if user defined path is file elif settings_path.is_file(): settings_path = settings_path else: @@ -314,11 +323,12 @@ def get_config(settings_path: Path|str|None=None) -> Settings: with open(settings_path, "r") as dset: default_settings = yaml.load(dset, Loader=yaml.Loader) return Settings(**copy_settings(settings_path=settings_path, settings=default_settings)) - logger.debug(f"Using {settings_path} for config file.") + # logger.debug(f"Using {settings_path} for config file.") with open(settings_path, "r") as stream: settings = yaml.load(stream, Loader=yaml.Loader) return Settings(**settings) + # Logging formatters class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler): @@ -334,13 +344,13 @@ class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler): os.chmod(self.baseFilename, currMode | stat.S_IWGRP) def _open(self): - prevumask=os.umask(0o002) - rtv=handlers.RotatingFileHandler._open(self) + prevumask = os.umask(0o002) + rtv = handlers.RotatingFileHandler._open(self) os.umask(prevumask) return rtv -class CustomFormatter(logging.Formatter): +class CustomFormatter(logging.Formatter): class bcolors: HEADER = '\033[95m' OKBLUE = '\033[94m' @@ -367,6 +377,7 @@ class CustomFormatter(logging.Formatter): formatter = logging.Formatter(log_fmt) return formatter.format(record) + class StreamToLogger(object): """ Fake file-like stream object that redirects writes to a logger instance. @@ -381,31 +392,33 @@ class StreamToLogger(object): for line in buf.rstrip().splitlines(): self.logger.log(self.log_level, line.rstrip()) -def setup_logger(verbosity:int=3): + +def setup_logger(verbosity: int = 3): """ Set logger levels using settings. Args: - verbosit (int, optional): Level of verbosity desired 3 is highest. Defaults to 3. + verbosity (int, optional): Level of verbosity desired 3 is highest. Defaults to 3. Returns: logger: logger object """ logger = logging.getLogger("submissions") logger.setLevel(logging.DEBUG) - # create file handler which logs even debug messages + # NOTE: create file handler which logs even debug messages try: Path(LOGDIR).mkdir(parents=True) except FileExistsError: - pass + logger.warning(f"Logging directory {LOGDIR} already exists.") + # NOTE: logging to file turned off due to repeated permission errors # fh = GroupWriteRotatingFileHandler(LOGDIR.joinpath('submissions.log'), mode='a', maxBytes=100000, backupCount=3, encoding=None, delay=False) # file logging will always be debug # fh.setLevel(logging.DEBUG) # fh.name = "File" - # create console handler with a higher log level - # create custom logger with STERR -> log + # NOTE: create console handler with a higher log level + # NOTE: create custom logger with STERR -> log ch = logging.StreamHandler(stream=sys.stdout) - # set looging level based on verbosity + # NOTE: set looging level based on verbosity match verbosity: case 3: ch.setLevel(logging.DEBUG) @@ -414,24 +427,26 @@ def setup_logger(verbosity:int=3): case 1: ch.setLevel(logging.WARNING) ch.name = "Stream" - # create formatter and add it to the handlers - # formatter = logging.Formatter('%(asctime)s - %(levelname)s - {%(pathname)s:%(lineno)d} - %(message)s') + # NOTE: create formatter and add it to the handlers formatter = CustomFormatter() # fh.setFormatter(formatter) ch.setFormatter(formatter) - # add the handlers to the logger + # NOTE: add the handlers to the logger # logger.addHandler(fh) logger.addHandler(ch) - # Output exception and traceback to logger + + # NOTE: Output exception and traceback to logger def handle_exception(exc_type, exc_value, exc_traceback): if issubclass(exc_type, KeyboardInterrupt): sys.__excepthook__(exc_type, exc_value, exc_traceback) return logger.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)) + sys.excepthook = handle_exception return logger -def copy_settings(settings_path:Path, settings:dict) -> dict: + +def copy_settings(settings_path: Path, settings: dict) -> dict: """ copies relevant settings dictionary from the default config.yml to a new directory @@ -441,8 +456,8 @@ def copy_settings(settings_path:Path, settings:dict) -> dict: Returns: dict: output dictionary for use in first run - """ - # if the current user is not a superuser remove the superusers entry + """ + # NOTE: if the current user is not a superuser remove the superusers entry if not getpass.getuser() in settings['super_users']: del settings['super_users'] if not getpass.getuser() in settings['power_users']: @@ -452,40 +467,40 @@ def copy_settings(settings_path:Path, settings:dict) -> dict: yaml.dump(settings, f) return settings + def jinja_template_loading() -> Environment: """ Returns jinja2 template environment. Returns: _type_: _description_ - """ - # determine if pyinstaller launcher is being used + """ + # NOTE: determine if pyinstaller launcher is being used if check_if_app(): loader_path = Path(sys._MEIPASS).joinpath("files", "templates") else: - loader_path = Path(__file__).parent.joinpath('templates').absolute()#.__str__() - # jinja template loading + loader_path = Path(__file__).parent.joinpath('templates').absolute() #.__str__() + # NOTE: jinja template loading loader = FileSystemLoader(loader_path) env = Environment(loader=loader) env.globals['STATIC_PREFIX'] = loader_path.joinpath("static", "css") return env + def check_if_app() -> bool: """ Checks if the program is running from pyinstaller compiled - Args: - ctx (dict, optional): Settings passed down from gui. Defaults to None. - Returns: bool: True if running from pyinstaller. Else False. - """ + """ if getattr(sys, 'frozen', False): return True else: return False - -def convert_well_to_row_column(input_str:str) -> Tuple[int, int]: + + +def convert_well_to_row_column(input_str: str) -> Tuple[int, int]: """ Converts typical alphanumeric (i.e. "A2") to row, column @@ -494,22 +509,24 @@ def convert_well_to_row_column(input_str:str) -> Tuple[int, int]: Returns: Tuple[int, int]: row, column - """ - row_keys = {v:k for k,v in row_map.items()} + """ + row_keys = {v: k for k, v in row_map.items()} try: row = int(row_keys[input_str[0].upper()]) - column = int(input_str[1:]) + column = int(input_str[1:]) except IndexError: return None, None return row, column + def setup_lookup(func): """ Checks to make sure all args are allowed Args: - func (_type_): _description_ - """ + func (_type_): wrapped function + """ + def wrapper(*args, **kwargs): sanitized_kwargs = {} for k, v in locals()['kwargs'].items(): @@ -521,20 +538,23 @@ def setup_lookup(func): elif v is not None: sanitized_kwargs[k] = v return func(*args, **sanitized_kwargs) + return wrapper -class Result(BaseModel): +class Result(BaseModel): owner: str = Field(default="", validate_default=True) code: int = Field(default=0) msg: str status: Literal["NoIcon", "Question", "Information", "Warning", "Critical"] = Field(default="NoIcon") - @field_validator('status', mode='before') @classmethod - def to_title(cls, value:str): - return value.title() + def to_title(cls, value: str): + if value.lower().replace(" ", "") == "noicon": + return "NoIcon" + else: + return value.title() def __repr__(self) -> str: return f"Result({self.owner})" @@ -546,15 +566,15 @@ class Result(BaseModel): def report(self): from frontend.widgets.misc import AlertPop return AlertPop(message=self.msg, status=self.status, owner=self.owner) - -class Report(BaseModel): + +class Report(BaseModel): results: List[Result] = Field(default=[]) def __repr__(self): return f"Report(result_count:{len(self.results)})" - def add_result(self, result:Result|Report|None): + def add_result(self, result: Result | Report | None): match result: case Result(): logger.debug(f"Adding {result} to results.") @@ -568,36 +588,41 @@ class Report(BaseModel): logger.debug(f"Adding {res} from to results.") self.results.append(res) case _: - pass - -def rreplace(s, old, new): - return (s[::-1].replace(old[::-1],new[::-1], 1))[::-1] + logger.error(f"Unknown variable type: {type(result)}") -def html_to_pdf(html, output_file:Path|str): + +def rreplace(s, old, new): + return (s[::-1].replace(old[::-1], new[::-1], 1))[::-1] + + +def html_to_pdf(html, output_file: Path | str): if isinstance(output_file, str): output_file = Path(output_file) - # document = QTextDocument() document = QWebEngineView() - document.setHtml(html) + document.setHtml(html) printer = QPrinter(QPrinter.PrinterMode.HighResolution) printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat) printer.setOutputFileName(output_file.absolute().__str__()) printer.setPageSize(QPageSize(QPageSize.PageSizeId.A4)) document.print(printer) -def remove_key_from_list_of_dicts(input:list, key:str): + +def remove_key_from_list_of_dicts(input: list, key: str): for item in input: del item[key] return input -def workbook_2_csv(worksheet: Worksheet, filename:Path): + +def workbook_2_csv(worksheet: Worksheet, filename: Path): with open(filename, 'w', newline="") as f: c = csv.writer(f) for r in worksheet.rows: c.writerow([cell.value for cell in r]) + ctx = get_config(None) + def is_power_user() -> bool: try: check = getpass.getuser() in ctx.power_users @@ -605,13 +630,14 @@ def is_power_user() -> bool: check = False return check + def check_authorization(func): """ Decorator to check if user is authorized to access function Args: func (_type_): Function to be used. - """ + """ def wrapper(*args, **kwargs): logger.debug(f"Checking authorization") if is_power_user():