Sample results writer improvements.

This commit is contained in:
lwark
2025-07-23 15:23:12 -05:00
parent b9ca9586ec
commit 1463cf9d2d
12 changed files with 160 additions and 46 deletions

View File

@@ -1558,7 +1558,7 @@ class Procedure(BaseClass):
def to_pydantic(self, **kwargs): def to_pydantic(self, **kwargs):
from backend.validators.pydant import PydResults, PydReagent from backend.validators.pydant import PydResults, PydReagent
output = super().to_pydantic() 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: try:
output.kittype = dict(value=output.kittype['name'], missing=False) output.kittype = dict(value=output.kittype['name'], missing=False)
except KeyError: except KeyError:
@@ -1580,17 +1580,18 @@ class Procedure(BaseClass):
pass pass
# output.reagent = [PydReagent(**item) for item in output.reagent] # output.reagent = [PydReagent(**item) for item in output.reagent]
output.reagent = reagents output.reagent = reagents
# results = []
results = [] # for result in output.results:
for result in output.results: # match result:
match result: # case dict():
case dict(): # results.append(PydResults(**result))
results.append(PydResults(**result)) # case PydResults():
case PydResults(): # results.append(result)
results.append(result) # case _:
case _: # pass
pass # output.results = results
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: # for sample in output.sample:
# sample.enabled = True # sample.enabled = True
return output return output
@@ -3116,6 +3117,13 @@ class Results(BaseClass):
sampleprocedureassociation = relationship("ProcedureSampleAssociation", back_populates="results") sampleprocedureassociation = relationship("ProcedureSampleAssociation", back_populates="results")
_img = Column(String(128)) _img = Column(String(128))
@property
def sample_id(self):
if self.assoc_id:
return self.sampleprocedureassociation.sample.sample_id
else:
return None
@property @property
def image(self) -> bytes|None: def image(self) -> bytes|None:
dir = self.__directory_path__.joinpath("submission_imgs.zip") dir = self.__directory_path__.joinpath("submission_imgs.zip")
@@ -3131,3 +3139,10 @@ class Results(BaseClass):
@image.setter @image.setter
def image(self, value): def image(self, value):
self._img = 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

View File

@@ -75,6 +75,7 @@ class PCRSampleParser(DefaultTABLEParser):
if assoc and not isinstance(assoc, list): if assoc and not isinstance(assoc, list):
output = self._pyd_object(results=list(item.values())[0], parent=assoc) output = self._pyd_object(results=list(item.values())[0], parent=assoc)
output.result_type = "PCR" output.result_type = "PCR"
del output.result['result_type']
yield output yield output
else: else:
continue continue

View File

