diff --git a/src/submissions/__main__.py b/src/submissions/__main__.py index 6a153f0..c69a6dd 100644 --- a/src/submissions/__main__.py +++ b/src/submissions/__main__.py @@ -1,6 +1,6 @@ import sys, os from tools import ctx, setup_logger, check_if_app - +from threading import Thread # environment variable must be set to enable qtwebengine in network path if check_if_app(): os.environ['QTWEBENGINE_DISABLE_SANDBOX'] = "1" @@ -23,12 +23,12 @@ def run_startup(): for script in startup_scripts: try: func = getattr(scripts, script) - # func = modules[script] except AttributeError as e: logger.error(f"Couldn't run startup script {script} due to {e}") continue logger.info(f"Running startup script: {func.__name__}") - func.script(ctx) + thread = Thread(target=func.script, args=(ctx, )) + thread.start() def run_teardown(): @@ -45,7 +45,8 @@ def run_teardown(): logger.error(f"Couldn't run teardown script {script} due to {e}") continue logger.info(f"Running teardown script: {func.__name__}") - func.script(ctx) + thread = Thread(target=func.script, args=(ctx,)) + thread.start() if __name__ == '__main__': run_startup() diff --git a/src/submissions/backend/db/models/__init__.py b/src/submissions/backend/db/models/__init__.py index d52ee1b..4e50257 100644 --- a/src/submissions/backend/db/models/__init__.py +++ b/src/submissions/backend/db/models/__init__.py @@ -269,7 +269,7 @@ from .controls import * from .organizations import * from .kits import * from .submissions import * -from .audit import AuditLog +from .audit import * # NOTE: Add a creator to the submission for reagent association. Assigned here due to circular import constraints. # https://docs.sqlalchemy.org/en/20/orm/extensions/associationproxy.html#sqlalchemy.ext.associationproxy.association_proxy.params.creator diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index e7942a7..c58c5f5 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -332,7 +332,6 @@ class KitType(BaseClass): return new_kit - class ReagentRole(BaseClass): """ Base of reagent type abstract diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 9132375..2538797 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -1265,7 +1265,9 @@ class BasicSubmission(BaseClass, LogMixin): logger.error(f"Couldn't save association with {equip} due to {e}") if equip.tips: for tips in equip.tips: + logger.debug(f"Attempting to add tips assoc: {tips} (pydantic)") tassoc = tips.to_sql(submission=self) + logger.debug(f"Attempting to add tips assoc: {tips.__dict__} (sql)") if tassoc not in self.submission_tips_associations: tassoc.save() else: diff --git a/src/submissions/backend/excel/__init__.py b/src/submissions/backend/excel/__init__.py index b09ca53..92009f2 100644 --- a/src/submissions/backend/excel/__init__.py +++ b/src/submissions/backend/excel/__init__.py @@ -2,6 +2,6 @@ Contains pandas and openpyxl convenience functions for interacting with excel workbooks ''' -from .reports import * from .parser import * +from .reports import * from .writer import * diff --git a/src/submissions/backend/scripts/import_irida.py b/src/submissions/backend/scripts/import_irida.py index 9e71f4e..be6a7d2 100644 --- a/src/submissions/backend/scripts/import_irida.py +++ b/src/submissions/backend/scripts/import_irida.py @@ -4,20 +4,24 @@ from datetime import datetime from tools import Settings from backend import BasicSample from backend.db import IridaControl, ControlType +from sqlalchemy.orm import Session logger = logging.getLogger(f"submissions.{__name__}") -def script(ctx:Settings): +def script(ctx: Settings): """ Grabs Irida controls from secondary database. Args: ctx (Settings): Settings inherited from app. - """ - ct = ControlType.query(name="Irida Control") - existing_controls = [item.name for item in IridaControl.query()] + # NOTE: Because the main session will be busy in another thread, this requires a new session. + new_session = Session(ctx.database_session.get_bind()) + # ct = ControlType.query(name="Irida Control") + ct = new_session.query(ControlType).filter(ControlType.name == "Irida Control").first() + # existing_controls = [item.name for item in IridaControl.query()] + existing_controls = [item.name for item in new_session.query(IridaControl)] prm_list = ", ".join([f"'{thing}'" for thing in existing_controls]) ctrl_db_path = ctx.directory_path.joinpath("submissions_parser_output", "submissions.db") try: @@ -25,29 +29,39 @@ def script(ctx:Settings): except AttributeError as e: logger.error(f"Error, could not import from irida due to {e}") return - sql = f"SELECT name, submitted_date, submission_id, contains, matches, kraken, subtype, refseq_version, " \ - f"kraken2_version, kraken2_db_version, sample_id FROM _iridacontrol INNER JOIN _control on _control.id " \ + sql = "SELECT name, submitted_date, submission_id, contains, matches, kraken, subtype, refseq_version, " \ + "kraken2_version, kraken2_db_version, sample_id FROM _iridacontrol INNER JOIN _control on _control.id " \ f"= _iridacontrol.id WHERE _control.name NOT IN ({prm_list})" cursor = conn.execute(sql) - records = [dict(name=row[0], submitted_date=row[1], submission_id=row[2], contains=row[3], matches=row[4], kraken=row[5], - subtype=row[6], refseq_version=row[7], kraken2_version=row[8], kraken2_db_version=row[9], - sample_id=row[10]) for row in cursor] + records = [ + dict(name=row[0], submitted_date=row[1], submission_id=row[2], contains=row[3], matches=row[4], kraken=row[5], + subtype=row[6], refseq_version=row[7], kraken2_version=row[8], kraken2_db_version=row[9], + sample_id=row[10]) for row in cursor] for record in records: - instance = IridaControl.query(name=record['name']) + # instance = IridaControl.query(name=record['name']) + instance = new_session.query(IridaControl).filter(IridaControl.name == record['name']).first() if instance: logger.warning(f"Irida Control {instance.name} already exists, skipping.") continue - record['contains'] = json.loads(record['contains']) - assert isinstance(record['contains'], dict) - record['matches'] = json.loads(record['matches']) - assert isinstance(record['matches'], dict) - record['kraken'] = json.loads(record['kraken']) - assert isinstance(record['kraken'], dict) + for thing in ['contains', 'matches', 'kraken']: + if record[thing]: + record[thing] = json.loads(record[thing]) + assert isinstance(record[thing], dict) + else: + record[thing] = {} + # record['matches'] = json.loads(record['matches']) + # assert isinstance(record['matches'], dict) + # record['kraken'] = json.loads(record['kraken']) + # assert isinstance(record['kraken'], dict) record['submitted_date'] = datetime.strptime(record['submitted_date'], "%Y-%m-%d %H:%M:%S.%f") assert isinstance(record['submitted_date'], datetime) instance = IridaControl(controltype=ct, **record) - sample = BasicSample.query(submitter_id=instance.name) + # sample = BasicSample.query(submitter_id=instance.name) + sample = new_session.query(BasicSample).filter(BasicSample.submitter_id == instance.name).first() if sample: instance.sample = sample instance.submission = sample.submissions[0] - instance.save() + # instance.save() + new_session.add(instance) + new_session.commit() + new_session.close() diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 093d1f0..4b75ba5 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -295,7 +295,8 @@ class PydTips(BaseModel): Returns: SubmissionTipsAssociation: Association between queried tips and submission """ - tips = Tips.query(name=self.name, lot=self.lot, limit=1) + tips = Tips.query(name=self.name, limit=1) + logger.debug(f"Tips query has yielded: {tips}") assoc = SubmissionTipsAssociation.query(tip_id=tips.id, submission_id=submission.id, role=self.role, limit=1) if assoc is None: assoc = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=self.role) @@ -900,17 +901,20 @@ class PydSubmission(BaseModel, extra='allow'): return render # @report_result - def check_kit_integrity(self, extraction_kit: str | dict | None = None) -> Tuple[List[PydReagent], Report]: + def check_kit_integrity(self, extraction_kit: str | dict | None = None, exempt:List[PydReagent]=[]) -> Tuple[ + List[PydReagent], Report]: """ Ensures all reagents expected in kit are listed in Submission Args: - reagenttypes (list | None, optional): List to check against complete list. Defaults to None. + extraction_kit (str | dict | None, optional): kit to be checked. Defaults to None. + exempt (List[PydReagent], optional): List of reagents that don't need to be checked. Defaults to [] Returns: - Report: Result object containing a message and any missing components. + Tuple[List[PydReagent], Report]: List of reagents and Result object containing a message and any missing components. """ report = Report() + # logger.debug(f"The following reagents are exempt from the kit integrity check:\n{exempt}") if isinstance(extraction_kit, str): extraction_kit = dict(value=extraction_kit) if extraction_kit is not None and extraction_kit != self.extraction_kit['value']: @@ -922,7 +926,8 @@ class PydSubmission(BaseModel, extra='allow'): expected_check = [item.role for item in ext_kit_rtypes] output_reagents = [rt for rt in self.reagents if rt.role in expected_check] missing_check = [item.role for item in output_reagents] - missing_reagents = [rt for rt in ext_kit_rtypes if rt.role not in missing_check] + missing_reagents = [rt for rt in ext_kit_rtypes if rt.role not in missing_check and rt.role not in exempt] + # logger.debug(f"Missing reagents: {missing_reagents}") missing_reagents += [rt for rt in output_reagents if rt.missing] output_reagents += [rt for rt in missing_reagents if rt not in output_reagents] # NOTE: if lists are equal return no problem @@ -930,8 +935,8 @@ class PydSubmission(BaseModel, extra='allow'): 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.role.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") + msg=f"The excel sheet you are importing is missing some reagents expected by the kit.\n\nIt looks like you are missing: {[item.role.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 diff --git a/src/submissions/frontend/widgets/__init__.py b/src/submissions/frontend/widgets/__init__.py index afef2b5..a3367f1 100644 --- a/src/submissions/frontend/widgets/__init__.py +++ b/src/submissions/frontend/widgets/__init__.py @@ -2,15 +2,17 @@ Contains all custom generated PyQT6 derivative widgets. """ +from .app import App +from .controls_chart import * +from .equipment_usage import * from .functions import * +from .gel_checker import * +from .info_tab import * from .misc import * +from .omni_search import * from .pop_ups import * +from .submission_details import * from .submission_table import * from .submission_widget import * -from .controls_chart import * -from .submission_details import * -from .equipment_usage import * -from .gel_checker import * -from .summary import Summary -from .turnaround import TurnaroundTime -from .app import App +from .summary import * +from .turnaround import * diff --git a/src/submissions/frontend/widgets/equipment_usage.py b/src/submissions/frontend/widgets/equipment_usage.py index 69393da..891a623 100644 --- a/src/submissions/frontend/widgets/equipment_usage.py +++ b/src/submissions/frontend/widgets/equipment_usage.py @@ -160,7 +160,7 @@ class RoleComboBox(QWidget): PydEquipment|None: PydEquipment matching form """ eq = Equipment.query(name=self.box.currentText()) - tips = [PydTips(name=item.currentText(), role=item.objectName().lstrip("tips").lstrip("_")) for item in + tips = [PydTips(name=item.currentText(), role=item.objectName().lstrip("tips").lstrip("_"), lot="") for item in self.findChildren(QComboBox) if item.objectName().startswith("tips")] try: return PydEquipment( diff --git a/src/submissions/frontend/widgets/submission_widget.py b/src/submissions/frontend/widgets/submission_widget.py index a6dd38a..5d7bf49 100644 --- a/src/submissions/frontend/widgets/submission_widget.py +++ b/src/submissions/frontend/widgets/submission_widget.py @@ -6,7 +6,7 @@ from PyQt6.QtWidgets import ( QComboBox, QDateEdit, QLineEdit, QLabel, QCheckBox, QHBoxLayout, QGridLayout ) from PyQt6.QtCore import pyqtSignal, Qt, QSignalBlocker -from . import select_open_file, select_save_file +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 @@ -338,11 +338,15 @@ class SubmissionFormWidget(QWidget): report = Report() result = self.parse_form() report.add_result(result) + # allow = not all([item.lot.isEnabled() for item in self.findChildren(self.ReagentFormWidget)]) + exempt = [item.reagent.role for item in self.findChildren(self.ReagentFormWidget) if not item.lot.isEnabled()] + # if allow: + # logger.warning(f"Some reagents are disabled, allowing incomplete kit.") if self.disabler.checkbox.isChecked(): - _, result = self.pyd.check_kit_integrity() + _, result = self.pyd.check_kit_integrity(exempt=exempt) report.add_result(result) if len(result.results) > 0: - return + return report base_submission, result = self.pyd.to_sql() # NOTE: check output message for issues try: