diff --git a/src/scripts/import_irida.py b/src/scripts/import_irida.py index 6cd7924..b73a63b 100644 --- a/src/scripts/import_irida.py +++ b/src/scripts/import_irida.py @@ -5,7 +5,7 @@ from tools import Settings from sqlalchemy.orm import Session -logger = logging.getLogger(f"procedure.{__name__}") +logger = logging.getLogger(f"submissions.{__name__}") def import_irida(ctx: Settings): """ diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index d05f68b..0ecc093 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -22,7 +22,7 @@ if TYPE_CHECKING: from backend.db.models.submissions import Run, ProcedureSampleAssociation from backend.validators.pydant import PydSample, PydResults -logger = logging.getLogger(f'procedure.{__name__}') +logger = logging.getLogger(f'submissions.{__name__}') reagentrole_reagent = Table( "_reagentrole_reagent", @@ -1097,9 +1097,9 @@ class SubmissionType(BaseClass): str: String from which regex will be compiled. """ # logger.debug(f"Class for regex: {cls}") - logger.debug(f"Looking for {submission_type}") + # logger.debug(f"Looking for {submission_type}") if not isinstance(submission_type, SubmissionType): - submission_type = cls.query(name=submission_type) + submission_type = cls.query(name=submission_type['name']) if isinstance(submission_type, list): if len(submission_type) > 1: regex = "|".join([item.defaults['regex'] for item in submission_type]) @@ -1176,7 +1176,43 @@ class ProcedureType(BaseClass): super().__init__(*args, **kwargs) self.allowed_result_methods = dict() - def construct_field_map(self, field: Literal['equipment', 'tip']) -> Generator[(str, dict), None, None]: + @property + def template_file_sheets(self) -> List[str]: + """ + Gets names of sheet in the stored blank form. + + Returns: + List[str]: List of sheet names + """ + try: + return ExcelFile(BytesIO(self.template_file), engine="openpyxl").sheet_names + except zipfile.BadZipfile: + return [] + + def set_template_file(self, filepath: Path | str): + """ + + Sets the binary store to an Excel file. + + Args: + filepath (Path | str): Path to the template file. + + Raises: + ValueError: Raised if file is not Excel file. + """ + if isinstance(filepath, str): + filepath = Path(filepath) + try: + ExcelFile(filepath) + except ValueError: + raise ValueError(f"File {filepath} is not of appropriate type.") + with open(filepath, "rb") as f: + data = f.read() + self.template_file = data + self.save() + + +def construct_field_map(self, field: Literal['equipment', 'tip']) -> Generator[(str, dict), None, None]: """ Make a map of all locations for tips or equipment. diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 65caa97..2281c08 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -22,6 +22,7 @@ from sqlalchemy.ext.hybrid import hybrid_property from frontend.widgets.functions import select_save_file from . import Base, BaseClass, Reagent, SubmissionType, KitType, ClientLab, Contact, LogMixin, Procedure, \ kittype_procedure + from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, func, Table, Sequence from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.orm.attributes import flag_modified @@ -43,7 +44,7 @@ from PIL import Image if TYPE_CHECKING: from backend.db.models.kits import ProcedureType, Procedure -logger = logging.getLogger(f"procedure.{__name__}") +logger = logging.getLogger(f"submissions.{__name__}") class ClientSubmission(BaseClass, LogMixin): @@ -630,6 +631,10 @@ class Run(BaseClass, LogMixin): output["completed_date"] = self.completed_date return output + @property + def sample_count(self): + return len(self.sample) + def details_dict(self, **kwargs): output = super().details_dict() output['plate_number'] = self.plate_number @@ -651,7 +656,7 @@ class Run(BaseClass, LogMixin): output['permission'] = is_power_user() output['excluded'] = ['procedure', "runsampleassociation", 'excluded', 'expanded', 'sample', 'id', 'custom', 'permission'] - + output['sample_count'] = self.sample_count return output @classmethod @@ -923,37 +928,24 @@ class Run(BaseClass, LogMixin): Returns: PydSubmission: converted object. """ - from backend.validators import PydSubmission - dicto = self.to_dict(full_data=True, backup=backup) + from backend.validators import PydRun + dicto = self.details_dict(full_data=True, backup=backup) new_dict = {} for key, value in dicto.items(): missing = value in ['', 'None', None] match key: - case "reagents": - field_value = [item.to_pydantic(kittype=self.extraction_kit) for item in - self.submission_reagent_associations] case "sample": - field_value = [item.to_pydantic() for item in self.submission_sample_associations] - case "equipment": - field_value = [item.to_pydantic() for item in self.submission_equipment_associations] - case "control": - try: - field_value = [item.to_pydantic() for item in self.__getattribute__(key)] - except TypeError as e: - logger.error(f"Error converting {key} to pydantic :{e}") - continue - case "tips": - field_value = [item.to_pydantic() for item in self.submission_tips_associations] - case "proceduretype": - field_value = dict(value=self.__getattribute__(key).name, missing=missing) + field_value = [item.to_pydantic() for item in self.runsampleassociation] case "plate_number": - key = 'name' + key = 'rsl_plate_number' field_value = dict(value=self.rsl_plate_number, missing=missing) - case "submitter_plate_number": - key = "submitter_plate_id" - field_value = dict(value=self.submitter_plate_number, missing=missing) + new_dict['name'] = field_value case "id": continue + case "clientsubmission": + field_value = self.clientsubmission.to_pydantic() + case "procedure": + field_value = [item.to_pydantic() for item in self.procedure] case _: try: key = key.lower().replace(" ", "_") @@ -967,7 +959,7 @@ class Run(BaseClass, LogMixin): new_dict[key] = field_value new_dict['filepath'] = Path(tempfile.TemporaryFile().name) dicto.update(new_dict) - return PydSubmission(**dicto) + return PydRun(**dicto) def save(self, original: bool = True): """ @@ -1289,14 +1281,12 @@ class Run(BaseClass, LogMixin): self.save(original=False) def export(self, obj, output_filepath: str | Path | None = None): - from backend.excel import writers - clientsubmission_pyd = self.clientsubmission.to_pydantic() + from backend import managers if not output_filepath: output_filepath = select_save_file(obj=obj, default_name=self.construct_filename(), extension="xlsx") - Writer = getattr(writers, "ClientSubmissionWriter") - writer = Writer(output_filepath=output_filepath, pydant_obj=clientsubmission_pyd, - range_dict=self.clientsubmission.range_dict) - workbook: openpyxl.Workbook = writer.write_info() + Manager = getattr(managers, f"Default{self.__class__.__name__}Manager") + manager = Manager(parent=obj, input_object=self.to_pydantic()) + workbook = manager.write() workbook.save(filename=output_filepath) def construct_filename(self): @@ -1970,13 +1960,13 @@ class RunSampleAssociation(BaseClass): # NOTE: Get associated sample info sample = self.sample.to_sub_dict() sample['name'] = self.sample.sample_id - sample['row'] = self.row - sample['column'] = self.column - try: - sample['well'] = f"{row_map[self.row]}{self.column}" - except KeyError as e: - logger.error(f"Unable to find row {self.row} in row_map.") - sample['Well'] = None + # sample['row'] = self.row + # sample['column'] = self.column + # try: + # sample['well'] = f"{row_map[self.row]}{self.column}" + # except KeyError as e: + # logger.error(f"Unable to find row {self.row} in row_map.") + # sample['Well'] = None sample['plate_name'] = self.run.rsl_plate_number sample['positive'] = False return sample @@ -1989,7 +1979,7 @@ class RunSampleAssociation(BaseClass): PydSample: Pydantic Model """ from backend.validators import PydSample - return PydSample(**self.to_sub_dict()) + return PydSample(**self.details_dict()) @property def hitpicked(self) -> dict | None: diff --git a/src/submissions/backend/excel/parser.py b/src/submissions/backend/excel/parser.py index a710166..2562801 100644 --- a/src/submissions/backend/excel/parser.py +++ b/src/submissions/backend/excel/parser.py @@ -9,7 +9,7 @@ from typing import List from openpyxl import load_workbook, Workbook from pathlib import Path from backend.db.models import * -from backend.validators import PydSubmission, RSLNamer +from backend.validators import PydRun, RSLNamer from collections import OrderedDict from tools import check_not_nan, is_missing, check_key_or_attr @@ -45,7 +45,7 @@ class SheetParser(object): self.sub['proceduretype'] = dict(value=RSLNamer.retrieve_submission_type(filename=self.filepath), missing=True) self.submission_type = SubmissionType.query(name=self.sub['proceduretype']) - self.sub_object = BasicRun.find_polymorphic_subclass(polymorphic_identity=self.submission_type) + self.sub_object = Run.find_polymorphic_subclass(polymorphic_identity=self.submission_type) # NOTE: grab the info map from the procedure type in database self.parse_info() self.import_kit_validation_check() @@ -130,14 +130,14 @@ class SheetParser(object): if isinstance(self.sub['kittype'], str): self.sub['kittype'] = dict(value=self.sub['kittype'], missing=True) - def to_pydantic(self) -> PydSubmission: + def to_pydantic(self) -> PydRun: """ Generates a pydantic model of scraped data for validation Returns: PydSubmission: output pydantic model """ - return PydSubmission(filepath=self.filepath, run_custom=True, **self.sub) + return PydRun(filepath=self.filepath, run_custom=True, **self.sub) class InfoParser(object): diff --git a/src/submissions/backend/excel/writer.py b/src/submissions/backend/excel/writer.py index 0263761..15d214d 100644 --- a/src/submissions/backend/excel/writer.py +++ b/src/submissions/backend/excel/writer.py @@ -9,7 +9,7 @@ from pprint import pformat from typing import List, Generator, Tuple from openpyxl import load_workbook, Workbook from backend.db.models import SubmissionType, KitType, Run -from backend.validators.pydant import PydSubmission +from backend.validators.pydant import PydRun from io import BytesIO from collections import OrderedDict @@ -21,7 +21,7 @@ class SheetWriter(object): object to manage data placement into excel file """ - def __init__(self, submission: PydSubmission): + def __init__(self, submission: PydRun): """ Args: submission (PydSubmission): Object containing procedure information. @@ -35,7 +35,7 @@ class SheetWriter(object): case 'proceduretype': self.sub[k] = v['value'] self.submission_type = SubmissionType.query(name=v['value']) - self.run_object = BasicRun.find_polymorphic_subclass( + self.run_object = Run.find_polymorphic_subclass( polymorphic_identity=self.submission_type) case _: if isinstance(v, dict): diff --git a/src/submissions/backend/managers/__init__.py b/src/submissions/backend/managers/__init__.py index 843275d..675315a 100644 --- a/src/submissions/backend/managers/__init__.py +++ b/src/submissions/backend/managers/__init__.py @@ -13,8 +13,9 @@ logger = logging.getLogger(f"submissions.{__name__}") class DefaultManager(object): def __init__(self, parent, input_object: Path | str | None = None): - logger.debug(f"FName before correction: {input_object}") + logger.debug(f"FName before correction: {type(input_object)}") # if input_object != "no_file": + self.parent = parent match input_object: case str(): self.input_object = Path(input_object) @@ -23,15 +24,19 @@ class DefaultManager(object): self.input_object = input_object self.pyd = self.parse() case x if issubclass(input_object.__class__, PydBaseClass): + logger.debug("Subclass of PydBaseClass") self.pyd = input_object case x if issubclass(input_object.__class__, BaseClass): + logger.debug("Subclass of BaseClass") self.pyd = input_object.to_pydantic() case _: self.input_object = select_open_file(file_extension="xlsx", obj=get_application_from_parent(parent)) self.pyd = self.parse() - logger.debug(f"FName after correction: {input_object}") + # logger.debug(f"FName after correction: {input_object}") -from .clientsubmissions import DefaultClientSubmission -from .procedures import DefaultProcedure -from.results import DefaultResults + +from .clientsubmissions import DefaultClientSubmissionManager +from .procedures import DefaultProcedureManager +from .results import DefaultResultsManager +from .runs import DefaultRunManager \ No newline at end of file diff --git a/src/submissions/backend/managers/procedures/__init__.py b/src/submissions/backend/managers/procedures.py similarity index 93% rename from src/submissions/backend/managers/procedures/__init__.py rename to src/submissions/backend/managers/procedures.py index d283ba0..6df90f6 100644 --- a/src/submissions/backend/managers/procedures/__init__.py +++ b/src/submissions/backend/managers/procedures.py @@ -10,13 +10,13 @@ if TYPE_CHECKING: logger = logging.getLogger(f"submissions.{__name__}") -class DefaultProcedure(DefaultManager): +class DefaultProcedureManager(DefaultManager): - def __init__(self, proceduretype: "ProcedureType"|str, parent, fname: Path | str | None = None): - super().__init__(parent=parent, fname=fname) + def __init__(self, proceduretype: "ProcedureType"|str, parent, input_object: Path | str | None = None): if isinstance(proceduretype, str): proceduretype = ProcedureType.query(name=proceduretype) self.proceduretype = proceduretype + super().__init__(parent=parent, input_object=input_object) def parse(self): diff --git a/src/submissions/backend/managers/results/__init__.py b/src/submissions/backend/managers/results/__init__.py index a63b489..d5bf697 100644 --- a/src/submissions/backend/managers/results/__init__.py +++ b/src/submissions/backend/managers/results/__init__.py @@ -7,7 +7,7 @@ from tools import get_application_from_parent logger = logging.getLogger(f"submission.{__name__}") -class DefaultResults(DefaultManager): +class DefaultResultsManager(DefaultManager): def __init__(self, procedure: Procedure, parent, fname: Path | str | None = None): logger.debug(f"FName before correction: {fname}") @@ -18,4 +18,4 @@ class DefaultResults(DefaultManager): self.fname = Path(fname) logger.debug(f"FName after correction: {fname}") -from .pcr_results_manager import PCR +from .pcr_results_manager import PCRManager diff --git a/src/submissions/backend/managers/results/pcr_results_manager.py b/src/submissions/backend/managers/results/pcr_results_manager.py index 2398d2e..1876a6b 100644 --- a/src/submissions/backend/managers/results/pcr_results_manager.py +++ b/src/submissions/backend/managers/results/pcr_results_manager.py @@ -5,11 +5,11 @@ import logging from pathlib import Path from backend.db.models import Procedure from backend.excel.parsers.results_parsers.pcr_results_parser import PCRSampleParser, PCRInfoParser -from . import DefaultResults +from . import DefaultResultsManager logger = logging.getLogger(f"submissions.{__name__}") -class PCR(DefaultResults): +class PCRManager(DefaultResultsManager): def __init__(self, procedure: Procedure, parent, fname:Path|str|None=None): super().__init__(procedure=procedure, parent=parent, fname=fname) diff --git a/src/submissions/backend/validators/__init__.py b/src/submissions/backend/validators/__init__.py index 383b93d..4a470d5 100644 --- a/src/submissions/backend/validators/__init__.py +++ b/src/submissions/backend/validators/__init__.py @@ -92,10 +92,10 @@ class RSLNamer(object): self.submission_type = submission_type if not self.submission_type: self.submission_type = self.retrieve_submission_type(filename=filename) - logger.info(f"got submission type: {self.submission_type}") + # logger.info(f"got submission type: {self.submission_type}") if self.submission_type: # self.sub_object = BasicRun.find_polymorphic_subclass(polymorphic_identity=self.submission_type) - self.sub_object = SubmissionType.query(name=submission_type, limit=1) + self.sub_object = SubmissionType.query(name=self.submission_type['name'], limit=1) self.parsed_name = self.retrieve_rsl_number(filename=filename, regex=self.sub_object.get_regex( submission_type=self.submission_type)) # if not data: @@ -275,5 +275,5 @@ class RSLNamer(object): return "" -from .pydant import PydSubmission, PydKitType, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \ +from .pydant import PydRun, PydKitType, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \ PydEquipment, PydEquipmentRole, PydTips, PydProcess, PydElastic, PydClientSubmission, PydProcedure, PydResults diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 6d9ffd9..45380f6 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -22,11 +22,12 @@ from sqlalchemy.orm.relationships import _RelationshipDeclared from sqlalchemy.orm.attributes import InstrumentedAttribute from PyQt6.QtWidgets import QWidget -logger = logging.getLogger(f"procedure.{__name__}") +logger = logging.getLogger(f"submission.{__name__}") class PydBaseClass(BaseModel, extra='allow', validate_assignment=True): _sql_object: ClassVar = None + # _misc_info: dict|None = None @model_validator(mode="before") @@ -113,7 +114,8 @@ class PydBaseClass(BaseModel, extra='allow', validate_assignment=True): if hasattr(self, "misc_info") and "info_placement" in self.misc_info: for k, v in output.items(): try: - output[k]['location'] = [item['location'] for item in self.misc_info['info_placement'] if item['name'] == k] + output[k]['location'] = [item['location'] for item in self.misc_info['info_placement'] if + item['name'] == k] except (TypeError, KeyError): continue return output @@ -128,9 +130,6 @@ class PydBaseClass(BaseModel, extra='allow', validate_assignment=True): return sql - - - class PydReagent(PydBaseClass): lot: str | None reagentrole: str | None @@ -299,9 +298,19 @@ class PydSample(PydBaseClass): @field_validator("row", mode="before") @classmethod - def str_to_int(cls, value): + def row_str_to_int(cls, value): if isinstance(value, str): - value = row_keys[value] + try: + value = row_keys[value] + except KeyError: + value = 0 + return value + + @field_validator("column", mode="before") + @classmethod + def column_str_to_int(cls, value): + if isinstance(value, str): + value = 0 return value def improved_dict(self, dictionaries: bool = True) -> dict: @@ -316,7 +325,7 @@ class PydSample(PydBaseClass): return sql -class PydTips(BaseModel): +class PydTips(PydBaseClass): name: str lot: str | None = Field(default=None) tiprole: str @@ -354,7 +363,7 @@ class PydTips(BaseModel): return assoc, report -class PydEquipment(BaseModel, extra='ignore'): +class PydEquipment(PydBaseClass, extra='ignore'): asset_number: str name: str nickname: str | None @@ -473,66 +482,74 @@ class PydEquipment(BaseModel, extra='ignore'): return {k: getattr(self, k) for k in fields} -class PydSubmission(BaseModel, extra='allow'): - filepath: Path - submissiontype: dict | None - submitter_plate_id: dict | None = Field(default=dict(value=None, missing=True), validate_default=True) +class PydRun(PydBaseClass, extra='allow'): + + clientsubmission: PydClientSubmission | None = Field(default=None) rsl_plate_number: dict | None = Field(default=dict(value=None, missing=True), validate_default=True) - submitted_date: dict | None = Field(default=dict(value=date.today(), missing=True), validate_default=True) - clientlab: dict | None + started_date: dict | None = Field(default=dict(value=date.today(), missing=True), validate_default=True) + completed_date: dict | None = Field(default=dict(value=date.today(), missing=True), validate_default=True) sample_count: dict | None - kittype: dict | None - technician: dict | None - submission_category: dict | None = Field(default=dict(value=None, missing=True), validate_default=True) comment: dict | None = Field(default=dict(value="", missing=True), validate_default=True) - reagent: List[dict] | List[PydReagent] = [] - sample: List[PydSample] | Generator - equipment: List[PydEquipment] | None = [] - cost_centre: dict | None = Field(default=dict(value=None, missing=True), validate_default=True) - contact: dict | None = Field(default=dict(value=None, missing=True), validate_default=True) - tips: List[PydTips] | None = [] + sample: List[PydSample] | Generator = Field(default=[]) + run_cost: float | dict = Field(default=dict(value=0.0, missing=True)) + signed_by: str | dict = Field(default="", validate_default=True) + procedure: List[PydProcedure] | Generator = Field(default=[]) - @field_validator("tips", mode="before") + @field_validator("signed_by") @classmethod - def expand_tips(cls, value): - if isinstance(value, dict): - value = value['value'] - if isinstance(value, Generator): - return [PydTips(**tips) for tips in value] - if not value: - return [] + def rescue_signed_by(cls, value): + if isinstance(value, str): + value = dict(value=value, missing=True) return value - @field_validator('equipment', mode='before') + # @field_validator("tips", mode="before") + # @classmethod + # def expand_tips(cls, value): + # if isinstance(value, dict): + # value = value['value'] + # if isinstance(value, Generator): + # return [PydTips(**tips) for tips in value] + # if not value: + # return [] + # return value + # + # @field_validator('equipment', mode='before') + # @classmethod + # def convert_equipment_dict(cls, value): + # if isinstance(value, dict): + # return value['value'] + # if isinstance(value, Generator): + # return [PydEquipment(**equipment) for equipment in value] + # if not value: + # return [] + # return value + + # @field_validator('comment', mode='before') + # @classmethod + # def create_comment(cls, value): + # if value is None: + # return "" + # return value + # + # @field_validator("submitter_plate_id") + # @classmethod + # def enforce_with_uuid(cls, value): + # if value['value'] in [None, "None"]: + # return dict(value=uuid.uuid4().hex.upper(), missing=True) + # else: + # value['value'] = value['value'].strip() + # return value + + @field_validator("run_cost") @classmethod - def convert_equipment_dict(cls, value): - if isinstance(value, dict): - return value['value'] - if isinstance(value, Generator): - return [PydEquipment(**equipment) for equipment in value] - if not value: - return [] + def rescue_run_cost(cls, value): + if isinstance(value, float): + value = dict(value=value, missing=False) return value - @field_validator('comment', mode='before') + @field_validator("started_date", mode="before") @classmethod - def create_comment(cls, value): - if value is None: - return "" - return value - - @field_validator("submitter_plate_id") - @classmethod - def enforce_with_uuid(cls, value): - if value['value'] in [None, "None"]: - return dict(value=uuid.uuid4().hex.upper(), missing=True) - else: - value['value'] = value['value'].strip() - return value - - @field_validator("submitted_date", mode="before") - @classmethod - def rescue_date(cls, value): + def rescue_start_date(cls, value): try: check = value['value'] is None except TypeError: @@ -541,9 +558,20 @@ class PydSubmission(BaseModel, extra='allow'): return dict(value=date.today(), missing=True) return value - @field_validator("submitted_date") + @field_validator("completed_date", mode="before") @classmethod - def strip_datetime_string(cls, value): + def rescue_completed_date(cls, value): + try: + check = value['value'] is None + except TypeError: + check = True + if check: + return dict(value=date.today(), missing=True) + return value + + @field_validator("started_date") + @classmethod + def strip_started_datetime_string(cls, value): match value['value']: case date(): output = datetime.combine(value['value'], datetime.min.time()) @@ -567,35 +595,61 @@ class PydSubmission(BaseModel, extra='allow'): value['value'] = output.replace(tzinfo=timezone) return value - @field_validator("clientlab", mode="before") + @field_validator("completed_date") @classmethod - def rescue_submitting_lab(cls, value): - if value is None: - return dict(value=None, missing=True) + def strip_completed_datetime_string(cls, value): + match value['value']: + case date(): + output = datetime.combine(value['value'], datetime.min.time()) + case datetime(): + pass + case int(): + output = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + value['value'] - 2) + case str(): + string = re.sub(r"(_|-)\d(R\d)?$", "", value['value']) + try: + output = dict(value=parse(string).date(), missing=True) + except ParserError as e: + logger.error(f"Problem parsing date: {e}") + try: + output = parse(string.replace("-", "")).date() + except Exception as e: + logger.error(f"Problem with parse fallback: {e}") + return value + case _: + raise ValueError(f"Could not get datetime from {value['value']}") + value['value'] = output.replace(tzinfo=timezone) return value - @field_validator("clientlab") - @classmethod - def lookup_submitting_lab(cls, value): - if isinstance(value['value'], str): - try: - value['value'] = ClientLab.query(name=value['value']).name - except AttributeError: - value['value'] = None - if value['value'] is None: - value['missing'] = True - if "pytest" in sys.modules: - value['value'] = "Nosocomial" - return value - from frontend.widgets.pop_ups import ObjectSelector - dlg = ObjectSelector(title="Missing Submitting Lab", - message="We need a submitting lab. Please select from the list.", - obj_type=ClientLab) - if dlg.exec(): - value['value'] = dlg.parse_form() - else: - value['value'] = None - return value + # @field_validator("clientlab", mode="before") + # @classmethod + # def rescue_submitting_lab(cls, value): + # if value is None: + # return dict(value=None, missing=True) + # return value + # + # @field_validator("clientlab") + # @classmethod + # def lookup_submitting_lab(cls, value): + # if isinstance(value['value'], str): + # try: + # value['value'] = ClientLab.query(name=value['value']).name + # except AttributeError: + # value['value'] = None + # if value['value'] is None: + # value['missing'] = True + # if "pytest" in sys.modules: + # value['value'] = "Nosocomial" + # return value + # from frontend.widgets.pop_ups import ObjectSelector + # dlg = ObjectSelector(title="Missing Submitting Lab", + # message="We need a submitting lab. Please select from the list.", + # obj_type=ClientLab) + # if dlg.exec(): + # value['value'] = dlg.parse_form() + # else: + # value['value'] = None + # return value @field_validator("rsl_plate_number", mode='before') @classmethod @@ -607,7 +661,7 @@ class PydSubmission(BaseModel, extra='allow'): @field_validator("rsl_plate_number") @classmethod def rsl_from_file(cls, value, values): - sub_type = values.data['proceduretype']['value'] + sub_type = values.data['clientsubmission'] if check_not_nan(value['value']): value['value'] = value['value'].strip() return value @@ -615,25 +669,28 @@ class PydSubmission(BaseModel, extra='allow'): if "pytest" in sys.modules and sub_type.replace(" ", "") == "BasicRun": output = "RSL-BS-Test001" else: - output = RSLNamer(filename=values.data['filepath'].__str__(), submission_type=sub_type, + # try: + output = RSLNamer(filename=sub_type.filepath.__str__(), submission_type=sub_type.submissiontype, data=values.data).parsed_name + + return dict(value=output, missing=True) - @field_validator("technician", mode="before") - @classmethod - def rescue_tech(cls, value): - if value is None: - return dict(value=None, missing=True) - return value - - @field_validator("technician") - @classmethod - def enforce_tech(cls, value): - if check_not_nan(value['value']): - value['value'] = re.sub(r"\: \d", "", value['value']) - return value - else: - return dict(value=convert_nans_to_nones(value['value']), missing=True) + # @field_validator("technician", mode="before") + # @classmethod + # def rescue_tech(cls, value): + # if value is None: + # return dict(value=None, missing=True) + # return value + # + # @field_validator("technician") + # @classmethod + # def enforce_tech(cls, value): + # if check_not_nan(value['value']): + # value['value'] = re.sub(r"\: \d", "", value['value']) + # return value + # else: + # return dict(value=convert_nans_to_nones(value['value']), missing=True) @field_validator("sample_count", mode='before') @classmethod @@ -642,54 +699,54 @@ class PydSubmission(BaseModel, extra='allow'): return dict(value=None, missing=True) return value - @field_validator("kittype", mode='before') - @classmethod - def rescue_kit(cls, value): - if check_not_nan(value): - if isinstance(value, str): - return dict(value=value, missing=False) - elif isinstance(value, dict): - return value - else: - raise ValueError(f"No extraction kittype found.") - if value is None: - # NOTE: Kit selection is done in the clientsubmissionparser, so should not be necessary here. - return dict(value=None, missing=True) - return value + # @field_validator("kittype", mode='before') + # @classmethod + # def rescue_kit(cls, value): + # if check_not_nan(value): + # if isinstance(value, str): + # return dict(value=value, missing=False) + # elif isinstance(value, dict): + # return value + # else: + # raise ValueError(f"No extraction kittype found.") + # if value is None: + # # NOTE: Kit selection is done in the clientsubmissionparser, so should not be necessary here. + # return dict(value=None, missing=True) + # return value + # + # @field_validator("submissiontype", mode='before') + # @classmethod + # def make_submission_type(cls, value, values): + # if not isinstance(value, dict): + # value = dict(value=value) + # if check_not_nan(value['value']): + # value = value['value'].title() + # return dict(value=value, missing=False) + # else: + # return dict(value=RSLNamer.retrieve_submission_type(filename=values.data['filepath']).title(), missing=True) + # + # @field_validator("submission_category", mode="before") + # @classmethod + # def create_category(cls, value): + # if not isinstance(value, dict): + # return dict(value=value, missing=True) + # return value + # + # @field_validator("submission_category") + # @classmethod + # def rescue_category(cls, value, values): + # if isinstance(value['value'], str): + # value['value'] = value['value'].title() + # if value['value'] not in ["Research", "Diagnostic", "Surveillance", "Validation"]: + # value['value'] = values.data['proceduretype']['value'] + # return value - @field_validator("submissiontype", mode='before') - @classmethod - def make_submission_type(cls, value, values): - if not isinstance(value, dict): - value = dict(value=value) - if check_not_nan(value['value']): - value = value['value'].title() - return dict(value=value, missing=False) - else: - return dict(value=RSLNamer.retrieve_submission_type(filename=values.data['filepath']).title(), missing=True) - - @field_validator("submission_category", mode="before") - @classmethod - def create_category(cls, value): - if not isinstance(value, dict): - return dict(value=value, missing=True) - return value - - @field_validator("submission_category") - @classmethod - def rescue_category(cls, value, values): - if isinstance(value['value'], str): - value['value'] = value['value'].title() - if value['value'] not in ["Research", "Diagnostic", "Surveillance", "Validation"]: - value['value'] = values.data['proceduretype']['value'] - return value - - @field_validator("reagent", mode="before") - @classmethod - def expand_reagents(cls, value): - if isinstance(value, Generator): - return [PydReagent(**reagent) for reagent in value] - return value + # @field_validator("reagent", mode="before") + # @classmethod + # def expand_reagents(cls, value): + # if isinstance(value, Generator): + # return [PydReagent(**reagent) for reagent in value] + # return value @field_validator("sample", mode="before") @classmethod @@ -698,80 +755,82 @@ class PydSubmission(BaseModel, extra='allow'): return [PydSample(**sample) for sample in value] return value - @field_validator("sample") - @classmethod - def assign_ids(cls, value): - starting_id = ClientSubmissionSampleAssociation.autoincrement_id() - for iii, sample in enumerate(value, start=starting_id): - # NOTE: Why is this a list? Answer: to zip with the lists of rows and columns in case of multiple of the same sample. - sample.assoc_id = [iii] - return value + # @field_validator("sample") + # @classmethod + # def assign_ids(cls, value): + # starting_id = ClientSubmissionSampleAssociation.autoincrement_id() + # for iii, sample in enumerate(value, start=starting_id): + # # NOTE: Why is this a list? Answer: to zip with the lists of rows and columns in case of multiple of the same sample. + # sample.assoc_id = [iii] + # return value - @field_validator("cost_centre", mode="before") - @classmethod - def rescue_cost_centre(cls, value): - match value: - case dict(): - return value - case _: - return dict(value=value, missing=True) - - @field_validator("cost_centre") - @classmethod - def get_cost_centre(cls, value, values): - match value['value']: - case None: - from backend.db.models import Organization - org = Organization.query(name=values.data['clientlab']['value']) - try: - return dict(value=org.cost_centre, missing=True) - except AttributeError: - return dict(value="xxx", missing=True) - case _: - return value - - @field_validator("contact") - @classmethod - def get_contact_from_org(cls, value, values): - # logger.debug(f"Value coming in: {value}") - match value: - case dict(): - if isinstance(value['value'], tuple): - value['value'] = value['value'][0] - case tuple(): - value = dict(value=value[0], missing=False) - case _: - value = dict(value=value, missing=False) - # logger.debug(f"Value after match: {value}") - check = Contact.query(name=value['value']) - # logger.debug(f"Check came back with {check}") - if not isinstance(check, Contact): - org = values.data['clientlab']['value'] - # logger.debug(f"Checking organization: {org}") - if isinstance(org, str): - org = ClientLab.query(name=values.data['clientlab']['value'], limit=1) - if isinstance(org, ClientLab): - contact = org.contact[0].name - else: - logger.warning(f"All attempts at defaulting Contact failed, returning: {value}") - return value - if isinstance(contact, tuple): - contact = contact[0] - value = dict(value=f"Defaulted to: {contact}", missing=False) - # logger.debug(f"Value after query: {value}") - return value - else: - # logger.debug(f"Value after bypass check: {value}") - return value + # @field_validator("cost_centre", mode="before") + # @classmethod + # def rescue_cost_centre(cls, value): + # match value: + # case dict(): + # return value + # case _: + # return dict(value=value, missing=True) + # + # @field_validator("cost_centre") + # @classmethod + # def get_cost_centre(cls, value, values): + # match value['value']: + # case None: + # from backend.db.models import Organization + # org = Organization.query(name=values.data['clientlab']['value']) + # try: + # return dict(value=org.cost_centre, missing=True) + # except AttributeError: + # return dict(value="xxx", missing=True) + # case _: + # return value + # + # @field_validator("contact") + # @classmethod + # def get_contact_from_org(cls, value, values): + # # logger.debug(f"Value coming in: {value}") + # match value: + # case dict(): + # if isinstance(value['value'], tuple): + # value['value'] = value['value'][0] + # case tuple(): + # value = dict(value=value[0], missing=False) + # case _: + # value = dict(value=value, missing=False) + # # logger.debug(f"Value after match: {value}") + # check = Contact.query(name=value['value']) + # # logger.debug(f"Check came back with {check}") + # if not isinstance(check, Contact): + # org = values.data['clientlab']['value'] + # # logger.debug(f"Checking organization: {org}") + # if isinstance(org, str): + # org = ClientLab.query(name=values.data['clientlab']['value'], limit=1) + # if isinstance(org, ClientLab): + # contact = org.contact[0].name + # else: + # logger.warning(f"All attempts at defaulting Contact failed, returning: {value}") + # return value + # if isinstance(contact, tuple): + # contact = contact[0] + # value = dict(value=f"Defaulted to: {contact}", missing=False) + # # logger.debug(f"Value after query: {value}") + # return value + # else: + # # logger.debug(f"Value after bypass check: {value}") + # return value def __init__(self, run_custom: bool = False, **data): super().__init__(**data) # NOTE: this could also be done with default_factory - self.submission_object = Run.find_polymorphic_subclass( - polymorphic_identity=self.submission_type['value']) - self.namer = RSLNamer(self.rsl_plate_number['value'], submission_type=self.submission_type['value']) - if run_custom: - self.submission_object.custom_validation(pyd=self) + # self.submission_object = Run.find_polymorphic_subclass( + # polymorphic_identity=self.submission_type['value']) + submission_type = self.clientsubmission.submissiontype + # logger.debug(submission_type) + self.namer = RSLNamer(self.rsl_plate_number['value'], submission_type=submission_type) + # if run_custom: + # self.submission_object.custom_validation(pyd=self) def set_attribute(self, key: str, value): """ @@ -1020,7 +1079,7 @@ class PydSubmission(BaseModel, extra='allow'): Returns: str: Output filename """ - template = self.submission_object.filename_template() + template = self.clientsubmission.filename_template render = self.namer.construct_export_name(template=template, **self.improved_dict(dictionaries=False)).replace( "/", "") return render @@ -1537,7 +1596,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): self.kittype['value'] = kittype self.possible_kits.insert(0, self.possible_kits.pop(self.possible_kits.index(kittype))) - def reorder_reagents(self, reagentrole: str, options:list): + def reorder_reagents(self, reagentrole: str, options: list): reagent_used = next((reagent for reagent in self.reagent if reagent.reagentrole == reagentrole), None) if not reagent_used: return options @@ -1547,8 +1606,6 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): options.insert(0, options.pop(options.index(roi))) return options - - def update_kittype_equipmentroles(self, kittype: str | KitType): if kittype == self.__class__.model_fields['kittype'].default['value']: return @@ -1808,6 +1865,23 @@ class PydClientSubmission(PydBaseClass): sql._misc_info['info_placement'] = info_placement return sql + def pad_samples_to_length(self, row_count, column_names): + output_samples = [] + for iii in range(1, row_count + 1): + try: + sample = next((item for item in self.model_extra['samples'] if item.submission_rank == iii)) + except StopIteration: + sample = PydSample(sample_id="") + for column in column_names: + setattr(sample, column[0], "") + sample.submission_rank = iii + output_samples.append(sample) + return sorted(output_samples, key=lambda x: x.submission_rank) + + @property + def filename_template(self): + submissiontype = SubmissionType.query(name=self.submissiontype['value']) + return submissiontype.defaults['filename_template'] class PydResults(PydBaseClass, arbitrary_types_allowed=True): diff --git a/src/submissions/frontend/widgets/app.py b/src/submissions/frontend/widgets/app.py index 4a736c6..b267b89 100644 --- a/src/submissions/frontend/widgets/app.py +++ b/src/submissions/frontend/widgets/app.py @@ -30,7 +30,7 @@ from .turnaround import TurnaroundTime from .concentrations import Concentrations from .omni_search import SearchBox -logger = logging.getLogger(f'procedure.{__name__}') +logger = logging.getLogger(f'submissions.{__name__}') class App(QMainWindow): diff --git a/src/submissions/frontend/widgets/submission_details.py b/src/submissions/frontend/widgets/submission_details.py index 1fe0682..5976e3b 100644 --- a/src/submissions/frontend/widgets/submission_details.py +++ b/src/submissions/frontend/widgets/submission_details.py @@ -18,7 +18,7 @@ from pprint import pformat from typing import List -logger = logging.getLogger(f"procedure.{__name__}") +logger = logging.getLogger(f"submissions.{__name__}") class SubmissionDetails(QDialog): diff --git a/src/submissions/frontend/widgets/submission_table.py b/src/submissions/frontend/widgets/submission_table.py index 643f001..376e1bb 100644 --- a/src/submissions/frontend/widgets/submission_table.py +++ b/src/submissions/frontend/widgets/submission_table.py @@ -16,7 +16,7 @@ from backend.db.models import Run, ClientSubmission, Procedure from tools import Report, Result, report_result, get_application_from_parent from .functions import select_open_file -logger = logging.getLogger(f"procedure.{__name__}") +logger = logging.getLogger(f"submissions.{__name__}") class pandasModel(QAbstractTableModel): diff --git a/src/submissions/frontend/widgets/submission_widget.py b/src/submissions/frontend/widgets/submission_widget.py index 1a6d82c..a06558e 100644 --- a/src/submissions/frontend/widgets/submission_widget.py +++ b/src/submissions/frontend/widgets/submission_widget.py @@ -7,13 +7,13 @@ from PyQt6.QtWidgets import ( ) from PyQt6.QtCore import pyqtSignal, Qt, QSignalBlocker -from backend.managers import DefaultClientSubmission + from .functions import select_open_file, select_save_file import logging from pathlib import Path from tools import Report, Result, check_not_nan, main_form_style, report_result, get_application_from_parent from backend.excel.parsers.clientsubmission_parser import ClientSubmissionInfoParser, ClientSubmissionSampleParser -from backend.validators import PydSubmission, PydReagent, PydClientSubmission, PydSample +from backend.validators import PydRun, PydReagent, PydClientSubmission, PydSample from backend.db import ( ClientLab, SubmissionType, Reagent, ReagentRole, KitTypeReagentRoleAssociation, Run @@ -103,6 +103,7 @@ class SubmissionFormContainer(QWidget): Returns: Report: Object to give results of import. """ + from backend.managers import DefaultClientSubmissionManager self.app.raise_() self.app.activateWindow() logger.info(f"\n\nStarting Import...\n\n") @@ -141,7 +142,7 @@ class SubmissionFormContainer(QWidget): # self.pydclientsubmission = self.clientsubmissionparser.to_pydantic() # self.pydsamples = self.sampleparser.to_pydantic() # logger.debug(f"Samples: {pformat(self.pydclientsubmission.sample)}") - self.clientsubmission_manager = DefaultClientSubmission(parent=self, fname=fname) + self.clientsubmission_manager = DefaultClientSubmissionManager(parent=self, fname=fname) self.pydclientsubmission = self.clientsubmission_manager.parse() checker = SampleChecker(self, "Sample Checker", self.pydclientsubmission.samples) if checker.exec(): @@ -188,7 +189,7 @@ class SubmissionFormContainer(QWidget): class SubmissionFormWidget(QWidget): update_reagent_fields = ['kittype'] - def __init__(self, parent: QWidget, pyd: PydSubmission, disable: list | None = None) -> None: + def __init__(self, parent: QWidget, pyd: PydRun, disable: list | None = None) -> None: super().__init__(parent) if disable is None: disable = [] diff --git a/src/submissions/tools/__init__.py b/src/submissions/tools/__init__.py index 37b76d7..97571b1 100644 --- a/src/submissions/tools/__init__.py +++ b/src/submissions/tools/__init__.py @@ -503,7 +503,8 @@ def setup_lookup(func): try: sanitized_kwargs[k] = v['value'] except KeyError: - raise ValueError("Could not sanitize dictionary in query. Make sure you parse it first.") + + raise ValueError(f"Could not sanitize dictionary {v} in query. Make sure you parse it first.") elif v is not None: sanitized_kwargs[k] = v return func(*args, **sanitized_kwargs)