@@ -10,7 +10,7 @@ from openpyxl.workbook.workbook import Workbook
from openpyxl.worksheet.worksheet import Worksheet from openpyxl.worksheet.worksheet import Worksheet
from pandas import DataFrame from pandas import DataFrame
from backend.db.models import BaseClass from backend.db.models import BaseClass, ProcedureType
from backend.validators.pydant import PydBaseClass from backend.validators.pydant import PydBaseClass
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -21,10 +21,10 @@ class DefaultWriter(object):
def __repr__(self): def __repr__(self):
return f"{self.__class__.__name__}<{self.filepath.stem}>" 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.filepath = output_filepath
self.pydant_obj = pydant_obj self.pydant_obj = pydant_obj
self.fill_dictionary = pydant_obj.improved_dict() self.proceduretype = proceduretype
if range_dict: if range_dict:
self.range_dict = range_dict self.range_dict = range_dict
else: else:
@@ -71,6 +71,10 @@ class DefaultKEYVALUEWriter(DefaultWriter):
sheet="Sample List" 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 @classmethod
def check_location(cls, locations: list, sheet: str): def check_location(cls, locations: list, sheet: str):
return any([item['sheet'] == sheet for item in locations]) return any([item['sheet'] == sheet for item in locations])
@@ -82,21 +86,6 @@ class DefaultKEYVALUEWriter(DefaultWriter):
worksheet = workbook[rng['sheet']] worksheet = workbook[rng['sheet']]
try: try:
for ii, (k, v) in enumerate(self.fill_dictionary.items(), start=rng['start_row']): 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: try:
worksheet.cell(column=rng['key_column'], row=rows[ii], value=self.prettify_key(k)) 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)) 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 from backend import PydSample
output_samples = [] output_samples = []
for iii in range(1, row_count + 1): 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): if isinstance(self.pydant_obj, list):
iterator = self.pydant_obj iterator = self.pydant_obj
else: else:
@@ -139,8 +128,8 @@ class DefaultTABLEWriter(DefaultWriter):
for column in column_names: for column in column_names:
setattr(sample, column[0], "") setattr(sample, column[0], "")
sample.submission_rank = iii sample.submission_rank = iii
logger.debug(f"Appending {sample.sample_id}") # logger.debug(f"Appending {sample.sample_id}")
logger.debug(f"Iterator now: {[item.submission_rank for item in iterator]}") # logger.debug(f"Iterator now: {[item.submission_rank for item in iterator]}")
output_samples.append(sample) output_samples.append(sample)
return sorted(output_samples, key=lambda x: x.submission_rank) return sorted(output_samples, key=lambda x: x.submission_rank)

View File

@@ -80,9 +80,9 @@ class ProcedureSampleWriter(DefaultTABLEWriter):
list_worksheet[rng['header_row']] if item.value] list_worksheet[rng['header_row']] if item.value]
samples = self.pad_samples_to_length(row_count=row_count, column_names=column_names) samples = self.pad_samples_to_length(row_count=row_count, column_names=column_names)
# samples = self.pydant_obj # 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: for sample in samples:
logger.debug(f"Writing sample: {sample}") # logger.debug(f"Writing sample: {sample}")
write_row = rng['header_row'] + sample.submission_rank write_row = rng['header_row'] + sample.submission_rank
for column in column_names: for column in column_names:
if column[0].lower() in ["well"]:#, "row", "column"]: if column[0].lower() in ["well"]:#, "row", "column"]:
@@ -92,6 +92,6 @@ class ProcedureSampleWriter(DefaultTABLEWriter):
value = getattr(sample, column[0]) value = getattr(sample, column[0])
except KeyError: except KeyError:
value = "" 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) list_worksheet.cell(row=write_row, column=write_column, value=value)
return workbook return workbook

View File

@@ -0,0 +1 @@
from .pcr_results_writer import PCRInfoWriter, PCRSampleWriter

View File

@@ -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)))

View File

@@ -1,15 +1,16 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from io import BytesIO from io import BytesIO
from pprint import pformat
from openpyxl.reader.excel import load_workbook from openpyxl.reader.excel import load_workbook
from openpyxl.workbook import Workbook from openpyxl.workbook import Workbook
from backend.managers import DefaultManager from backend.managers import DefaultManager, results
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from pathlib import Path from pathlib import Path
from backend.excel.parsers import procedure_parsers 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: if TYPE_CHECKING:
from backend.db.models import ProcedureType from backend.db.models import ProcedureType
@@ -81,5 +82,15 @@ class DefaultProcedureManager(DefaultManager):
sample_writer = procedure_writers.ProcedureSampleWriter sample_writer = procedure_writers.ProcedureSampleWriter
self.sample_writer = sample_writer(pydant_obj=self.pyd, range_dict=self.proceduretype.sample_map) self.sample_writer = sample_writer(pydant_obj=self.pyd, range_dict=self.proceduretype.sample_map)
workbook = self.sample_writer.write_to_workbook(workbook) 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 return workbook

View File

