From 1463cf9d2d43259f7d9f78f05116b0083fdbff84 Mon Sep 17 00:00:00 2001 From: lwark Date: Wed, 23 Jul 2025 15:23:12 -0500 Subject: [PATCH] Sample results writer improvements. --- src/submissions/backend/db/models/kits.py | 39 ++++++--- .../results_parsers/pcr_results_parser.py | 1 + .../backend/excel/writers/__init__.py | 31 +++---- .../writers/procedure_writers/__init__.py | 6 +- .../excel/writers/results_writers/__init__.py | 1 + .../results_writers/pcr_results_writer.py | 83 +++++++++++++++++++ .../backend/managers/procedures.py | 17 +++- .../managers/results/pcr_results_manager.py | 12 +++ src/submissions/backend/managers/runs.py | 6 +- src/submissions/backend/validators/pydant.py | 4 +- .../frontend/widgets/submission_details.py | 4 +- .../frontend/widgets/submission_table.py | 2 +- 12 files changed, 160 insertions(+), 46 deletions(-) create mode 100644 src/submissions/backend/excel/writers/results_writers/__init__.py create mode 100644 src/submissions/backend/excel/writers/results_writers/pcr_results_writer.py diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index 1b40273..520fc59 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -1558,7 +1558,7 @@ class Procedure(BaseClass): def to_pydantic(self, **kwargs): from backend.validators.pydant import PydResults, PydReagent output = super().to_pydantic() - logger.debug(f"Pydantic output: \n\n{pformat(output.__dict__)}\n\n") + print(f"Pydantic output: \n\n{pformat(output.__dict__)}\n\n") try: output.kittype = dict(value=output.kittype['name'], missing=False) except KeyError: @@ -1580,17 +1580,18 @@ class Procedure(BaseClass): pass # output.reagent = [PydReagent(**item) for item in output.reagent] output.reagent = reagents - - results = [] - for result in output.results: - match result: - case dict(): - results.append(PydResults(**result)) - case PydResults(): - results.append(result) - case _: - pass - output.results = results + # results = [] + # for result in output.results: + # match result: + # case dict(): + # results.append(PydResults(**result)) + # case PydResults(): + # results.append(result) + # case _: + # pass + # output.results = results + output.result = [item.to_pydantic() for item in self.results] + output.sample_results = flatten_list([[result.to_pydantic() for result in item.results] for item in self.proceduresampleassociation]) # for sample in output.sample: # sample.enabled = True return output @@ -3116,6 +3117,13 @@ class Results(BaseClass): sampleprocedureassociation = relationship("ProcedureSampleAssociation", back_populates="results") _img = Column(String(128)) + @property + def sample_id(self): + if self.assoc_id: + return self.sampleprocedureassociation.sample.sample_id + else: + return None + @property def image(self) -> bytes|None: dir = self.__directory_path__.joinpath("submission_imgs.zip") @@ -3131,3 +3139,10 @@ class Results(BaseClass): @image.setter def image(self, value): self._img = value + + def to_pydantic(self, pyd_model_name:str|None=None, **kwargs): + output = super().to_pydantic(pyd_model_name=pyd_model_name, **kwargs) + if self.sample_id: + output.sample_id = self.sample_id + return output + diff --git a/src/submissions/backend/excel/parsers/results_parsers/pcr_results_parser.py b/src/submissions/backend/excel/parsers/results_parsers/pcr_results_parser.py index 2c59210..53202e2 100644 --- a/src/submissions/backend/excel/parsers/results_parsers/pcr_results_parser.py +++ b/src/submissions/backend/excel/parsers/results_parsers/pcr_results_parser.py @@ -75,6 +75,7 @@ class PCRSampleParser(DefaultTABLEParser): if assoc and not isinstance(assoc, list): output = self._pyd_object(results=list(item.values())[0], parent=assoc) output.result_type = "PCR" + del output.result['result_type'] yield output else: continue diff --git a/src/submissions/backend/excel/writers/__init__.py b/src/submissions/backend/excel/writers/__init__.py index a745a9e..be1a961 100644 --- a/src/submissions/backend/excel/writers/__init__.py +++ b/src/submissions/backend/excel/writers/__init__.py @@ -10,7 +10,7 @@ from openpyxl.workbook.workbook import Workbook from openpyxl.worksheet.worksheet import Worksheet from pandas import DataFrame -from backend.db.models import BaseClass +from backend.db.models import BaseClass, ProcedureType from backend.validators.pydant import PydBaseClass logger = logging.getLogger(f"submissions.{__name__}") @@ -21,10 +21,10 @@ class DefaultWriter(object): def __repr__(self): return f"{self.__class__.__name__}<{self.filepath.stem}>" - def __init__(self, pydant_obj, range_dict: dict | None = None, *args, **kwargs): + def __init__(self, pydant_obj, proceduretype: ProcedureType|None=None, range_dict: dict | None = None, *args, **kwargs): # self.filepath = output_filepath self.pydant_obj = pydant_obj - self.fill_dictionary = pydant_obj.improved_dict() + self.proceduretype = proceduretype if range_dict: self.range_dict = range_dict else: @@ -71,6 +71,10 @@ class DefaultKEYVALUEWriter(DefaultWriter): sheet="Sample List" )] + def __init__(self, pydant_obj, proceduretype: ProcedureType|None=None, range_dict: dict | None = None, *args, **kwargs): + super().__init__(pydant_obj=pydant_obj, proceduretype=proceduretype, range_dict=range_dict, *args, **kwargs) + self.fill_dictionary = self.pydant_obj.improved_dict() + @classmethod def check_location(cls, locations: list, sheet: str): return any([item['sheet'] == sheet for item in locations]) @@ -82,21 +86,6 @@ class DefaultKEYVALUEWriter(DefaultWriter): worksheet = workbook[rng['sheet']] try: for ii, (k, v) in enumerate(self.fill_dictionary.items(), start=rng['start_row']): - # match v: - # case x if issubclass(v.__class__, BaseClass): - # v = v.name - # case x if issubclass(v.__class__, PydBaseClass): - # v = v.name - # case dict(): - # try: - # v = v['value'] - # except ValueError: - # try: - # v = v['name'] - # except ValueError: - # v = v.__str__() - # case _: - # pass try: worksheet.cell(column=rng['key_column'], row=rows[ii], value=self.prettify_key(k)) worksheet.cell(column=rng['value_column'], row=rows[ii], value=self.stringify_value(v)) @@ -127,7 +116,7 @@ class DefaultTABLEWriter(DefaultWriter): from backend import PydSample output_samples = [] for iii in range(1, row_count + 1): - logger.debug(f"Submission rank: {iii}") + # logger.debug(f"Submission rank: {iii}") if isinstance(self.pydant_obj, list): iterator = self.pydant_obj else: @@ -139,8 +128,8 @@ class DefaultTABLEWriter(DefaultWriter): for column in column_names: setattr(sample, column[0], "") sample.submission_rank = iii - logger.debug(f"Appending {sample.sample_id}") - logger.debug(f"Iterator now: {[item.submission_rank for item in iterator]}") + # logger.debug(f"Appending {sample.sample_id}") + # logger.debug(f"Iterator now: {[item.submission_rank for item in iterator]}") output_samples.append(sample) return sorted(output_samples, key=lambda x: x.submission_rank) diff --git a/src/submissions/backend/excel/writers/procedure_writers/__init__.py b/src/submissions/backend/excel/writers/procedure_writers/__init__.py index a224bfa..94d488c 100644 --- a/src/submissions/backend/excel/writers/procedure_writers/__init__.py +++ b/src/submissions/backend/excel/writers/procedure_writers/__init__.py @@ -80,9 +80,9 @@ class ProcedureSampleWriter(DefaultTABLEWriter): list_worksheet[rng['header_row']] if item.value] samples = self.pad_samples_to_length(row_count=row_count, column_names=column_names) # samples = self.pydant_obj - logger.debug(f"Samples: {[item.submission_rank for item in samples]}") + # logger.debug(f"Samples: {[item.submission_rank for item in samples]}") for sample in samples: - logger.debug(f"Writing sample: {sample}") + # logger.debug(f"Writing sample: {sample}") write_row = rng['header_row'] + sample.submission_rank for column in column_names: if column[0].lower() in ["well"]:#, "row", "column"]: @@ -92,6 +92,6 @@ class ProcedureSampleWriter(DefaultTABLEWriter): value = getattr(sample, column[0]) except KeyError: value = "" - logger.debug(f"{column} Writing {value} to row {write_row}, column {write_column}") + # logger.debug(f"{column} Writing {value} to row {write_row}, column {write_column}") list_worksheet.cell(row=write_row, column=write_column, value=value) return workbook diff --git a/src/submissions/backend/excel/writers/results_writers/__init__.py b/src/submissions/backend/excel/writers/results_writers/__init__.py new file mode 100644 index 0000000..0a0d0e7 --- /dev/null +++ b/src/submissions/backend/excel/writers/results_writers/__init__.py @@ -0,0 +1 @@ +from .pcr_results_writer import PCRInfoWriter, PCRSampleWriter \ No newline at end of file diff --git a/src/submissions/backend/excel/writers/results_writers/pcr_results_writer.py b/src/submissions/backend/excel/writers/results_writers/pcr_results_writer.py new file mode 100644 index 0000000..6ce3857 --- /dev/null +++ b/src/submissions/backend/excel/writers/results_writers/pcr_results_writer.py @@ -0,0 +1,83 @@ +import logging +from pathlib import Path +from typing import Generator + +from openpyxl import Workbook +from openpyxl.styles import Alignment + +from backend.excel.writers import DefaultKEYVALUEWriter, DefaultTABLEWriter +from tools import flatten_list + +logger = logging.getLogger(f"submissions.{__name__}") + +class PCRInfoWriter(DefaultKEYVALUEWriter): + + default_range_dict = [dict( + start_row=1, + end_row=24, + key_column=1, + value_column=2, + sheet="Results" + )] + + def write_to_workbook(self, workbook: Workbook) -> Workbook: + worksheet = workbook[f"{self.proceduretype.name} Results"] + for key, value in self.fill_dictionary['result'].items(): + logger.debug(f"Filling in {key} with {value}") + worksheet.cell(value['location']['row'], value['location']['key_column'], value=key.replace("_", " ").title()) + worksheet.cell(value['location']['row'], value['location']['value_column'], value=value['value']) + return workbook + + +class PCRSampleWriter(DefaultTABLEWriter): + + def write_to_workbook(self, workbook: Workbook) -> Workbook: + worksheet = workbook[f"{self.proceduretype.name} Results"] + header_row = self.proceduretype.allowed_result_methods['PCR']['sample']['header_row'] + proto_columns = [(1, "sample"), (2, "target")] + columns = [] + for iii, header in enumerate(self.column_headers, start=3): + worksheet.cell(row=header_row, column=iii, value=header.replace("_", " ").title()) + columns.append((iii, header)) + columns = sorted(columns, key=lambda x: x[0]) + columns = proto_columns + columns + logger.debug(columns) + all_results = flatten_list([[item for item in self.rearrange_results(result)] for result in self.pydant_obj]) + if len(all_results) > 0 : + worksheet.cell(row=header_row, column=1, value="Sample") + worksheet.cell(row=header_row, column=2, value="Target") + for iii, item in enumerate(all_results, start=1): + row = header_row + iii + for k, v in item.items(): + column = next((col[0] for col in columns if col[1]==k), None) + cell = worksheet.cell(row=row, column=column) + cell.value = v + cell.alignment = Alignment(horizontal='left') + return workbook + + @classmethod + def rearrange_results(cls, result) -> Generator[dict, None, None]: + for target, values in result.result.items(): + values['target'] = target + values['sample'] = result.sample_id + yield values + + @property + def column_headers(self): + output = [] + for item in self.pydant_obj: + logger.debug(item) + dicto: dict = item.result + for value in dicto.values(): + for key in value.keys(): + output.append(key) + return sorted(list(set(output))) + + + + + + + + + diff --git a/src/submissions/backend/managers/procedures.py b/src/submissions/backend/managers/procedures.py index ac9ded0..e68776d 100644 --- a/src/submissions/backend/managers/procedures.py +++ b/src/submissions/backend/managers/procedures.py @@ -1,15 +1,16 @@ from __future__ import annotations import logging from io import BytesIO +from pprint import pformat from openpyxl.reader.excel import load_workbook from openpyxl.workbook import Workbook -from backend.managers import DefaultManager +from backend.managers import DefaultManager, results from typing import TYPE_CHECKING from pathlib import Path from backend.excel.parsers import procedure_parsers -from backend.excel.writers import procedure_writers +from backend.excel.writers import procedure_writers, results_writers if TYPE_CHECKING: from backend.db.models import ProcedureType @@ -81,5 +82,15 @@ class DefaultProcedureManager(DefaultManager): sample_writer = procedure_writers.ProcedureSampleWriter self.sample_writer = sample_writer(pydant_obj=self.pyd, range_dict=self.proceduretype.sample_map) workbook = self.sample_writer.write_to_workbook(workbook) + logger.debug(self.pyd.result) + # TODO: Find way to group results by result_type. + for result in self.pyd.result: + Writer = getattr(results_writers, f"{result.result_type}InfoWriter") + res_info_writer = Writer(pydant_obj=result, proceduretype=self.proceduretype) + workbook = res_info_writer.write_to_workbook(workbook=workbook) + # sample_results = [sample.result for sample in self.pyd.sample] + logger.debug(pformat(self.pyd.sample_results)) + Writer = getattr(results_writers, "PCRSampleWriter") + res_sample_writer = Writer(pydant_obj=self.pyd.sample_results, proceduretype=self.proceduretype) + workbook = res_sample_writer.write_to_workbook(workbook=workbook) return workbook - diff --git a/src/submissions/backend/managers/results/pcr_results_manager.py b/src/submissions/backend/managers/results/pcr_results_manager.py index 07ed53e..ee42cca 100644 --- a/src/submissions/backend/managers/results/pcr_results_manager.py +++ b/src/submissions/backend/managers/results/pcr_results_manager.py @@ -3,10 +3,15 @@ """ from __future__ import annotations import logging +from io import BytesIO from pathlib import Path from typing import Tuple, List, TYPE_CHECKING + +from openpyxl.reader.excel import load_workbook + from backend.db.models import Procedure from backend.excel.parsers.results_parsers.pcr_results_parser import PCRSampleParser, PCRInfoParser +from backend.excel.writers.results_writers.pcr_results_writer import PCRInfoWriter, PCRSampleWriter from . import DefaultResultsManager if TYPE_CHECKING: from backend.validators.pydant import PydResults @@ -23,6 +28,13 @@ class PCRManager(DefaultResultsManager): self.info_parser = PCRInfoParser(filepath=self.fname, procedure=self.procedure) self.sample_parser = PCRSampleParser(filepath=self.fname, procedure=self.procedure) + def write(self): + workbook = load_workbook(BytesIO(self.procedure.proceduretype.template_file)) + self.info_writer = PCRInfoWriter(pydant_obj=self.procedure.to_pydantic(), proceduretype=self.procedure.proceduretype) + workbook = self.info_writer.write_to_workbook(workbook) + self.sample_writer = PCRSampleWriter(pydant_obj=self.procedure.to_pydantic(), proceduretype=self.procedure.proceduretype) + + diff --git a/src/submissions/backend/managers/runs.py b/src/submissions/backend/managers/runs.py index 96ca701..631c4c9 100644 --- a/src/submissions/backend/managers/runs.py +++ b/src/submissions/backend/managers/runs.py @@ -1,6 +1,8 @@ from __future__ import annotations import logging from pathlib import Path +from pprint import pformat + from openpyxl import load_workbook from openpyxl.workbook.workbook import Workbook from tools import copy_xl_sheet @@ -18,8 +20,8 @@ class DefaultRunManager(DefaultManager): clientsubmission = DefaultClientSubmissionManager(parent=self.parent, input_object=self.pyd.clientsubmission, submissiontype=self.pyd.clientsubmission.submissiontype) workbook = clientsubmission.write() for procedure in self.pyd.procedure: - logger.debug(f"Running procedure: {procedure}") - procedure = DefaultProcedureManager(proceduretype=procedure.proceduretype.name, parent=self.parent, input_object=procedure) + logger.debug(f"Running procedure: {pformat(procedure.__dict__)}") + procedure = DefaultProcedureManager(proceduretype=procedure.proceduretype, parent=self.parent, input_object=procedure) wb: Workbook = procedure.write() for sheetname in wb.sheetnames: source_sheet = wb[sheetname] diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 8284710..adb714d 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -1309,7 +1309,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): reagentrole: dict | None = Field(default={}, validate_default=True) sample: List[PydSample] = Field(default=[]) equipment: List[PydEquipment] = Field(default=[]) - results: List[PydResults] | List[dict] = Field(default=[]) + result: List[PydResults] | List[dict] = Field(default=[]) @field_validator("name", "technician", "kittype", mode="before") @classmethod @@ -1749,7 +1749,7 @@ class PydClientSubmission(PydBaseClass): class PydResults(PydBaseClass, arbitrary_types_allowed=True): - results: dict = Field(default={}) + result: dict = Field(default={}) result_type: str = Field(default="NA") img: None | bytes = Field(default=None) parent: Procedure | ProcedureSampleAssociation | None = Field(default=None) diff --git a/src/submissions/frontend/widgets/submission_details.py b/src/submissions/frontend/widgets/submission_details.py index f40b4b3..81af0d4 100644 --- a/src/submissions/frontend/widgets/submission_details.py +++ b/src/submissions/frontend/widgets/submission_details.py @@ -26,9 +26,9 @@ class SubmissionDetails(QDialog): a window showing text details of procedure """ - def __init__(self, parent, sub: Run | Sample | Reagent) -> None: + def __init__(self, parent, sub: Run | Sample | Reagent, **kwargs) -> None: - super().__init__(parent) + super().__init__(parent, **kwargs) self.app = get_application_from_parent(parent) self.webview = QWebEngineView(parent=self) self.webview.setMinimumSize(900, 500) diff --git a/src/submissions/frontend/widgets/submission_table.py b/src/submissions/frontend/widgets/submission_table.py index 376e1bb..d21e64b 100644 --- a/src/submissions/frontend/widgets/submission_table.py +++ b/src/submissions/frontend/widgets/submission_table.py @@ -424,7 +424,7 @@ class SubmissionsTree(QTreeView): # Run.query(id=id).show_details(self) obj = dicto['item_type'].query(name=dicto['query_str'], limit=1) logger.debug(obj) - obj.show_details(obj) + obj.show_details(self) def link_extractions(self): pass