New Excel writer.

This commit is contained in:
lwark
2024-05-06 14:51:47 -05:00
parent 61c1a613e2
commit f30f6403d6
10 changed files with 1003 additions and 430 deletions

View File

@@ -4,10 +4,11 @@ Contains pydantic models and accompanying validators
from __future__ import annotations
from operator import attrgetter
import uuid, re, logging
from pydantic import BaseModel, field_validator, Field
from pydantic import BaseModel, field_validator, Field, model_validator
from datetime import date, datetime, timedelta
from dateutil.parser import parse
from dateutil.parser._parser import ParserError
# from dateutil.parser._parser import ParserError
from dateutil.parser import ParserError
from typing import List, Tuple, Literal
from . import RSLNamer
from pathlib import Path
@@ -20,14 +21,17 @@ from io import BytesIO
logger = logging.getLogger(f"submissions.{__name__}")
# class PydMixin(object):
class PydReagent(BaseModel):
lot: str|None
type: str|None
expiry: date|Literal['NA']|None
name: str|None
lot: str | None
type: str | None
expiry: date | Literal['NA'] | None
name: str | None
missing: bool = Field(default=True)
comment: str|None = Field(default="", validate_default=True)
comment: str | None = Field(default="", validate_default=True)
@field_validator('comment', mode='before')
@classmethod
@@ -44,7 +48,7 @@ class PydReagent(BaseModel):
return None
case _:
return value
@field_validator("type")
@classmethod
def rescue_type_with_lookup(cls, value, values):
@@ -62,14 +66,14 @@ class PydReagent(BaseModel):
if value != None:
return convert_nans_to_nones(str(value))
return value
@field_validator("lot")
@classmethod
def enforce_lot_string(cls, value):
if value != None:
return value.upper()
return value
@field_validator("expiry", mode="before")
@classmethod
def enforce_date(cls, value):
@@ -88,14 +92,14 @@ class PydReagent(BaseModel):
if value == None:
value = date.today()
return value
@field_validator("expiry")
@classmethod
def date_na(cls, value):
if isinstance(value, date) and value.year == 1970:
value = "NA"
return value
@field_validator("name", mode="before")
@classmethod
def enforce_name(cls, value, values):
@@ -104,13 +108,30 @@ class PydReagent(BaseModel):
else:
return values.data['type']
def toSQL(self, submission:BasicSubmission|str=None) -> Tuple[Reagent, Report]:
def improved_dict(self) -> dict:
try:
extras = list(self.model_extra.keys())
except AttributeError:
extras = []
fields = list(self.model_fields.keys()) + extras
# output = {}
# for k in fields:
# value = getattr(self, k)
# match value:
# case date():
# value = value.strftime("%Y-%m-%d")
# case _:
# pass
# output[k] = value
return {k: getattr(self, k) for k in fields}
def toSQL(self, submission: BasicSubmission | str = None) -> Tuple[Reagent, Report]:
"""
Converts this instance into a backend.db.models.kit.Reagent instance
Returns:
Tuple[Reagent, Report]: Reagent instance and result of function
"""
"""
report = Report()
# logger.debug("Adding extra fields.")
if self.model_extra != None:
@@ -150,28 +171,51 @@ class PydReagent(BaseModel):
# add end-of-life extension from reagent type to expiry date
# NOTE: this will now be done only in the reporting phase to account for potential changes in end-of-life extensions
return reagent, report
class PydSample(BaseModel, extra='allow'):
# def improved_dict(self) -> dict:
# fields = list(self.model_fields.keys()) + list(self.model_extra.keys())
# return {k:getattr(self,k) for k in fields}
class PydSample(BaseModel, extra='allow'):
submitter_id: str
sample_type: str
row: int|List[int]|None
column: int|List[int]|None
assoc_id: int|List[int]|None = Field(default=None)
row: int | List[int] | None
column: int | List[int] | None
assoc_id: int | List[int] | None = Field(default=None)
submission_rank: int | List[int] | None
@field_validator("row", "column", "assoc_id")
@model_validator(mode='after')
@classmethod
def validate_model(cls, 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)
if k in model.timestamps():
if isinstance(v, str):
v = datetime.strptime(v, "%Y-%m-%d")
data.__setattr__(k, v)
# print(dir(data))
return data
@field_validator("row", "column", "assoc_id", "submission_rank")
@classmethod
def row_int_to_list(cls, value):
if isinstance(value, int):
return [value]
return value
@field_validator("submitter_id", mode="before")
@classmethod
def int_to_str(cls, value):
return str(value)
def toSQL(self, submission:BasicSubmission|str=None) -> Tuple[BasicSample, Result]:
def improved_dict(self) -> dict:
fields = list(self.model_fields.keys()) + list(self.model_extra.keys())
return {k: getattr(self, k) for k in fields}
def toSQL(self, submission: BasicSubmission | str = None) -> Tuple[BasicSample, Result]:
"""
Converts this instance into a backend.db.models.submissions.Sample object
@@ -180,7 +224,7 @@ class PydSample(BaseModel, extra='allow'):
Returns:
Tuple[BasicSample, Result]: Sample object and result object.
"""
"""
report = None
self.__dict__.update(self.model_extra)
logger.debug(f"Here is the incoming sample dict: \n{self.__dict__}")
@@ -199,10 +243,10 @@ class PydSample(BaseModel, extra='allow'):
for row, column, id in zip(self.row, self.column, self.assoc_id):
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,
row=row, column=column, id=id)
association = SubmissionSampleAssociation.query_or_create(association_type=f"{assoc_type} Association",
submission=submission,
sample=instance,
row=row, column=column, id=id)
# logger.debug(f"Using submission_sample_association: {association}")
try:
# instance.sample_submission_associations.append(association)
@@ -212,13 +256,21 @@ class PydSample(BaseModel, extra='allow'):
instance.metadata.session.rollback()
return instance, out_associations, report
class PydEquipment(BaseModel, extra='ignore'):
def improved_dict(self) -> dict:
try:
extras = list(self.model_extra.keys())
except AttributeError:
extras = []
fields = list(self.model_fields.keys()) + extras
return {k: getattr(self, k) for k in fields}
class PydEquipment(BaseModel, extra='ignore'):
asset_number: str
name: str
nickname: str|None
processes: List[str]|None
role: str|None
nickname: str | None
processes: List[str] | None
role: str | None
@field_validator('processes', mode='before')
@classmethod
@@ -227,11 +279,11 @@ class PydEquipment(BaseModel, extra='ignore'):
value = convert_nans_to_nones(value)
if value == None:
value = ['']
if len(value)==0:
value=['']
if len(value) == 0:
value = ['']
return value
def toSQL(self, submission:BasicSubmission|str=None) -> Tuple[Equipment, SubmissionEquipmentAssociation]:
def toSQL(self, submission: BasicSubmission | str = None) -> Tuple[Equipment, SubmissionEquipmentAssociation]:
"""
Creates Equipment and SubmssionEquipmentAssociations for this PydEquipment
@@ -240,7 +292,7 @@ class PydEquipment(BaseModel, extra='ignore'):
Returns:
Tuple[Equipment, SubmissionEquipmentAssociation]: SQL objects
"""
"""
if isinstance(submission, str):
submission = BasicSubmission.query(rsl_number=submission)
equipment = Equipment.query(asset_number=self.asset_number)
@@ -252,7 +304,8 @@ class PydEquipment(BaseModel, extra='ignore'):
if process == None:
# logger.debug("Adding in unknown process.")
from frontend.widgets.pop_ups import QuestionAsker
dlg = QuestionAsker(title="Add Process?", message=f"Unable to find {self.processes[0]} in the database.\nWould you like to add it?")
dlg = QuestionAsker(title="Add Process?",
message=f"Unable to find {self.processes[0]} in the database.\nWould you like to add it?")
if dlg.exec():
kit = submission.extraction_kit
submission_type = submission.submission_type
@@ -267,24 +320,33 @@ class PydEquipment(BaseModel, extra='ignore'):
assoc = None
return equipment, assoc
def improved_dict(self) -> dict:
try:
extras = list(self.model_extra.keys())
except AttributeError:
extras = []
fields = list(self.model_fields.keys()) + extras
return {k: getattr(self, k) for k in fields}
class PydSubmission(BaseModel, extra='allow'):
filepath: Path
submission_type: dict|None
submission_type: dict | None
# For defaults
submitter_plate_num: dict|None = Field(default=dict(value=None, missing=True), validate_default=True)
submitted_date: dict|None
rsl_plate_num: dict|None = Field(default=dict(value=None, missing=True), validate_default=True)
submitted_date: dict|None
submitting_lab: dict|None
sample_count: dict|None
extraction_kit: 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)
reagents: List[dict]|List[PydReagent] = []
submitter_plate_num: dict | None = Field(default=dict(value=None, missing=True), validate_default=True)
submitted_date: dict | None
rsl_plate_num: dict | None = Field(default=dict(value=None, missing=True), validate_default=True)
submitted_date: dict | None
submitting_lab: dict | None
sample_count: dict | None
extraction_kit: 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)
reagents: List[dict] | List[PydReagent] = []
samples: List[PydSample]
equipment: List[PydEquipment]|None =[]
cost_centre: dict|None = Field(default=dict(value=None, missing=True), validate_default=True)
equipment: List[PydEquipment] | None = []
cost_centre: dict | None = Field(default=dict(value=None, missing=True), validate_default=True)
@field_validator('equipment', mode='before')
@classmethod
@@ -309,7 +371,7 @@ class PydSubmission(BaseModel, extra='allow'):
return dict(value=uuid.uuid4().hex.upper(), missing=True)
else:
return value
@field_validator("submitted_date", mode="before")
@classmethod
def rescue_date(cls, value):
@@ -325,31 +387,32 @@ class PydSubmission(BaseModel, extra='allow'):
@field_validator("submitted_date")
@classmethod
def strip_datetime_string(cls, value):
if isinstance(value['value'], datetime):
return value
if isinstance(value['value'], date):
return value
if isinstance(value['value'], int):
return dict(value=datetime.fromordinal(datetime(1900, 1, 1).toordinal() + value['value'] - 2).date(), missing=True)
return dict(value=datetime.fromordinal(datetime(1900, 1, 1).toordinal() + value['value'] - 2).date(),
missing=True)
string = re.sub(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 = dict(value=parse(string.replace("-","")).date(), missing=True)
output = dict(value=parse(string.replace("-", "")).date(), missing=True)
except Exception as e:
logger.error(f"Problem with parse fallback: {e}")
return output
@field_validator("submitting_lab", mode="before")
@classmethod
def rescue_submitting_lab(cls, value):
if value is None:
return dict(value=None, missing=True)
return value
@field_validator("submitting_lab")
@classmethod
def lookup_submitting_lab(cls, value):
@@ -361,13 +424,15 @@ class PydSubmission(BaseModel, extra='allow'):
if value['value'] is None:
value['missing'] = True
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=Organization)
dlg = ObjectSelector(title="Missing Submitting Lab",
message="We need a submitting lab. Please select from the list.",
obj_type=Organization)
if dlg.exec():
value['value'] = dlg.parse_form()
else:
value['value'] = None
return value
@field_validator("rsl_plate_num", mode='before')
@classmethod
def rescue_rsl_number(cls, value):
@@ -383,7 +448,8 @@ class PydSubmission(BaseModel, extra='allow'):
if check_not_nan(value['value']):
return value
else:
output = RSLNamer(filename=values.data['filepath'].__str__(), sub_type=sub_type, data=values.data).parsed_name
output = RSLNamer(filename=values.data['filepath'].__str__(), sub_type=sub_type,
data=values.data).parsed_name
return dict(value=output, missing=True)
@field_validator("technician", mode="before")
@@ -401,14 +467,14 @@ class PydSubmission(BaseModel, extra='allow'):
return value
else:
return dict(value=convert_nans_to_nones(value['value']), missing=True)
@field_validator("sample_count", mode='before')
@classmethod
def rescue_sample_count(cls, value):
if value == None:
return dict(value=None, missing=True)
return value
@field_validator("extraction_kit", mode='before')
@classmethod
def rescue_kit(cls, value):
@@ -422,7 +488,7 @@ class PydSubmission(BaseModel, extra='allow'):
if value == None:
return dict(value=None, missing=True)
return value
@field_validator("submission_type", mode='before')
@classmethod
def make_submission_type(cls, value, values):
@@ -434,7 +500,7 @@ class PydSubmission(BaseModel, extra='allow'):
else:
# return dict(value=RSLNamer(instr=values.data['filepath'].__str__()).submission_type.title(), missing=True)
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):
@@ -490,25 +556,28 @@ class PydSubmission(BaseModel, extra='allow'):
"""
Collapses multiple samples with same submitter id into one with lists for rows, columns.
Necessary to prevent trying to create duplicate samples in SQL creation.
"""
"""
submitter_ids = list(set([sample.submitter_id for sample in self.samples]))
output = []
for id in submitter_ids:
relevants = [item for item in self.samples if item.submitter_id==id]
relevants = [item for item in self.samples if item.submitter_id == id]
if len(relevants) <= 1:
output += relevants
else:
rows = [item.row[0] for item in relevants]
columns = [item.column[0] for item in relevants]
ids = [item.assoc_id[0] for item in relevants]
ranks = [item.submission_rank[0] for item in relevants]
dummy = relevants[0]
dummy.assoc_id = ids
dummy.row = rows
dummy.column = columns
dummy.submission_rank = ranks
output.append(dummy)
self.samples = output
def improved_dict(self, dictionaries:bool=True) -> dict:
# TODO: Return samples, reagents, etc to dictionaries as well.
def improved_dict(self, dictionaries: bool = True) -> dict:
"""
Adds model_extra to fields.
@@ -517,13 +586,18 @@ class PydSubmission(BaseModel, extra='allow'):
Returns:
dict: This instance as a dictionary
"""
"""
fields = list(self.model_fields.keys()) + list(self.model_extra.keys())
if dictionaries:
output = {k:getattr(self, k) for k in fields}
output = {k: getattr(self, k) for k in fields}
output['reagents'] = [item.improved_dict() for item in self.reagents]
output['samples'] = [item.improved_dict() for item in self.samples]
output['equipment'] = [item.improved_dict() for item in self.equipment]
else:
# logger.debug("Extracting 'value' from attributes")
output = {k:(getattr(self, k) if not isinstance(getattr(self, k), dict) else getattr(self, k)['value']) for k in fields}
output = {k: (getattr(self, k) if not isinstance(getattr(self, k), dict) else getattr(self, k)['value']) for
k in fields}
return output
def find_missing(self) -> Tuple[dict, dict]:
@@ -532,9 +606,9 @@ class PydSubmission(BaseModel, extra='allow'):
Returns:
Tuple[dict, dict]: Dict for missing info, dict for missing reagents.
"""
info = {k:v for k,v in self.improved_dict().items() if isinstance(v, dict)}
missing_info = {k:v for k,v in info.items() if v['missing']}
"""
info = {k: v for k, v in self.improved_dict().items() if isinstance(v, dict)}
missing_info = {k: v for k, v in info.items() if v['missing']}
missing_reagents = [reagent for reagent in self.reagents if reagent.missing]
return missing_info, missing_reagents
@@ -544,10 +618,11 @@ class PydSubmission(BaseModel, extra='allow'):
Returns:
Tuple[BasicSubmission, Result]: BasicSubmission instance, result object
"""
"""
# self.__dict__.update(self.model_extra)
dicto = self.improved_dict()
instance, code, msg = BasicSubmission.query_or_create(submission_type=self.submission_type['value'], rsl_plate_num=self.rsl_plate_num['value'])
instance, code, msg = BasicSubmission.query_or_create(submission_type=self.submission_type['value'],
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}")
@@ -574,9 +649,19 @@ class PydSubmission(BaseModel, extra='allow'):
equip, association = equip.toSQL(submission=instance)
if association != 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)
# TODO: case item if item in instance.jsons()
case item if item in instance.jsons():
logger.debug(f"{item} is a json.")
try:
ii = value.items()
except AttributeError:
ii = {}
for k, v in ii:
if isinstance(v, datetime):
value[k] = v.strftime("%Y-%m-%d %H:%M:%S")
instance.set_attribute(key=key, value=value)
case _:
try:
instance.set_attribute(key=key, value=value)
@@ -598,7 +683,8 @@ class PydSubmission(BaseModel, extra='allow'):
# Apply any discounts that are applicable for client and kit.
try:
logger.debug("Checking and applying discounts...")
discounts = [item.amount for item in Discount.query(kit_type=instance.extraction_kit, organization=instance.submitting_lab)]
discounts = [item.amount for item in
Discount.query(kit_type=instance.extraction_kit, organization=instance.submitting_lab)]
logger.debug(f"We got discounts: {discounts}")
if len(discounts) > 0:
discounts = sum(discounts)
@@ -613,8 +699,8 @@ class PydSubmission(BaseModel, extra='allow'):
logger.debug(f"Something went wrong constructing instance {self.rsl_plate_num}: {e}")
logger.debug(f"Constructed submissions message: {msg}")
return instance, result
def toForm(self, parent:QWidget):
def toForm(self, parent: QWidget):
"""
Converts this instance into a frontend.widgets.submission_widget.SubmissionFormWidget
@@ -623,11 +709,11 @@ class PydSubmission(BaseModel, extra='allow'):
Returns:
SubmissionFormWidget: Submission form widget
"""
"""
from frontend.widgets.submission_widget import SubmissionFormWidget
return SubmissionFormWidget(parent=parent, submission=self)
def autofill_excel(self, missing_only:bool=True, backup:bool=False) -> Workbook:
def autofill_excel(self, missing_only: bool = True, backup: bool = False) -> Workbook:
"""
Fills in relevant information/reagent cells in an excel workbook.
@@ -637,13 +723,13 @@ class PydSubmission(BaseModel, extra='allow'):
Returns:
Workbook: Filled in workbook
"""
"""
# open a new workbook using openpyxl
if self.filepath.stem.startswith("tmp"):
template = SubmissionType.query(name=self.submission_type['value']).template_file
workbook = load_workbook(BytesIO(template))
missing_only = False
else:
else:
try:
workbook = load_workbook(self.filepath)
except Exception as e:
@@ -654,12 +740,12 @@ class PydSubmission(BaseModel, extra='allow'):
if missing_only:
info, reagents = self.find_missing()
else:
info = {k:v for k,v in self.improved_dict().items() if isinstance(v, dict)}
info = {k: v for k, v in self.improved_dict().items() if isinstance(v, dict)}
reagents = self.reagents
if len(reagents + list(info.keys())) == 0:
# logger.warning("No info to fill in, returning")
return None
logger.debug(f"Info: {pformat(info)}")
logger.debug(f"We have blank info and/or reagents in the excel sheet.\n\tLet's try to fill them in.")
# extraction_kit = lookup_kit_types(ctx=self.ctx, name=self.extraction_kit['value'])
extraction_kit = KitType.query(name=self.extraction_kit['value'])
@@ -687,7 +773,7 @@ class PydSubmission(BaseModel, extra='allow'):
new_reagents.append(new_reagent)
new_info = []
# logger.debug("Constructing info map and values")
for k,v in info.items():
for k, v in info.items():
try:
new_item = {}
new_item['type'] = k
@@ -708,33 +794,38 @@ class PydSubmission(BaseModel, extra='allow'):
# get list of sheet names
for sheet in workbook.sheetnames:
# open sheet
worksheet=workbook[sheet]
worksheet = workbook[sheet]
# Get relevant reagents for that sheet
sheet_reagents = [item for item in new_reagents if sheet in item['sheet']]
for reagent in sheet_reagents:
# logger.debug(f"Attempting to write lot {reagent['lot']['value']} in: row {reagent['lot']['row']}, column {reagent['lot']['column']}")
worksheet.cell(row=reagent['lot']['row'], column=reagent['lot']['column'], value=reagent['lot']['value'])
worksheet.cell(row=reagent['lot']['row'], column=reagent['lot']['column'],
value=reagent['lot']['value'])
# logger.debug(f"Attempting to write expiry {reagent['expiry']['value']} in: row {reagent['expiry']['row']}, column {reagent['expiry']['column']}")
if isinstance(reagent['expiry']['value'], date) and reagent['expiry']['value'].year == 1970:
reagent['expiry']['value'] = "NA"
worksheet.cell(row=reagent['expiry']['row'], column=reagent['expiry']['column'], value=reagent['expiry']['value'])
worksheet.cell(row=reagent['expiry']['row'], column=reagent['expiry']['column'],
value=reagent['expiry']['value'])
try:
# logger.debug(f"Attempting to write name {reagent['name']['value']} in: row {reagent['name']['row']}, column {reagent['name']['column']}")
worksheet.cell(row=reagent['name']['row'], column=reagent['name']['column'], value=reagent['name']['value'])
worksheet.cell(row=reagent['name']['row'], column=reagent['name']['column'],
value=reagent['name']['value'])
except Exception as e:
logger.error(f"Could not write name {reagent['name']['value']} due to {e}")
# Get relevant info for that sheet
new_info = [item for item in new_info if isinstance(item['location'], dict)]
sheet_info = [item for item in new_info if sheet in item['location']['sheets']]
logger.debug(f"New info: {pformat(new_info)}")
sheet_info = [item for item in new_info if item['location']['sheet'] == sheet]
for item in sheet_info:
logger.debug(f"Attempting: {item['type']} in row {item['location']['row']}, column {item['location']['column']}")
logger.debug(
f"Attempting: {item['type']} in row {item['location']['row']}, column {item['location']['column']}")
worksheet.cell(row=item['location']['row'], column=item['location']['column'], value=item['value'])
# Hacky way to pop in 'signed by'
custom_parser = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type['value'])
workbook = custom_parser.custom_autofill(workbook, info=self.improved_dict(), backup=backup)
return workbook
def autofill_samples(self, workbook:Workbook) -> Workbook:
def autofill_samples(self, workbook: Workbook) -> Workbook:
"""
Fill in sample rows on the excel sheet
@@ -743,8 +834,9 @@ class PydSubmission(BaseModel, extra='allow'):
Returns:
Workbook: Updated excel workbook
"""
sample_info = SubmissionType.query(name=self.submission_type['value']).info_map['samples']
"""
# sample_info = SubmissionType.query(name=self.submission_type['value']).info_map['samples']
sample_info = SubmissionType.query(name=self.submission_type['value']).construct_sample_map()
logger.debug(f"Sample info: {pformat(sample_info)}")
logger.debug(f"Workbook sheets: {workbook.sheetnames}")
worksheet = workbook[sample_info["lookup_table"]['sheet']]
@@ -762,10 +854,11 @@ class PydSubmission(BaseModel, extra='allow'):
logger.debug(f"Writing to {row}")
if row == None:
row = sample_info['lookup_table']['start_row'] + iii
fields = [field for field in list(sample.model_fields.keys()) + list(sample.model_extra.keys()) if field in sample_info['sample_columns'].keys()]
fields = [field for field in list(sample.model_fields.keys()) +
list(sample.model_extra.keys()) if field in sample_info['lookup_table']['sample_columns'].keys()]
logger.debug(f"Here are the fields we are going to fill:\n\t{fields}")
for field in fields:
column = sample_info['sample_columns'][field]
column = sample_info['lookup_table']['sample_columns'][field]
value = getattr(sample, field)
match value:
case list():
@@ -776,8 +869,8 @@ class PydSubmission(BaseModel, extra='allow'):
value = row_map[value]
worksheet.cell(row=row, column=column, value=value)
return workbook
def autofill_equipment(self, workbook:Workbook) -> Workbook:
def autofill_equipment(self, workbook: Workbook) -> Workbook:
"""
Fill in equipment on the excel sheet
@@ -786,7 +879,7 @@ class PydSubmission(BaseModel, extra='allow'):
Returns:
Workbook: Updated excel workbook
"""
"""
equipment_map = SubmissionType.query(name=self.submission_type['value']).construct_equipment_map()
logger.debug(f"Equipment map: {equipment_map}")
# See if all equipment has a location map
@@ -820,23 +913,31 @@ class PydSubmission(BaseModel, extra='allow'):
case _:
pass
worksheet.cell(row=rel['name']['row'], column=rel['name']['column'], value=rel['name']['value'])
worksheet.cell(row=rel['process']['row'], column=rel['process']['column'], value=rel['process']['value'])
worksheet.cell(row=rel['process']['row'], column=rel['process']['column'],
value=rel['process']['value'])
return workbook
def toWriter(self):
from backend.excel.writer import SheetWriter
return SheetWriter(self)
def construct_filename(self) -> str:
"""
Creates filename for this instance
Returns:
str: Output filename
"""
template = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=self.submission_type).filename_template()
"""
template = BasicSubmission.find_polymorphic_subclass(
polymorphic_identity=self.submission_type).filename_template()
# logger.debug(f"Using template string: {template}")
render = RSLNamer.construct_export_name(template=template, **self.improved_dict(dictionaries=False)).replace("/", "")
render = RSLNamer.construct_export_name(template=template, **self.improved_dict(dictionaries=False)).replace(
"/", "")
# logger.debug(f"Template rendered as: {render}")
return render
def check_kit_integrity(self, reagenttypes:list=[], extraction_kit:str|dict|None=None) -> Tuple[List[PydReagent], Report]:
def check_kit_integrity(self, reagenttypes: list = [], extraction_kit: str | dict | None = None) -> Tuple[
List[PydReagent], Report]:
"""
Ensures all reagents expected in kit are listed in Submission
@@ -845,13 +946,13 @@ class PydSubmission(BaseModel, extra='allow'):
Returns:
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)}")
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']
self.extraction_kit['value'] = extraction_kit['value']
# reagenttypes = []
# else:
# reagenttypes = [item.type for item in self.reagents]
@@ -859,7 +960,8 @@ class PydSubmission(BaseModel, extra='allow'):
# reagenttypes = [item.type for item in self.reagents]
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'])]
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}")
# check if lists are equal
@@ -883,18 +985,20 @@ class PydSubmission(BaseModel, extra='allow'):
output_reagents += [rt for rt in missing_reagents if rt not in output_reagents]
logger.debug(f"Missing reagents types: {missing_reagents}")
# if lists are equal return no problem
if len(missing_reagents)==0:
if len(missing_reagents) == 0:
result = None
else:
result = Result(msg=f"The excel sheet you are importing is missing some reagents expected by the kit.\n\nIt looks like you are missing: {[item.type.upper() for item in missing_reagents]}\n\nAlternatively, 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!", status="Warning")
result = Result(
msg=f"The excel sheet you are importing is missing some reagents expected by the kit.\n\nIt looks like you are missing: {[item.type.upper() for item in missing_reagents]}\n\nAlternatively, 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!",
status="Warning")
report.add_result(result)
return output_reagents, report
class PydContact(BaseModel):
name: str
phone: str|None
email: str|None
phone: str | None
email: str | None
def toSQL(self) -> Contact:
"""
@@ -902,14 +1006,14 @@ class PydContact(BaseModel):
Returns:
Contact: Contact instance
"""
"""
return Contact(name=self.name, phone=self.phone, email=self.email)
class PydOrganization(BaseModel):
class PydOrganization(BaseModel):
name: str
cost_centre: str
contacts: List[PydContact]|None
contacts: List[PydContact] | None
def toSQL(self) -> Organization:
"""
@@ -917,7 +1021,7 @@ class PydOrganization(BaseModel):
Returns:
Organization: Organization instance
"""
"""
instance = Organization()
for field in self.model_fields:
match field:
@@ -929,12 +1033,12 @@ class PydOrganization(BaseModel):
instance.__setattr__(name=field, value=value)
return instance
class PydReagentType(BaseModel):
class PydReagentType(BaseModel):
name: str
eol_ext: timedelta|int|None
uses: dict|None
required: int|None = Field(default=1)
eol_ext: timedelta | int | None
uses: dict | None
required: int | None = Field(default=1)
@field_validator("eol_ext")
@classmethod
@@ -942,8 +1046,8 @@ class PydReagentType(BaseModel):
if isinstance(value, int):
return timedelta(days=value)
return value
def toSQL(self, kit:KitType) -> ReagentType:
def toSQL(self, kit: KitType) -> ReagentType:
"""
Converts this instance into a backend.db.models.ReagentType instance
@@ -952,7 +1056,7 @@ class PydReagentType(BaseModel):
Returns:
ReagentType: ReagentType instance
"""
"""
instance: ReagentType = ReagentType.query(name=self.name)
if instance == None:
instance = ReagentType(name=self.name, eol_ext=self.eol_ext)
@@ -962,11 +1066,12 @@ class PydReagentType(BaseModel):
except StatementError:
assoc = None
if assoc == None:
assoc = KitTypeReagentTypeAssociation(kit_type=kit, reagent_type=instance, uses=self.uses, required=self.required)
assoc = KitTypeReagentTypeAssociation(kit_type=kit, reagent_type=instance, uses=self.uses,
required=self.required)
return instance
class PydKit(BaseModel):
class PydKit(BaseModel):
name: str
reagent_types: List[PydReagentType] = []
@@ -976,7 +1081,7 @@ class PydKit(BaseModel):
Returns:
Tuple[KitType, Report]: KitType instance and report of results.
"""
"""
report = Report()
instance = KitType.query(name=self.name)
if instance == None:
@@ -984,13 +1089,13 @@ class PydKit(BaseModel):
[item.toSQL(instance) for item in self.reagent_types]
return instance, report
class PydEquipmentRole(BaseModel):
class PydEquipmentRole(BaseModel):
name: str
equipment: List[PydEquipment]
processes: List[str]|None
def toForm(self, parent, used:list) -> "RoleComboBox":
processes: List[str] | None
def toForm(self, parent, used: list) -> "RoleComboBox":
"""
Creates a widget for user input into this class.
@@ -1000,7 +1105,6 @@ class PydEquipmentRole(BaseModel):
Returns:
RoleComboBox: widget
"""
"""
from frontend.widgets.equipment_usage import RoleComboBox
return RoleComboBox(parent=parent, role=self, used=used)