@@ -3,10 +3,15 @@
""" """
from __future__ import annotations from __future__ import annotations
import logging import logging
from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Tuple, List, TYPE_CHECKING from typing import Tuple, List, TYPE_CHECKING
from openpyxl.reader.excel import load_workbook
from backend.db.models import Procedure from backend.db.models import Procedure
from backend.excel.parsers.results_parsers.pcr_results_parser import PCRSampleParser, PCRInfoParser 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 from . import DefaultResultsManager
if TYPE_CHECKING: if TYPE_CHECKING:
from backend.validators.pydant import PydResults from backend.validators.pydant import PydResults
@@ -23,6 +28,13 @@ class PCRManager(DefaultResultsManager):
self.info_parser = PCRInfoParser(filepath=self.fname, procedure=self.procedure) self.info_parser = PCRInfoParser(filepath=self.fname, procedure=self.procedure)
self.sample_parser = PCRSampleParser(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)

View File

@@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from pathlib import Path from pathlib import Path
from pprint import pformat
from openpyxl import load_workbook from openpyxl import load_workbook
from openpyxl.workbook.workbook import Workbook from openpyxl.workbook.workbook import Workbook
from tools import copy_xl_sheet 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) clientsubmission = DefaultClientSubmissionManager(parent=self.parent, input_object=self.pyd.clientsubmission, submissiontype=self.pyd.clientsubmission.submissiontype)
workbook = clientsubmission.write() workbook = clientsubmission.write()
for procedure in self.pyd.procedure: for procedure in self.pyd.procedure:
logger.debug(f"Running procedure: {procedure}") logger.debug(f"Running procedure: {pformat(procedure.__dict__)}")
procedure = DefaultProcedureManager(proceduretype=procedure.proceduretype.name, parent=self.parent, input_object=procedure) procedure = DefaultProcedureManager(proceduretype=procedure.proceduretype, parent=self.parent, input_object=procedure)
wb: Workbook = procedure.write() wb: Workbook = procedure.write()
for sheetname in wb.sheetnames: for sheetname in wb.sheetnames:
source_sheet = wb[sheetname] source_sheet = wb[sheetname]

View File

@@ -1309,7 +1309,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
reagentrole: dict | None = Field(default={}, validate_default=True) reagentrole: dict | None = Field(default={}, validate_default=True)
sample: List[PydSample] = Field(default=[]) sample: List[PydSample] = Field(default=[])
equipment: List[PydEquipment] = 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") @field_validator("name", "technician", "kittype", mode="before")
@classmethod @classmethod
@@ -1749,7 +1749,7 @@ class PydClientSubmission(PydBaseClass):
class PydResults(PydBaseClass, arbitrary_types_allowed=True): class PydResults(PydBaseClass, arbitrary_types_allowed=True):
results: dict = Field(default={}) result: dict = Field(default={})
result_type: str = Field(default="NA") result_type: str = Field(default="NA")
img: None | bytes = Field(default=None) img: None | bytes = Field(default=None)
parent: Procedure | ProcedureSampleAssociation | None = Field(default=None) parent: Procedure | ProcedureSampleAssociation | None = Field(default=None)

View File

@@ -26,9 +26,9 @@ class SubmissionDetails(QDialog):
a window showing text details of procedure 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.app = get_application_from_parent(parent)
self.webview = QWebEngineView(parent=self) self.webview = QWebEngineView(parent=self)
self.webview.setMinimumSize(900, 500) self.webview.setMinimumSize(900, 500)

View File

@@ -424,7 +424,7 @@ class SubmissionsTree(QTreeView):
# Run.query(id=id).show_details(self) # Run.query(id=id).show_details(self)
obj = dicto['item_type'].query(name=dicto['query_str'], limit=1) obj = dicto['item_type'].query(name=dicto['query_str'], limit=1)
logger.debug(obj) logger.debug(obj)
obj.show_details(obj) obj.show_details(self)
def link_extractions(self): def link_extractions(self):
pass pass