From 6c7422e26521f136c00051917f26af3cb0146e1f Mon Sep 17 00:00:00 2001 From: John Major Date: Sat, 7 Mar 2026 12:36:55 -0800 Subject: [PATCH] Hard-cut Bloom TapDB/EUID integration and accessioning removal --- bloom_lims/api/v1/external_specimens.py | 2 - bloom_lims/app.py | 9 +- bloom_lims/config/action/accessioning-ay.json | 68 --- .../action/assay-queue-accessioning.json | 48 -- bloom_lims/config/action/core.json | 8 +- bloom_lims/config/action/data.json | 4 +- bloom_lims/config/action/equipment.json | 8 +- bloom_lims/config/action/file.json | 2 +- bloom_lims/config/action/file_set.json | 4 +- bloom_lims/config/action/move-queues.json | 2 +- bloom_lims/config/action/object.json | 2 +- bloom_lims/config/action/packages.json | 94 --- bloom_lims/config/action/plate.json | 6 +- bloom_lims/config/action/pool.json | 4 +- .../config/action/test_requisitions.json | 6 +- bloom_lims/config/action/workflow.json | 51 +- .../action/workflow_step_accessioning.json | 310 ---------- .../config/action/workflow_step_analysis.json | 2 +- .../config/action/workflow_step_destroy.json | 2 +- .../action/workflow_step_extraction.json | 2 +- .../workflow_step_plate_operations.json | 4 +- .../config/action/workflow_step_queue.json | 26 +- bloom_lims/config/container/kit.json | 33 -- bloom_lims/config/container/package.json | 33 -- bloom_lims/config/workflow/accessioning.json | 34 -- .../workflow_step/accessioning-steps.json | 210 ------- bloom_lims/core/action_execution.py | 54 +- bloom_lims/core/template_validation.py | 2 +- bloom_lims/domain/base.py | 53 +- bloom_lims/domain/external_specimens.py | 110 +++- bloom_lims/domain/workflows.py | 71 +-- bloom_lims/gui/routes/files.py | 10 +- bloom_lims/integrations/atlas/client.py | 67 +-- bloom_lims/schemas/external_specimens.py | 9 - tests/test_accessioning_package_flow.py | 533 ------------------ tests/test_action_schema_coverage.py | 14 +- tests/test_api_actions_execute.py | 16 + tests/test_atlas_lookup_resilience.py | 19 +- tests/test_atlas_workflow_contract.py | 20 +- tests/test_create_acc_workflows.py | 318 ----------- 40 files changed, 241 insertions(+), 2029 deletions(-) delete mode 100644 bloom_lims/config/action/accessioning-ay.json delete mode 100644 bloom_lims/config/action/assay-queue-accessioning.json delete mode 100644 bloom_lims/config/action/packages.json delete mode 100644 bloom_lims/config/action/workflow_step_accessioning.json delete mode 100644 bloom_lims/config/container/kit.json delete mode 100644 bloom_lims/config/container/package.json delete mode 100644 bloom_lims/config/workflow/accessioning.json delete mode 100644 bloom_lims/config/workflow_step/accessioning-steps.json delete mode 100644 tests/test_accessioning_package_flow.py delete mode 100644 tests/test_create_acc_workflows.py diff --git a/bloom_lims/api/v1/external_specimens.py b/bloom_lims/api/v1/external_specimens.py index e5ca82d..d38396f 100644 --- a/bloom_lims/api/v1/external_specimens.py +++ b/bloom_lims/api/v1/external_specimens.py @@ -111,7 +111,6 @@ async def find_external_specimens_by_reference( order_number: str | None = Query(None), patient_id: str | None = Query(None), shipment_number: str | None = Query(None), - package_number: str | None = Query(None), kit_barcode: str | None = Query(None), user: APIUser = Depends(require_external_token_auth), ): @@ -119,7 +118,6 @@ async def find_external_specimens_by_reference( order_number=order_number, patient_id=patient_id, shipment_number=shipment_number, - package_number=package_number, kit_barcode=kit_barcode, ) service = ExternalSpecimenService(app_username=user.email) diff --git a/bloom_lims/app.py b/bloom_lims/app.py index 644ed17..0e14e44 100644 --- a/bloom_lims/app.py +++ b/bloom_lims/app.py @@ -5,6 +5,7 @@ from __future__ import annotations +import logging import os from fastapi import FastAPI @@ -14,7 +15,6 @@ from bloom_lims.api import RateLimitMiddleware, api_v1_router from bloom_lims.gui.errors import register_exception_handlers -from bloom_lims.gui.router import router as gui_router from bloom_lims.tapdb_metrics import request_method_var, request_path_var, stop_all_writers @@ -58,7 +58,12 @@ async def _metrics_request_context(request, call_next): # Include routers app.include_router(api_v1_router) - app.include_router(gui_router) + try: + from bloom_lims.gui.router import router as gui_router + except ModuleNotFoundError as exc: + logging.warning("Skipping GUI router due to missing optional dependency: %s", exc.name) + else: + app.include_router(gui_router) register_exception_handlers(app) return app diff --git a/bloom_lims/config/action/accessioning-ay.json b/bloom_lims/config/action/accessioning-ay.json deleted file mode 100644 index ef14d5c..0000000 --- a/bloom_lims/config/action/accessioning-ay.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "create_package_and_first_workflow_step_assay_root": { - "1.0": { - "action_template": { - "child_workflow_step_obj": { - "workflow_step/accessioning-steps/package-generated/1.0": { - "json_addl": { - "properties": { - "step_number": "1" - } - } - } - }, - "child_workflow_object": { - "workflow/accessioning/accession-package-kit-tubes-testreq/1.0": { - "json_addl": { - "properties": { - "step_number": "1" - } - } - } - }, - "workflow_step_to_attach_as_child": { - "workflow_step/queue/all-purpose/1.0/": { - "json_addl": { - "properties": { - "name": "", - "comments": "", - "step_number": "0" - } - } - } - }, - "method_name": "do_action_create_package_and_first_workflow_step_assay", - "new_container_obj": { - "container/package/generic/*/": { - "json_addl": { - "properties": { - "comments": "" - } - } - } - }, - "description": "Create Package and First Workflow Step", - "action_name": "Register Package", - "action_executed": "0", - "max_executions": "-1", - "action_enabled": "1", - "capture_data": "yes", - "captured_data": {}, - "deactivate_actions_when_executed": [], - "executed_datetime": [], - "action_order": "0", - "action_simple_value": "", - "action_user": [], - "curr_user": "", - "ui_schema": { - "title": "Register Package", - "fields": [] - } - }, - "properties": { - "name": "", - "comments": "" - } - } - } -} diff --git a/bloom_lims/config/action/assay-queue-accessioning.json b/bloom_lims/config/action/assay-queue-accessioning.json deleted file mode 100644 index 9e80f12..0000000 --- a/bloom_lims/config/action/assay-queue-accessioning.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "create_package_and_first_workflow_step": { - "1.0": { - "action_template": { - "child_workflow_step_obj": { - "workflow_step/accessioning-steps/package-generated/1.0": { - "json_addl": { - "properties": { - "step_number": "1" - } - } - } - }, - "method_name": "do_action_create_package_and_first_workflow_stepxx", - "new_container_obj": { - "container/package/generic/*/": { - "json_addl": { - "properties": { - "comments": "" - } - } - } - }, - "description": "Create Package and First Workflow Stepxx", - "action_name": "Register Packagexx", - "action_executed": "0", - "max_executions": "-1", - "action_enabled": "1", - "capture_data": "yes", - "captured_data": {}, - "deactivate_actions_when_executed": [], - "executed_datetime": [], - "action_order": "0", - "action_simple_value": "", - "action_user": [], - "curr_user": "", - "ui_schema": { - "title": "Register Packagexx", - "fields": [] - } - }, - "properties": { - "name": "", - "comments": "" - } - } - } -} diff --git a/bloom_lims/config/action/core.json b/bloom_lims/config/action/core.json index ebab8e5..5951321 100644 --- a/bloom_lims/config/action/core.json +++ b/bloom_lims/config/action/core.json @@ -1,7 +1,7 @@ { "set_object_status": { "1.0": { - "action_template": { + "action_definition": { "action_name": "Set Status", "method_name": "do_action_set_object_status", "action_executed": "0", @@ -29,7 +29,7 @@ }, "print_barcode_label": { "1.0": { - "action_template": { + "action_definition": { "action_name": "Print Label", "method_name": "do_action_print_barcode_label", "action_executed": "0", @@ -61,7 +61,7 @@ }, "add-relationships": { "1.0": { - "action_template": { + "action_definition": { "action_name": "Add Parent or Child Relationship(s)", "method_name": "do_action_add_relationships", "action_executed": "0", @@ -85,7 +85,7 @@ }, "create-subject-and-anchor": { "1.0": { - "action_template": { + "action_definition": { "action_name": "Create Subject (Decision Scope)", "method_name": "do_action_create_subject_and_anchor", "action_executed": "0", diff --git a/bloom_lims/config/action/data.json b/bloom_lims/config/action/data.json index 94a5324..ff90d18 100644 --- a/bloom_lims/config/action/data.json +++ b/bloom_lims/config/action/data.json @@ -1,7 +1,7 @@ { "record_cfdna_quant_outcome": { "1.0": { - "action_template": { + "action_definition": { "action_name": "Record Quantification Outcome", "method_name": "do_action_record_cfdna_quant_outcome", "action_executed": "0", @@ -32,7 +32,7 @@ }, "log-temperature": { "1.0": { - "action_template": { + "action_definition": { "action_name": "Log Temperature", "method_name": "do_action_log_temperature", "action_executed": "0", diff --git a/bloom_lims/config/action/equipment.json b/bloom_lims/config/action/equipment.json index 402eb30..b634914 100644 --- a/bloom_lims/config/action/equipment.json +++ b/bloom_lims/config/action/equipment.json @@ -1,7 +1,7 @@ { "add-container-to": { "1.0": { - "action_template": { + "action_definition": { "child_workflow_step_obj": {}, "attach_under_root_workflow": {}, "workflow_to_attach": {}, @@ -36,7 +36,7 @@ }, "remove-container-from": { "1.0": { - "action_template": { + "action_definition": { "child_workflow_step_obj": {}, "child_container_obj": {}, "test_requisition_obj": {}, @@ -70,7 +70,7 @@ }, "download-inventory-tsv": { "1.0": { - "action_template": { + "action_definition": { "child_workflow_step_obj": {}, "child_container_obj": {}, "test_requisition_obj": {}, @@ -104,7 +104,7 @@ }, "log-temperature": { "1.0": { - "action_template": { + "action_definition": { "child_workflow_step_obj": {}, "child_container_obj": { "data/generic/temperature-log/*/": { diff --git a/bloom_lims/config/action/file.json b/bloom_lims/config/action/file.json index 0ef8533..c1dff4d 100644 --- a/bloom_lims/config/action/file.json +++ b/bloom_lims/config/action/file.json @@ -1,7 +1,7 @@ { "download_file": { "1.0": { - "action_template": { + "action_definition": { "action_name": "Download A File", "method_name": "do_action_download_file", "action_executed": "0", diff --git a/bloom_lims/config/action/file_set.json b/bloom_lims/config/action/file_set.json index 1f64a88..16f4b9f 100644 --- a/bloom_lims/config/action/file_set.json +++ b/bloom_lims/config/action/file_set.json @@ -1,7 +1,7 @@ { "add-file": { "1.0": { - "action_template": { + "action_definition": { "action_name": "Add File To Set", "method_name": "do_action_add_file_to_file_set", "action_executed": "0", @@ -25,7 +25,7 @@ }, "remove-file": { "1.0": { - "action_template": { + "action_definition": { "action_name": "Remove File From File Set", "method_name": "do_action_remove_file_from_file_set", "action_executed": "0", diff --git a/bloom_lims/config/action/move-queues.json b/bloom_lims/config/action/move-queues.json index e420b63..26f98bb 100644 --- a/bloom_lims/config/action/move-queues.json +++ b/bloom_lims/config/action/move-queues.json @@ -1,7 +1,7 @@ { "move-among-ay-top-queues": { "1.0": { - "action_template": { + "action_definition": { "child_workflow_step_obj": {}, "child_workflow_object": {}, "workflow_step_to_attach_as_child": {}, diff --git a/bloom_lims/config/action/object.json b/bloom_lims/config/action/object.json index 3a09a9c..f8fdbd6 100644 --- a/bloom_lims/config/action/object.json +++ b/bloom_lims/config/action/object.json @@ -1,7 +1,7 @@ { "set-child-object": { "1.0": { - "action_template": { + "action_definition": { "action_name": "Set Object As Child", "method_name": "do_action_set_child_object", "action_executed": "0", diff --git a/bloom_lims/config/action/packages.json b/bloom_lims/config/action/packages.json deleted file mode 100644 index cd76b7b..0000000 --- a/bloom_lims/config/action/packages.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "create_package_and_first_workflow_step": { - "1.0": { - "action_template": { - "action_enabled": "1", - "action_executed": "0", - "action_name": "Register Package", - "action_order": "0", - "action_simple_value": "", - "action_user": [], - "capture_data": "yes", - "captured_data": {}, - "child_workflow_step_obj": { - "workflow_step/accessioning-steps/package-generated/1.0": { - "json_addl": { - "properties": { - "step_number": "1" - } - } - } - }, - "curr_user": "", - "deactivate_actions_when_executed": [], - "description": "Create Package and First Workflow Step", - "executed_datetime": [], - "max_executions": "1", - "method_name": "do_action_create_package_and_first_workflow_step", - "new_container_obj": { - "container/package/generic/*/": { - "json_addl": { - "properties": { - "comments": "x" - } - } - } - }, - "ui_schema": { - "title": "Register Package", - "fields": [] - } - }, - "properties": { - "comments": "", - "name": "" - } - } - }, - "create_package_and_first_workflow_step__generic": { - "1.0": { - "action_template": { - "action_enabled": "1", - "action_executed": "0", - "action_name": "Override Action Name", - "action_order": "0", - "action_simple_value": "", - "action_user": [], - "capture_data": "yes", - "captured_data": {}, - "child_workflow_step_obj": { - "workflow_step/accessioning-steps/package-generated/1.0": { - "json_addl": { - "properties": { - "step_number": "1" - } - } - } - }, - "curr_user": "", - "deactivate_actions_when_executed": [], - "description": "Override Default Description", - "executed_datetime": [], - "max_executions": "1", - "method_name": "do_action_create_package_and_first_workflow_step", - "new_container_obj": { - "container/package/generic/*/": { - "json_addl": { - "properties": { - "comments": "x" - } - } - } - }, - "ui_schema": { - "title": "Override Action Name", - "fields": [] - } - }, - "properties": { - "comments": "", - "name": "" - } - } - } -} diff --git a/bloom_lims/config/action/plate.json b/bloom_lims/config/action/plate.json index 6f3e215..36d64f4 100644 --- a/bloom_lims/config/action/plate.json +++ b/bloom_lims/config/action/plate.json @@ -1,7 +1,7 @@ { "download_plate_map": { "1.0": { - "action_template": { + "action_definition": { "action_enabled": "1", "action_executed": "0", "action_name": "Download Plate Map TSV", @@ -34,7 +34,7 @@ }, "stamp_plates_into_plate": { "1.0": { - "action_template": { + "action_definition": { "action_enabled": "1", "action_executed": "0", "action_name": "Stamp plates into plate", @@ -80,7 +80,7 @@ }, "stamp_plates_into_plate__extraction_plate_filled": { "1.0": { - "action_template": { + "action_definition": { "action_enabled": "1", "action_executed": "0", "action_name": "Add Plates to Plate", diff --git a/bloom_lims/config/action/pool.json b/bloom_lims/config/action/pool.json index efca37c..175a171 100644 --- a/bloom_lims/config/action/pool.json +++ b/bloom_lims/config/action/pool.json @@ -1,7 +1,7 @@ { "create-pool-from-containers": { "1.0": { - "action_template": { + "action_definition": { "action_enabled": "1", "action_executed": "0", "action_name": "Create A Pool", @@ -56,7 +56,7 @@ }, "create_pool_from_containers__extraction_plate_filled": { "1.0": { - "action_template": { + "action_definition": { "action_enabled": "1", "action_executed": "0", "action_name": "Create Pool From Containers", diff --git a/bloom_lims/config/action/test_requisitions.json b/bloom_lims/config/action/test_requisitions.json index f8bc824..fe26581 100644 --- a/bloom_lims/config/action/test_requisitions.json +++ b/bloom_lims/config/action/test_requisitions.json @@ -1,7 +1,7 @@ { "add_container_to_assay_q": { "1.0": { - "action_template": { + "action_definition": { "action_enabled": "1", "action_executed": "0", "action_name": "Add Specimen to Assay Queue", @@ -48,7 +48,7 @@ }, "add_container_to_assay_q__pan_cancer_panel": { "1.0": { - "action_template": { + "action_definition": { "action_enabled": "1", "action_executed": "0", "action_name": "Add Specimen to Assay Queue", @@ -95,7 +95,7 @@ }, "set_verification_state": { "1.0": { - "action_template": { + "action_definition": { "action_enabled": "1", "action_executed": "0", "action_name": "Verify Test Req", diff --git a/bloom_lims/config/action/workflow.json b/bloom_lims/config/action/workflow.json index 6e7ce42..772caaf 100644 --- a/bloom_lims/config/action/workflow.json +++ b/bloom_lims/config/action/workflow.json @@ -1,56 +1,7 @@ { - "create_package_and_first_workflow_step": { - "1.0": { - "action_template": { - "child_workflow_step_obj": { - "workflow_step/accessioning-steps/package-generated/1.0": { - "json_addl": { - "properties": { - "step_number": "1" - } - } - } - }, - "new_container_obj": { - "container/package/generic/*/": { - "json_addl": { - "properties": { - "comments": "x" - } - } - } - }, - "action_name": "Register Package", - "method_name": "do_action_create_package_and_first_workflow_step", - "action_executed": "0", - "max_executions": "1", - "action_enabled": "1", - "capture_data": "yes", - "captured_data": {}, - "deactivate_actions_when_executed": [], - "executed_datetime": [], - "action_order": "0", - "action_simple_value": "", - "action_user": [], - "curr_user": "", - "printer_opts": { - "printer_name": "", - "label_style": "" - }, - "ui_schema": { - "title": "Register Package", - "fields": [] - } - }, - "properties": { - "name": "", - "comments": "" - } - } - }, "destroy_specimen_containers": { "1.0": { - "action_template": { + "action_definition": { "child_workflow_step_obj": { "workflow_step/destroy-steps/specimen-containers-destroyed/1.0": { "json_addl": { diff --git a/bloom_lims/config/action/workflow_step_accessioning.json b/bloom_lims/config/action/workflow_step_accessioning.json deleted file mode 100644 index ac58a55..0000000 --- a/bloom_lims/config/action/workflow_step_accessioning.json +++ /dev/null @@ -1,310 +0,0 @@ -{ - "create_child_container_and_link_child_workflow_step": { - "1.0": { - "action_template": { - "action_enabled": "1", - "action_executed": "0", - "action_name": "Register Kit", - "action_user": [], - "capture_data": "yes", - "captured_data": {}, - "child_container_obj": { - "container/kit/generic/*/": { - "json_addl": { - "properties": { - "comments": "" - } - } - } - }, - "child_workflow_step_obj": { - "workflow_step/accessioning-steps/kit-generated/*/": { - "json_addl": { - "properties": { - "comments": "" - } - } - } - }, - "curr_user": "", - "deactivate_actions_when_executed": [], - "executed_datetime": [], - "max_executions": "-1", - "method_name": "do_action_create_child_container_and_link_child_workflow_step", - "printer_opts": { - "label_style": "", - "printer_name": "" - }, - "simple_value": "", - "ui_schema": { - "title": "Register Kit", - "fields": [] - } - }, - "properties": { - "comments": "", - "name": "" - } - } - }, - "create_child_container_and_link_child_workflow_step__package_generated": { - "1.0": { - "action_template": { - "action_enabled": "1", - "action_executed": "0", - "action_name": "Register Kit", - "action_user": [], - "capture_data": "yes", - "captured_data": {}, - "child_container_obj": { - "container/kit/generic/*/": { - "json_addl": { - "properties": { - "comments": "" - } - } - } - }, - "child_workflow_step_obj": { - "workflow_step/accessioning-steps/kit-generated/*/": { - "json_addl": { - "properties": { - "comments": "" - } - } - } - }, - "curr_user": "", - "deactivate_actions_when_executed": [], - "deactivate_when_executed": [ - "action/core/print_barcode_label/1.0" - ], - "executed_datetime": [], - "max_executions": "-1", - "method_name": "do_action_create_child_container_and_link_child_workflow_step", - "printer_opts": { - "label_style": "", - "printer_name": "" - }, - "simple_value": "", - "ui_schema": { - "title": "Register Kit", - "fields": [] - } - }, - "properties": { - "comments": "", - "name": "" - } - } - }, - "create_child_container_and_link_child_workflow_step_specimen": { - "1.0": { - "action_template": { - "action_enabled": "1", - "action_executed": "0", - "action_name": "Register Specimen Container", - "action_user": [], - "capture_data": "yes", - "captured_data": {}, - "child_container_obj": { - "container/tube/tube-strek-10ml/*/": { - "instantiation_layouts": [ - [ - { - "content/specimen/blood-whole/*/": { - "json_addl": { - "properties": { - "comment": "" - } - } - } - } - ] - ], - "json_addl": { - "properties": { - "comments": "" - } - } - } - }, - "child_workflow_step_obj": { - "workflow_step/accessioning-steps/container-generated/*/": { - "json_addl": { - "properties": { - "comments": "" - } - } - } - }, - "curr_user": "", - "deactivate_actions_when_executed": [], - "executed_datetime": [], - "max_executions": "-1", - "method_name": "do_action_create_child_container_and_link_child_workflow_step", - "printer_opts": { - "label_style": "", - "printer_name": "" - }, - "simple_value": "", - "ui_schema": { - "title": "Register Specimen Container", - "fields": [] - } - }, - "properties": { - "comments": "", - "name": "" - } - } - }, - "create_test_req_and_link_child_workflow_step_dup": { - "1.0": { - "action_template": { - "action_enabled": "1", - "action_executed": "0", - "action_name": "Create New Test Req & Assoc To Specimen Tube", - "action_order": "0", - "action_simple_value": "", - "action_user": [], - "capture_data": "yes", - "captured_data": {}, - "child_container_obj": {}, - "child_workflow_step_obj": { - "workflow_step/accessioning-steps/test-requisition-generated2/*/": { - "json_addl": { - "properties": { - "comments": "" - } - } - } - }, - "curr_user": "", - "deactivate_actions_when_executed": [ - "action/workflow_step_accessioning/create_test_req_and_link_child_workflow_step_dup/1.0" - ], - "executed_datetime": [], - "max_executions": "1", - "method_name": "do_action_create_test_req_and_link_child_workflow_step", - "printer_opts": { - "label_style": "", - "printer_name": "" - }, - "test_requisition_obj": { - "test_requisition/clinical/pan-cancer-panel/1.0/": { - "json_addl": { - "properties": { - "comments": "" - } - } - } - }, - "ui_schema": { - "title": "Create New Test Req & Assoc To Specimen Tube", - "fields": [] - } - } - } - }, - "ycreate_test_req_and_link_child_workflow_step": { - "1.0": { - "action_template": { - "action_enabled": "1", - "action_executed": "0", - "action_name": "Link Tube To Existing Req.b", - "action_order": "0", - "action_simple_value": "", - "action_user": [], - "capture_data": "yes", - "captured_data": {}, - "child_container_obj": {}, - "child_workflow_step_obj": { - "workflow_step/accessioning-steps/test-requisition-generated2/*/": { - "json_addl": { - "properties": { - "comments": "" - } - } - } - }, - "curr_user": "", - "deactivate_actions_when_executed": [], - "executed_datetime": [], - "max_executions": "-1", - "method_name": "do_action_ycreate_test_req_and_link_child_workflow_step", - "printer_opts": { - "label_style": "", - "printer_name": "" - }, - "test_requisition_obj": { - "test_requisition/clinical/pan-cancer-panel/1.0/": { - "json_addl": { - "properties": { - "comments": "" - } - } - } - }, - "ui_schema": { - "title": "Link Tube To Existing Req.b", - "fields": [] - } - }, - "properties": { - "comments": "", - "name": "" - } - } - }, - "ycreate_test_req_and_link_child_workflow_step2": { - "1.0": { - "action_template": { - "action_enabled": "1", - "action_executed": "0", - "action_name": "Link Tube To Existing Req.b", - "action_order": "0", - "action_simple_value": "", - "action_user": [], - "capture_data": "yes", - "captured_data": {}, - "child_container_obj": {}, - "child_workflow_step_obj": { - "workflow_step/accessioning-steps/test-requisition-generated2/*/": { - "json_addl": { - "properties": { - "comments": "" - } - } - } - }, - "curr_user": "", - "deactivate_actions_when_executed": [], - "executed_datetime": [], - "max_executions": "-1", - "method_name": "do_action_ycreate_test_req_and_link_child_workflow_step", - "printer_opts": { - "label_style": "", - "printer_name": "" - }, - "test_requisition_obj": { - "test_requisition/clinical/pan-cancer-panel/1.0/": { - "json_addl": { - "properties": { - "comments": "" - } - } - } - }, - "ui_schema": { - "title": "Link Tube To Existing Req.b", - "fields": [] - } - }, - "properties": { - "comments": "", - "name": "" - } - } - } -} diff --git a/bloom_lims/config/action/workflow_step_analysis.json b/bloom_lims/config/action/workflow_step_analysis.json index 2522352..a0fbe97 100644 --- a/bloom_lims/config/action/workflow_step_analysis.json +++ b/bloom_lims/config/action/workflow_step_analysis.json @@ -1,7 +1,7 @@ { "download_quant_data": { "1.0": { - "action_template": { + "action_definition": { "child_workflow_step_obj": {}, "child_container_obj": {}, "child_data_obj": {}, diff --git a/bloom_lims/config/action/workflow_step_destroy.json b/bloom_lims/config/action/workflow_step_destroy.json index 81406cd..bab22d9 100644 --- a/bloom_lims/config/action/workflow_step_destroy.json +++ b/bloom_lims/config/action/workflow_step_destroy.json @@ -1,7 +1,7 @@ { "complete_specimen_destroy": { "1.0": { - "action_template": { + "action_definition": { "child_workflow_step_obj": {}, "child_container_obj": {}, "action_name": "Reverse Destroy", diff --git a/bloom_lims/config/action/workflow_step_extraction.json b/bloom_lims/config/action/workflow_step_extraction.json index d3dbfb2..b6fe75c 100644 --- a/bloom_lims/config/action/workflow_step_extraction.json +++ b/bloom_lims/config/action/workflow_step_extraction.json @@ -1,7 +1,7 @@ { "cfdna_post_extraction_quant": { "1.0": { - "action_template": { + "action_definition": { "child_workflow_step_obj": { "workflow_step/extraction-steps/cfdna-quantified/*/": { "json_addl": { diff --git a/bloom_lims/config/action/workflow_step_plate_operations.json b/bloom_lims/config/action/workflow_step_plate_operations.json index 4ef42ec..8329726 100644 --- a/bloom_lims/config/action/workflow_step_plate_operations.json +++ b/bloom_lims/config/action/workflow_step_plate_operations.json @@ -1,7 +1,7 @@ { "stamp_copy_plate": { "1.0": { - "action_template": { + "action_definition": { "child_workflow_step_obj": { "workflow_step/plate-operations/stamp-copy-plate/*/": { "json_addl": { @@ -42,7 +42,7 @@ }, "cfdna_quant": { "1.0": { - "action_template": { + "action_definition": { "child_workflow_step_obj": { "workflow_step/analysis/gdna-quantified/*/": { "json_addl": { diff --git a/bloom_lims/config/action/workflow_step_queue.json b/bloom_lims/config/action/workflow_step_queue.json index 25b4446..010fbb3 100644 --- a/bloom_lims/config/action/workflow_step_queue.json +++ b/bloom_lims/config/action/workflow_step_queue.json @@ -1,7 +1,7 @@ { "link_tubes_auto": { "1.0": { - "action_template": { + "action_definition": { "child_workflow_step_obj": { "workflow_step/queue/extraction-queue-available/*/": { "json_addl": { @@ -60,7 +60,7 @@ }, "create_test_req_and_link_child_workflow_step": { "1.0": { - "action_template": { + "action_definition": { "child_workflow_step_obj": { "workflow_step/accessioning-steps/test-requisition-generated2/*/": { "json_addl": { @@ -112,7 +112,7 @@ }, "ycreate_test_req_and_link_child_workflow_step": { "1.0": { - "action_template": { + "action_definition": { "child_workflow_step_obj": { "workflow_step/accessioning-steps/test-requisition-generated2/*/": { "json_addl": { @@ -162,7 +162,7 @@ }, "fill_plate_undirected": { "1.0": { - "action_template": { + "action_definition": { "child_workflow_step_obj": { "workflow_step/plate-operations/extraction-plate-filled/*/": { "json_addl": { @@ -203,7 +203,7 @@ }, "fill_plate_directed": { "1.0": { - "action_template": { + "action_definition": { "child_workflow_step_obj": { "workflow_step/plate-operations/plate-filled/*/": { "json_addl": { @@ -240,7 +240,7 @@ }, "add-to-blood-gdna-extraction-queue": { "1.0": { - "action_template": { + "action_definition": { "action_name": "Add to Blood to gDNA Extraction Queue", "method_name": "do_action_move_instances_to_queue", "target_queue_subtype": "blood-to-gdna-extraction-eligible", @@ -271,7 +271,7 @@ }, "add-to-buccal-gdna-extraction-queue": { "1.0": { - "action_template": { + "action_definition": { "action_name": "Add to Buccal to gDNA Extraction Queue", "method_name": "do_action_move_instances_to_queue", "target_queue_subtype": "buccal-to-gdna-extraction-eligible", @@ -302,7 +302,7 @@ }, "add-gdna-to-gdna-normalization-queue": { "1.0": { - "action_template": { + "action_definition": { "action_name": "Add gDNA to gDNA Normalization Queue", "method_name": "do_action_move_instances_to_queue", "target_queue_subtype": "input-gdna-normalization-eligible", @@ -333,7 +333,7 @@ }, "plate-create-fill-auto": { "1.0": { - "action_template": { + "action_definition": { "action_name": "Plate Create & Fill Auto", "method_name": "do_action_plate_create_fill_auto", "action_executed": "0", @@ -363,7 +363,7 @@ }, "plate-create-fill-directed": { "1.0": { - "action_template": { + "action_definition": { "action_name": "Plate Create & Fill Directed", "method_name": "do_action_plate_create_fill_directed", "action_executed": "0", @@ -393,7 +393,7 @@ }, "existing-plate-fill-auto": { "1.0": { - "action_template": { + "action_definition": { "action_name": "Existing Plate Fill Auto", "method_name": "do_action_existing_plate_fill_auto", "action_executed": "0", @@ -423,7 +423,7 @@ }, "existing-plate-fill-directed": { "1.0": { - "action_template": { + "action_definition": { "action_name": "Existing Plate Fill Directed", "method_name": "do_action_existing_plate_fill_directed", "action_executed": "0", @@ -453,7 +453,7 @@ }, "save-quant-data": { "1.0": { - "action_template": { + "action_definition": { "action_name": "Save Quant Data", "method_name": "do_action_save_quant_data", "action_executed": "0", diff --git a/bloom_lims/config/container/kit.json b/bloom_lims/config/container/kit.json deleted file mode 100644 index 4573df7..0000000 --- a/bloom_lims/config/container/kit.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "generic": { - "1.0": { - "action_groups": {}, - "action_imports": { - "add_relationships": "action/core/add-relationships/1.0", - "create_subject_and_anchor": "action/core/create-subject-and-anchor/1.0", - "print_barcode_label": "action/core/print_barcode_label/1.0", - "set_object_status": "action/core/set_object_status/1.0" - }, - "cogs": { - "allocation_type": "", - "cost": "10.00", - "cost_split_by_children": [ - { - "*/*/*/*": {} - } - ], - "state": "active" - }, - "description": "Collection Kit", - "expected_inputs": [], - "expected_outputs": [], - "instantiation_layouts": [], - "properties": { - "comments": "", - "lab_code": "", - "name": "Collection Kit" - }, - "singleton": "0" - } - } -} diff --git a/bloom_lims/config/container/package.json b/bloom_lims/config/container/package.json deleted file mode 100644 index 2f521e1..0000000 --- a/bloom_lims/config/container/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "generic": { - "1.0": { - "action_groups": {}, - "action_imports": { - "add_relationships": "action/core/add-relationships/1.0", - "create_subject_and_anchor": "action/core/create-subject-and-anchor/1.0", - "print_barcode_label": "action/core/print_barcode_label/1.0", - "set_object_status": "action/core/set_object_status/1.0" - }, - "cogs": { - "allocation_type": "", - "cost": "30.00", - "cost_split_by_children": [ - { - "*/*/*/*": {} - } - ], - "state": "active" - }, - "description": "Package", - "expected_inputs": [], - "expected_outputs": [], - "instantiation_layouts": [], - "properties": { - "comments": "", - "lab_code": "", - "name": "Package" - }, - "singleton": "0" - } - } -} diff --git a/bloom_lims/config/workflow/accessioning.json b/bloom_lims/config/workflow/accessioning.json deleted file mode 100644 index 3df1d4e..0000000 --- a/bloom_lims/config/workflow/accessioning.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "accession-package-kit-tubes-testreq": { - "1.0": { - "action_groups": {}, - "action_imports": { - "add_relationships": "action/core/add-relationships/1.0", - "create_package_and_first_workflow_step": "action/workflow/create_package_and_first_workflow_step/1.0", - "create_subject_and_anchor": "action/core/create-subject-and-anchor/1.0", - "print_barcode_label": "action/core/print_barcode_label/1.0", - "set_object_status": "action/core/set_object_status/1.0" - }, - "cogs": { - "allocation_type": "", - "cost": "0.00", - "cost_split_by_children": [ - { - "*/*/*/*": {} - } - ], - "state": "active" - }, - "description": "Carrier Package, Kit, Specimen Container, Test Requisition Accessioning & Specimen Assay Queue Registration", - "expected_inputs": [], - "expected_outputs": [], - "instantiation_layouts": [], - "properties": { - "comments": "", - "lab_code": "", - "name": "Carrier Package, Kit, Specimen Container, Test Requisition Accessioning & Specimen Assay Queue Registration" - }, - "singleton": "0" - } - } -} diff --git a/bloom_lims/config/workflow_step/accessioning-steps.json b/bloom_lims/config/workflow_step/accessioning-steps.json deleted file mode 100644 index 10ba78b..0000000 --- a/bloom_lims/config/workflow_step/accessioning-steps.json +++ /dev/null @@ -1,210 +0,0 @@ -{ - "container-generated": { - "1.0": { - "action_groups": {}, - "action_imports": { - "add_relationships": "action/core/add-relationships/1.0", - "create_subject_and_anchor": "action/core/create-subject-and-anchor/1.0", - "create_test_req_and_link_child_workflow_step_dup": "action/workflow_step_accessioning/create_test_req_and_link_child_workflow_step_dup/1.0", - "print_barcode_label": "action/core/print_barcode_label/1.0", - "set_object_status": "action/core/set_object_status/1.0" - }, - "cogs": { - "allocation_type": "", - "cost": "0.00", - "cost_split_by_children": [ - { - "*/*/*/*": {} - } - ], - "state": "active" - }, - "description": "Register Specimen Container To Test Req // Assay", - "expected_inputs": [ - { - "container/kit/generic/*/": {} - } - ], - "expected_outputs": [ - { - "container/tube/tube-strek-10ml/*/": { - "json_addl": { - "properties": { - "comments": "" - } - } - } - } - ], - "instantiation_layouts": [], - "properties": { - "comments": "", - "lab_code": "", - "name": "Register Specimen Containers To Test Req // Assay", - "step_number": "" - }, - "singleton": "0", - "step_properties": { - "comments": "", - "end_operator": "", - "end_timestamp": "", - "start_operator": "", - "start_timestamp": "", - "step_number": "" - } - } - }, - "kit-generated": { - "1.0": { - "action_groups": {}, - "action_imports": { - "add_relationships": "action/core/add-relationships/1.0", - "create_child_container_and_link_child_workflow_step_specimen": "action/workflow_step_accessioning/create_child_container_and_link_child_workflow_step_specimen/1.0", - "create_subject_and_anchor": "action/core/create-subject-and-anchor/1.0", - "print_barcode_label": "action/core/print_barcode_label/1.0", - "set_object_status": "action/core/set_object_status/1.0" - }, - "cogs": { - "allocation_type": "", - "cost": "0.00", - "cost_split_by_children": [ - { - "*/*/*/*": {} - } - ], - "state": "active" - }, - "description": "Register Specimen Containers To Kit", - "expected_inputs": [], - "expected_outputs": [ - { - "container/package/generic/*/": { - "json_addl": { - "properties": { - "carrier_name": "", - "carrier_tracking_number": "" - } - } - } - } - ], - "instantiation_layouts": [], - "properties": { - "carrier_name": "", - "carrier_tracking_number": "", - "comments": "", - "equipment_euid": [], - "lab_code": "", - "name": "Register Specimen Containers To Kit", - "step_number": "" - }, - "singleton": "0", - "step_properties": { - "comments": "", - "end_operator": "", - "end_timestamp": "", - "start_operator": "", - "start_timestamp": "", - "step_number": "" - } - } - }, - "package-generated": { - "1.0": { - "action_groups": {}, - "action_imports": { - "add_relationships": "action/core/add-relationships/1.0", - "create_child_container_and_link_child_workflow_step": "action/workflow_step_accessioning/create_child_container_and_link_child_workflow_step__package_generated/1.0", - "create_subject_and_anchor": "action/core/create-subject-and-anchor/1.0", - "move_among_ay_top_queues": "action/move-queues/move-among-ay-top-queues/1.0", - "print_barcode_label": "action/core/print_barcode_label/1.0", - "set_object_status": "action/core/set_object_status/1.0" - }, - "cogs": { - "allocation_type": "", - "cost": "0.00", - "cost_split_by_children": [ - { - "*/*/*/*": {} - } - ], - "state": "active" - }, - "description": "Register Kits To Package", - "expected_inputs": [], - "expected_outputs": [], - "instantiation_layouts": [], - "properties": { - "comments": "", - "lab_code": "", - "name": "Register Kits To Package", - "step_number": "" - }, - "singleton": "0", - "step_properties": { - "comments": "", - "end_operator": "", - "end_timestamp": "", - "start_operator": "", - "start_timestamp": "", - "step_number": "" - } - } - }, - "test-requisition-generated2": { - "1.0": { - "action_groups": {}, - "action_imports": { - "add_relationships": "action/core/add-relationships/1.0", - "create_subject_and_anchor": "action/core/create-subject-and-anchor/1.0", - "print_barcode_label": "action/core/print_barcode_label/1.0", - "set_object_status": "action/core/set_object_status/1.0", - "ycreate_test_req_and_link_child_workflow_step2": "action/workflow_step_accessioning/ycreate_test_req_and_link_child_workflow_step2/1.0" - }, - "cogs": { - "allocation_type": "", - "cost": "0.00", - "cost_split_by_children": [ - { - "*/*/*/*": {} - } - ], - "state": "active" - }, - "description": "Associate Other Specimen Containers To Test Requisition", - "expected_inputs": [ - { - "container/kit/generic/*/": {} - } - ], - "expected_outputs": [ - { - "container/tube/tube-strek-10ml/*/": { - "json_addl": { - "properties": { - "comments": "" - } - } - } - } - ], - "instantiation_layouts": [], - "properties": { - "comments": "", - "lab_code": "", - "name": "Associate Other Specimen Containers To Test Requisition", - "step_number": "" - }, - "singleton": "0", - "step_properties": { - "comments": "", - "end_operator": "", - "end_timestamp": "", - "name": "", - "start_operator": "", - "start_timestamp": "", - "step_number": "" - } - } - } -} diff --git a/bloom_lims/core/action_execution.py b/bloom_lims/core/action_execution.py index 0a5c0f8..afc515e 100644 --- a/bloom_lims/core/action_execution.py +++ b/bloom_lims/core/action_execution.py @@ -24,7 +24,6 @@ class ActionExecuteRequest: action_group: str action_key: str captured_data: dict[str, Any] - legacy_action_ds: dict[str, Any] | None = None class ActionExecutionError(Exception): @@ -55,27 +54,13 @@ def to_payload(self) -> dict[str, Any]: def normalize_action_execute_payload(payload: dict[str, Any]) -> ActionExecuteRequest: - """Normalize old and new request payloads into a shared execute contract.""" + """Normalize strict action execute payload into a shared execute contract.""" if not isinstance(payload, dict): raise ActionExecutionError(status_code=400, detail="Request body must be a JSON object") - euid = str( - payload.get("euid") - or payload.get("obj_euid") - or payload.get("step_euid") - or "" - ).strip() + euid = str(payload.get("euid") or "").strip() action_group = str(payload.get("action_group") or "").strip() - action_key = str(payload.get("action_key") or payload.get("action") or "").strip() - - captured_data: dict[str, Any] = {} - legacy_action_ds = None - - if isinstance(payload.get("captured_data"), dict): - captured_data = copy.deepcopy(payload.get("captured_data") or {}) - elif isinstance(payload.get("ds"), dict): - legacy_action_ds = copy.deepcopy(payload.get("ds") or {}) - captured_data = copy.deepcopy((legacy_action_ds or {}).get("captured_data") or {}) + action_key = str(payload.get("action_key") or "").strip() if not euid: raise ActionExecutionError(status_code=400, detail="Missing required field: euid", error_fields=["euid"]) @@ -91,13 +76,18 @@ def normalize_action_execute_payload(payload: dict[str, Any]) -> ActionExecuteRe detail="Missing required field: action_key", error_fields=["action_key"], ) + if not isinstance(payload.get("captured_data"), dict): + raise ActionExecutionError( + status_code=400, + detail="Missing required field: captured_data", + error_fields=["captured_data"], + ) return ActionExecuteRequest( euid=euid, action_group=action_group, action_key=action_key, - captured_data=captured_data, - legacy_action_ds=legacy_action_ds, + captured_data=copy.deepcopy(payload.get("captured_data") or {}), ) @@ -177,7 +167,6 @@ def _resolve_action_definition( instance: Any, action_group: str, action_key: str, - legacy_action_ds: dict[str, Any] | None = None, ) -> dict[str, Any]: action_groups = (instance.json_addl or {}).get("action_groups", {}) if not isinstance(action_groups, dict): @@ -194,21 +183,13 @@ def _resolve_action_definition( ) actions = group_data.get("actions", {}) - action_data = actions.get(action_key) + normalized_key = action_key.strip("/") + action_data = actions.get(action_key) or actions.get(normalized_key) if not isinstance(action_data, dict): - if isinstance(legacy_action_ds, dict) and legacy_action_ds: - logger.warning( - "Falling back to client action payload for %s/%s on %s", - action_group, - action_key, - instance.euid, - ) - action_data = copy.deepcopy(legacy_action_ds) - else: - raise ActionExecutionError( - status_code=404, - detail=f"Action not found: {action_key}", - ) + raise ActionExecutionError( + status_code=404, + detail=f"Action not found: {action_key}", + ) resolved = copy.deepcopy(action_data) if not isinstance(resolved.get("captured_data"), dict): @@ -315,7 +296,6 @@ def execute_action_for_instance( instance, request_data.action_group, request_data.action_key, - legacy_action_ds=request_data.legacy_action_ds, ) required_fields = _extract_required_fields_from_ui_schema(action_definition) @@ -338,6 +318,8 @@ def execute_action_for_instance( actor_user_id=actor_user_id, user_preferences=user_preferences, ) + action_ds["action_key"] = request_data.action_key + action_ds["action_group"] = request_data.action_group executor = _resolve_executor(instance, bdb) executor.set_actor_context(user_id=actor_user_id, email=actor_email) diff --git a/bloom_lims/core/template_validation.py b/bloom_lims/core/template_validation.py index 2f1821c..61e4812 100644 --- a/bloom_lims/core/template_validation.py +++ b/bloom_lims/core/template_validation.py @@ -71,7 +71,7 @@ class TemplateValidator: # Required fields for different template types REQUIRED_FIELDS = { - "action": ["action_template"], + "action": ["action_definition"], "workflow": ["action_imports"], "workflow_step": ["action_imports"], } diff --git a/bloom_lims/domain/base.py b/bloom_lims/domain/base.py index f97e355..364828d 100644 --- a/bloom_lims/domain/base.py +++ b/bloom_lims/domain/base.py @@ -606,14 +606,10 @@ def _create_action_ds(self, action_imports): action_key = f"{r.category}/{r.type}/{r.subtype}/{r.version}" action_payload = None if isinstance(r.json_addl, dict): - # Bloom legacy templates store "action_template"; newer TapDB - # core actions use "action_definition". - action_payload = r.json_addl.get("action_template") or r.json_addl.get( - "action_definition" - ) + action_payload = r.json_addl.get("action_definition") if action_payload is None: self.logger.warning( - "Skipping action import %s: no action_template/action_definition", + "Skipping action import %s: missing action_definition", action_key, ) continue @@ -1293,38 +1289,25 @@ def delete_obj(self, obj): # Global Object Actions # def do_action(self, euid, action, action_group, action_ds, now_dt=""): + action_code = str(action_ds.get("action_key") or action or "").strip("/") + parts = [p for p in action_code.split("/") if p] + if len(parts) >= 3: + action_name = parts[2] + elif parts: + action_name = parts[-1] + else: + raise Exception("Missing action key for action execution") + + handler_name = f"do_action_{action_name}" + handler = getattr(self, handler_name, None) + if not callable(handler): + raise Exception(f"Unknown action handler {handler_name}") - r = None - action_method = action_ds["method_name"] now_dt = get_datetime_string() - if action_method == "do_action_set_object_status": - r = self.do_action_set_object_status(euid, action_ds, action_group, action) - elif action_method == "do_action_print_barcode_label": - r = self.do_action_print_barcode_label(euid, action_ds) - - elif action_method == "do_action_destroy_specimen_containers": - r = self.do_action_destroy_specimen_containers(euid, action_ds) - elif action_method == "do_action_create_package_and_first_workflow_step_assay": - r = self.do_action_create_package_and_first_workflow_step_assay( - euid, action_ds - ) - elif action_method == "do_action_move_workset_to_another_queue": - r = self.do_action_move_workset_to_another_queue(euid, action_ds) - elif action_method == "do_stamp_plates_into_plate": - r = self.do_stamp_plates_into_plate(euid, action_ds) - elif action_method == "do_action_download_file": - r = self.do_action_download_file(euid, action_ds) - elif action_method == "do_action_add_file_to_file_set": - r = self.do_action_add_file_to_file_set(euid, action_ds) - elif action_method == "do_action_remove_file_from_file_set": - r = self.do_action_remove_file_from_file_set(euid, action_ds) - elif action_method == "do_action_add_relationships": - r = self.do_action_add_relationships(euid, action_ds) - elif action_method == "do_action_create_subject_and_anchor": - r = self.do_action_create_subject_and_anchor(euid, action_ds) + if handler_name == "do_action_set_object_status": + r = handler(euid, action_ds, action_group, action) else: - raise Exception(f"Unknown do_action method {action_method}") - + r = handler(euid, action_ds) self._do_action_base(euid, action, action_group, action_ds, now_dt) return r diff --git a/bloom_lims/domain/external_specimens.py b/bloom_lims/domain/external_specimens.py index e4c183d..7760478 100644 --- a/bloom_lims/domain/external_specimens.py +++ b/bloom_lims/domain/external_specimens.py @@ -21,6 +21,9 @@ class ExternalSpecimenService: """Creates, updates, and queries external specimens in Bloom.""" + EXTERNAL_REFERENCE_TEMPLATE_CODE = "generic/generic/external_object_link/1.0" + EXTERNAL_REFERENCE_RELATIONSHIP = "has_external_reference" + def __init__(self, *, app_username: str): self.bdb = BLOOMdb3(app_username=app_username) self.bobj = BloomObj(self.bdb) @@ -61,8 +64,6 @@ def create_specimen( specimen_props = self._props(specimen) specimen_props.update(payload.properties or {}) - specimen_props["atlas_refs"] = atlas_refs - specimen_props["atlas_validation"] = atlas_meta if payload.specimen_name: specimen_props["name"] = payload.specimen_name if idempotency_key: @@ -77,6 +78,12 @@ def create_specimen( if container is not None: self._ensure_container_link(container.euid, specimen.euid) + self._replace_external_references( + specimen=specimen, + atlas_refs=atlas_refs, + atlas_validation=atlas_meta, + ) + self.bdb.session.commit() return self._to_response(specimen, created=True) @@ -110,13 +117,13 @@ def update_specimen( specimen_props.update(payload.properties) if payload.specimen_name: specimen_props["name"] = payload.specimen_name + atlas_refs: dict[str, Any] | None = None + atlas_meta: dict[str, Any] | None = None if payload.atlas_refs is not None: atlas_refs, atlas_meta = self._validate_atlas_refs( payload.atlas_refs, container_euid=payload.container_euid, ) - specimen_props["atlas_refs"] = atlas_refs - specimen_props["atlas_validation"] = atlas_meta self._write_props(specimen, specimen_props) if payload.container_euid: @@ -127,6 +134,13 @@ def update_specimen( raise ValueError(f"EUID is not a container: {payload.container_euid}") self._ensure_container_link(container.euid, specimen.euid) + if atlas_refs is not None: + self._replace_external_references( + specimen=specimen, + atlas_refs=atlas_refs, + atlas_validation=atlas_meta or {}, + ) + self.bdb.session.commit() return self._to_response(specimen, created=True) @@ -134,7 +148,7 @@ def find_by_references(self, refs: AtlasReferences) -> list[ExternalSpecimenResp filters = { "order_number": refs.order_number, "patient_id": refs.patient_id, - "shipment_number": refs.shipment_number or refs.package_number, + "shipment_number": refs.shipment_number, "kit_barcode": refs.kit_barcode, } normalized_filters = { @@ -155,9 +169,8 @@ def find_by_references(self, refs: AtlasReferences) -> list[ExternalSpecimenResp ) results: list[ExternalSpecimenResponse] = [] for instance in query: - props = self._props(instance) - atlas_refs = props.get("atlas_refs") - if not isinstance(atlas_refs, dict): + atlas_refs = self._atlas_refs_for_specimen(instance) + if not atlas_refs: continue is_match = True for key, expected in normalized_filters.items(): @@ -238,8 +251,7 @@ def _validate_atlas_refs( payload = { "order_number": refs.order_number, "patient_id": refs.patient_id, - "shipment_number": refs.shipment_number or refs.package_number, - "package_number": refs.package_number, + "shipment_number": refs.shipment_number, "kit_barcode": refs.kit_barcode, } normalized = { @@ -316,8 +328,7 @@ def _validate_refs_against_container_context( context_values = { "order_number": str(order.get("order_number") or "").strip(), "patient_id": str(patient.get("patient_id") or "").strip(), - "shipment_number": str(links.get("shipment_number") or links.get("package_number") or "").strip(), - "package_number": str(links.get("package_number") or "").strip(), + "shipment_number": str(links.get("shipment_number") or "").strip(), "kit_barcode": str(links.get("testkit_barcode") or "").strip(), } @@ -352,14 +363,14 @@ def _build_container_context_summary(self, context: dict[str, Any]) -> dict[str, "order_number": order.get("order_number"), "patient_id": patient.get("patient_id"), "testkit_barcode": links.get("testkit_barcode"), - "package_number": links.get("package_number"), + "shipment_number": links.get("shipment_number"), "test_order_count": len(test_order_ids), "test_order_ids": test_order_ids, } def _to_response(self, specimen, *, created: bool) -> ExternalSpecimenResponse: props = self._props(specimen) - atlas_refs = props.get("atlas_refs") if isinstance(props.get("atlas_refs"), dict) else {} + atlas_refs = self._atlas_refs_for_specimen(specimen) container = self._linked_container_euid(specimen) return ExternalSpecimenResponse( specimen_euid=specimen.euid, @@ -426,3 +437,74 @@ def _safe_get_by_euid(self, euid: str): if "not found" in msg or "no template found" in msg: return None raise + + def _atlas_refs_for_specimen(self, specimen) -> dict[str, str]: + refs: dict[str, str] = {} + for lineage in get_parent_lineages(specimen): + if lineage.is_deleted: + continue + if lineage.relationship_type != self.EXTERNAL_REFERENCE_RELATIONSHIP: + continue + external_ref = lineage.child_instance + if external_ref is None or external_ref.is_deleted: + continue + if ( + external_ref.category != "generic" + or external_ref.type != "generic" + or external_ref.subtype != "external_object_link" + ): + continue + payload = self._props(external_ref) + if str(payload.get("provider") or "").strip() != "atlas": + continue + key = str(payload.get("reference_type") or "").strip() + value = str(payload.get("reference_value") or "").strip() + if key and value: + refs[key] = value + return refs + + def _replace_external_references( + self, + *, + specimen, + atlas_refs: dict[str, Any], + atlas_validation: dict[str, Any], + ) -> None: + existing_refs = [] + for lineage in get_parent_lineages(specimen): + if lineage.is_deleted: + continue + if lineage.relationship_type != self.EXTERNAL_REFERENCE_RELATIONSHIP: + continue + child = lineage.child_instance + if child is None: + continue + existing_refs.append((lineage, child)) + + for lineage, child in existing_refs: + lineage.is_deleted = True + child.is_deleted = True + + for reference_type, reference_value in sorted(atlas_refs.items()): + value = str(reference_value or "").strip() + if not value: + continue + validation_payload = atlas_validation.get(reference_type) + ref_payload = { + "properties": { + "provider": "atlas", + "reference_type": str(reference_type), + "reference_value": value, + "foreign_reference": value, + "validation": validation_payload if isinstance(validation_payload, dict) else {}, + } + } + ref_obj = self.bobj.create_instance_by_code( + self.EXTERNAL_REFERENCE_TEMPLATE_CODE, + {"json_addl": ref_payload}, + ) + self.bobj.create_generic_instance_lineage_by_euids( + specimen.euid, + ref_obj.euid, + relationship_type=self.EXTERNAL_REFERENCE_RELATIONSHIP, + ) diff --git a/bloom_lims/domain/workflows.py b/bloom_lims/domain/workflows.py index 92023b9..5496254 100644 --- a/bloom_lims/domain/workflows.py +++ b/bloom_lims/domain/workflows.py @@ -54,19 +54,7 @@ def create_empty_workflow(self, template_euid): return self.create_instances(template_euid) def do_action(self, wf_euid, action, action_group, action_ds={}): - - action_method = action_ds["method_name"] - now_dt = get_datetime_string() - if action_method == "do_action_create_and_link_child": - self.do_action_create_and_link_child(wf_euid, action_ds, None) - elif action_method == "do_action_create_package_and_first_workflow_step": - self.do_action_create_package_and_first_workflow_step(wf_euid, action_ds) - elif action_method == "do_action_destroy_specimen_containers": - self.do_action_destroy_specimen_containers(wf_euid, action_ds) - else: - return super().do_action(wf_euid, action, action_group, action_ds) - - return self._do_action_base(wf_euid, action, action_group, action_ds, now_dt) + return super().do_action(wf_euid, action, action_group, action_ds) def do_action_destroy_specimen_containers(self, wf_euid, action_ds): wf = self.get_by_euid(wf_euid) @@ -120,62 +108,7 @@ def create_empty_workflow_step(self, template_euid): # feels like it would be better more generalized. For now, most actions being jammed through this approach, even if the parent is now a WFS # . Though... also.... is there benefit to restricting actions to be required to be associated with a WFS? Ask Adam his thoughts. def do_action(self, wfs_euid, action, action_group, action_ds={}): - now_dt = get_datetime_string() - - action_method = action_ds["method_name"] - if action_method == "do_action_create_and_link_child": - self.do_action_create_and_link_child(wfs_euid, action_ds) - elif action_method == "do_action_create_input": - self.do_action_create_input(wfs_euid, action_ds) - elif ( - action_method - == "do_action_create_child_container_and_link_child_workflow_step" - ): - self.do_action_create_child_container_and_link_child_workflow_step( - wfs_euid, action_ds - ) - elif action_method == "do_action_create_test_req_and_link_child_workflow_step": - self.do_action_create_test_req_and_link_child_workflow_step( - wfs_euid, action_ds - ) - elif action_method == "do_action_xcreate_test_req_and_link_child_workflow_step": - self.do_action_xcreate_test_req_and_link_child_workflow_step( - wfs_euid, action_ds - ) - elif action_method == "do_action_ycreate_test_req_and_link_child_workflow_step": - self.do_action_ycreate_test_req_and_link_child_workflow_step( - wfs_euid, action_ds - ) - elif action_method == "do_action_add_container_to_assay_q": - self.do_action_add_container_to_assay_q(wfs_euid, action_ds) - elif action_method == "do_action_move_instances_to_queue": - self.do_action_move_instances_to_queue(wfs_euid, action_ds) - elif action_method == "do_action_plate_create_fill_auto": - self.do_action_plate_create_fill_auto(wfs_euid, action_ds) - elif action_method == "do_action_plate_create_fill_directed": - self.do_action_plate_create_fill_directed(wfs_euid, action_ds) - elif action_method == "do_action_existing_plate_fill_auto": - self.do_action_existing_plate_fill_auto(wfs_euid, action_ds) - elif action_method == "do_action_existing_plate_fill_directed": - self.do_action_existing_plate_fill_directed(wfs_euid, action_ds) - elif action_method == "do_action_save_quant_data": - self.do_action_save_quant_data(wfs_euid, action_ds) - elif action_method == "do_action_fill_plate_undirected": - self.do_action_fill_plate_undirected(wfs_euid, action_ds) - elif action_method == "do_action_fill_plate_directed": - self.do_action_fill_plate_directed(wfs_euid, action_ds) - elif action_method == "do_action_link_tubes_auto": - self.do_action_link_tubes_auto(wfs_euid, action_ds) - elif action_method == "do_action_cfdna_quant": - self.do_action_cfdna_quant(wfs_euid, action_ds) - elif action_method == "do_action_stamp_copy_plate": - self.do_action_stamp_copy_plate(wfs_euid, action_ds) - elif action_method == "do_action_log_temperature": - self.do_action_log_temperature(wfs_euid, action_ds) - else: - return super().do_action(wfs_euid, action, action_group, action_ds) - - return self._do_action_base(wfs_euid, action, action_group, action_ds, now_dt) + return super().do_action(wfs_euid, action, action_group, action_ds) def _add_random_values_to_plate(self, plate): for i in plate.parent_of_lineages: diff --git a/bloom_lims/gui/routes/files.py b/bloom_lims/gui/routes/files.py index d1c4b41..59c3e69 100644 --- a/bloom_lims/gui/routes/files.py +++ b/bloom_lims/gui/routes/files.py @@ -18,7 +18,6 @@ from pathlib import Path from typing import Dict, List -import matplotlib.pyplot as plt import pandas as pd from fastapi import ( APIRouter, @@ -42,6 +41,11 @@ from bloom_lims.gui.deps import require_auth from bloom_lims.gui.jinja import templates +try: + import matplotlib.pyplot as plt +except ModuleNotFoundError: # pragma: no cover - optional GUI dependency + plt = None + router = APIRouter() @@ -765,6 +769,9 @@ async def visual_report(request: Request): import base64 import io + if plt is None: + raise HTTPException(status_code=503, detail="matplotlib is required for visual reports") + file_path = "~/Downloads/dewey_search.tsv" data = pd.read_csv(file_path, sep="\t") @@ -1124,4 +1131,3 @@ async def bulk_create_files_from_tsv(request: Request, file: UploadFile = File(. writer.writerow({**row, **result}) return FileResponse(fin_tsv_path, media_type="text/tab-separated-values") - diff --git a/bloom_lims/integrations/atlas/client.py b/bloom_lims/integrations/atlas/client.py index 486d20b..cd22059 100644 --- a/bloom_lims/integrations/atlas/client.py +++ b/bloom_lims/integrations/atlas/client.py @@ -46,76 +46,17 @@ def __init__( def is_configured(self) -> bool: return bool(self.base_url and self.token) - def _get_with_fallback(self, preferred_path: str, fallback_path: str, *, label: str) -> dict[str, Any]: - try: - return self._get_json(preferred_path) - except AtlasClientError: - logger.warning( - "Atlas preferred lookup path failed for %s; falling back to legacy path (%s -> %s)", - label, - preferred_path, - fallback_path, - ) - return self._get_json(fallback_path) - def get_order(self, order_number: str) -> dict[str, Any]: - return self._get_with_fallback( - f"/api/integrations/bloom/v1/lookups/orders/{order_number}", - f"/api/orders/{order_number}", - label="order lookup", - ) + return self._get_json(f"/api/integrations/bloom/v1/lookups/orders/{order_number}") def get_patient(self, patient_id: str) -> dict[str, Any]: - return self._get_with_fallback( - f"/api/integrations/bloom/v1/lookups/patients/{patient_id}", - f"/api/patients/{patient_id}", - label="patient lookup", - ) + return self._get_json(f"/api/integrations/bloom/v1/lookups/patients/{patient_id}") def get_shipment(self, shipment_number: str) -> dict[str, Any]: - return self._get_with_fallback( - f"/api/integrations/bloom/v1/lookups/shipments/{shipment_number}", - f"/api/shipments/{shipment_number}", - label="shipment lookup", - ) + return self._get_json(f"/api/integrations/bloom/v1/lookups/shipments/{shipment_number}") def get_testkit(self, kit_barcode: str) -> dict[str, Any]: - # Preferred org-scoped integration lookup route. - try: - return self._get_json(f"/api/integrations/bloom/v1/lookups/testkits/{kit_barcode}") - except AtlasClientError: - logger.warning( - "Atlas preferred integration testkit lookup failed; falling back to legacy paths" - ) - - # Secondary fallback direct route if Atlas exposes it. - try: - return self._get_json(f"/api/testkits/{kit_barcode}") - except AtlasClientError: - logger.debug("Direct Atlas testkit endpoint not available, falling back to search") - - payload = { - "query": kit_barcode, - "record_types": ["testkit"], - "filters": {"barcode": [kit_barcode]}, - "page": 1, - "page_size": 10, - } - result = self._post_json("/api/search/v2/query", payload) - items = result.get("items", []) - if not isinstance(items, list): - raise AtlasClientError("Atlas testkit search returned malformed payload") - for item in items: - barcode = ( - item.get("barcode") - or item.get("kit_barcode") - or item.get("properties", {}).get("barcode") - ) - if str(barcode).strip() == str(kit_barcode).strip(): - return item - if items: - return items[0] - raise AtlasClientError(f"Atlas testkit not found for barcode '{kit_barcode}'") + return self._get_json(f"/api/integrations/bloom/v1/lookups/testkits/{kit_barcode}") def get_container_trf_context(self, container_euid: str, tenant_id: str) -> dict[str, Any]: path = f"/api/integrations/bloom/v1/lookups/containers/{container_euid}/trf-context" diff --git a/bloom_lims/schemas/external_specimens.py b/bloom_lims/schemas/external_specimens.py index 061a27b..7a8e1c8 100644 --- a/bloom_lims/schemas/external_specimens.py +++ b/bloom_lims/schemas/external_specimens.py @@ -11,15 +11,8 @@ class AtlasReferences(BaseModel): order_number: str | None = None patient_id: str | None = None shipment_number: str | None = None - package_number: str | None = None kit_barcode: str | None = None - @model_validator(mode="after") - def normalize_package_alias(self) -> "AtlasReferences": - if self.package_number and not self.shipment_number: - self.shipment_number = self.package_number - return self - class ExternalSpecimenCreateRequest(BaseModel): specimen_template_code: str = Field(default="content/specimen/generic/1.0") @@ -38,7 +31,6 @@ def validate_references(self) -> "ExternalSpecimenCreateRequest": refs.order_number, refs.patient_id, refs.shipment_number, - refs.package_number, refs.kit_barcode, ] ): @@ -67,4 +59,3 @@ class ExternalSpecimenResponse(BaseModel): class ExternalSpecimenLookupResponse(BaseModel): items: list[ExternalSpecimenResponse] total: int - diff --git a/tests/test_accessioning_package_flow.py b/tests/test_accessioning_package_flow.py deleted file mode 100644 index 9d03072..0000000 --- a/tests/test_accessioning_package_flow.py +++ /dev/null @@ -1,533 +0,0 @@ -""" -Database-backed regression tests for accessioning package workflow actions. -""" - -import copy -import html -import json -import os -import re -from pathlib import Path - -import pytest -from fastapi.testclient import TestClient - -from bloom_lims.bobjs import BloomWorkflow, BloomWorkflowStep, BloomObj - - -# Ensure GUI route auth bypass works in these endpoint tests. -os.environ["BLOOM_OAUTH"] = "no" -os.environ["BLOOM_DEV_AUTH_BYPASS"] = "true" - -from main import app - - -def _get_accessioning_assay_workflow(bwf: BloomWorkflow): - workflows = bwf.query_instance_by_component_v2( - category="workflow", - type="assay", - subtype="accessioning-RnD", - version="1.0", - ) - if not workflows: - pytest.skip("Missing seeded workflow instance: workflow/assay/accessioning-RnD/1.0") - return workflows[0] - - -def _find_action_by_method(instance, method_name: str): - action_groups = ( - instance.json_addl.get("action_groups", {}) - if isinstance(instance.json_addl, dict) - else {} - ) - for group_name, group_data in action_groups.items(): - actions = group_data.get("actions", {}) if isinstance(group_data, dict) else {} - for action_name, action_ds in actions.items(): - if action_ds.get("method_name") == method_name: - return group_name, action_name, copy.deepcopy(action_ds) - raise AssertionError(f"No action found for method {method_name}") - - -def _find_action(instance, method_name: str = None, action_display_name: str = None): - action_groups = ( - instance.json_addl.get("action_groups", {}) - if isinstance(instance.json_addl, dict) - else {} - ) - for group_name, group_data in action_groups.items(): - actions = group_data.get("actions", {}) if isinstance(group_data, dict) else {} - for action_name, action_ds in actions.items(): - if method_name and action_ds.get("method_name") != method_name: - continue - if action_display_name and action_ds.get("action_name") != action_display_name: - continue - return group_name, action_name, copy.deepcopy(action_ds) - raise AssertionError( - f"No action found for method={method_name!r} action_name={action_display_name!r}" - ) - - -def _run_register_package_action(bwf: BloomWorkflow, workflow): - action_group, action_name, action_ds = _find_action_by_method( - workflow, "do_action_create_package_and_first_workflow_step_assay" - ) - action_ds.setdefault("captured_data", {}) - action_ds["captured_data"]["Carrier Name"] = "FedEx" - action_ds["captured_data"]["Tracking Number"] = "TEST-TRACK-1001" - return bwf.do_action( - workflow.euid, - action=action_name, - action_group=action_group, - action_ds=action_ds, - ) - - -def _get_or_create_assay_workflow_with_step_one(bwfs: BloomWorkflowStep): - def _has_step_one(assay): - for lin in assay.parent_of_lineages: - child = lin.child_instance - if ( - not lin.is_deleted - and child.category == "workflow_step" - and str(child.json_addl.get("properties", {}).get("step_number", "")) == "1" - ): - return True - return False - - assays = bwfs.query_instance_by_component_v2(category="workflow", type="assay") - for assay in assays: - if _has_step_one(assay): - return assay - - assay_templates = bwfs.query_template_by_component_v2(category="workflow", type="assay") - for template in assay_templates: - try: - created = bwfs.create_instances(template.euid) - except Exception: - # Singleton assay templates may already have an existing instance. - continue - assay = created[0][0] - if _has_step_one(assay): - return assay - - pytest.skip("No workflow/assay instance with step_number=1 available for assay queue test") - - -def _get_assay_instance(bobj: BloomObj, subtype: str, version: str): - assays = bobj.query_instance_by_component_v2( - category="workflow", - type="assay", - subtype=subtype, - version=version, - ) - if not assays: - pytest.skip(f"Missing seeded workflow instance workflow/assay/{subtype}/{version}") - return assays[0] - - -def _get_active_queue_step_by_subtype(workflow_obj, subtype: str): - candidates = [] - for lin in workflow_obj.parent_of_lineages: - child = lin.child_instance - if lin.is_deleted: - continue - if child is None or child.is_deleted: - continue - if child.category == "workflow_step" and child.type == "queue" and child.subtype == subtype: - candidates.append(child) - - if not candidates: - return None - - def _sort_key(step_obj): - props = step_obj.json_addl.get("properties", {}) if isinstance(step_obj.json_addl, dict) else {} - step_number = props.get("step_number") - try: - order = int(step_number) - except (TypeError, ValueError): - order = 10**9 - return (order, str(step_obj.euid)) - - candidates.sort(key=_sort_key) - return candidates[0] - - -def _require_queue_template_or_skip(bobj: BloomObj, subtype: str): - templates = bobj.query_template_by_component_v2( - category="workflow_step", - type="queue", - subtype=subtype, - version="1.0", - ) - if not templates: - pytest.skip( - f"Missing workflow_step/queue/{subtype}/1.0 template in DB; run bloom db reset -y && bloom db seed" - ) - - -def _get_assay_workflow_without_steps(bwfs: BloomWorkflowStep): - assays = bwfs.query_instance_by_component_v2(category="workflow", type="assay") - for assay in assays: - has_workflow_step = any( - not lin.is_deleted - and lin.child_instance.category == "workflow_step" - and not lin.child_instance.is_deleted - for lin in assay.parent_of_lineages - ) - if not has_workflow_step: - return assay - pytest.skip("No workflow/assay instance without workflow_step children available") - - -def _get_or_create_test_requisition_with_assay_action(bobj: BloomObj): - test_reqs = bobj.query_instance_by_component_v2( - category="test_requisition", - type="clinical", - ) - for tri in test_reqs: - try: - _find_action_by_method(tri, "do_action_add_container_to_assay_q") - return tri - except AssertionError: - continue - - templates = bobj.query_template_by_component_v2( - category="test_requisition", - type="clinical", - subtype="pan-cancer-panel", - version="1.0", - ) - if not templates: - pytest.skip("Missing clinical test_requisition template with assay queue action") - created = bobj.create_instances(templates[0].euid) - tri = created[0][0] - _find_action_by_method(tri, "do_action_add_container_to_assay_q") - return tri - - -def test_register_package_creates_queue_step_when_missing(bdb_function): - bwf = BloomWorkflow(bdb_function) - workflow = _get_accessioning_assay_workflow(bwf) - - created_step = _run_register_package_action(bwf, workflow) - assert hasattr(created_step, "euid") - - workflow = bwf.get_by_euid(workflow.euid) - queue_steps = [ - lin.child_instance - for lin in workflow.parent_of_lineages - if ( - not lin.is_deleted - and lin.child_instance.category == "workflow_step" - and lin.child_instance.type == "queue" - and lin.child_instance.subtype == "all-purpose" - ) - ] - assert queue_steps, "Register Package should create/link an all-purpose queue step" - - created_step = bwf.get_by_euid(created_step.euid) - assert any( - not lin.is_deleted and lin.parent_instance.euid == queue_steps[0].euid - for lin in created_step.child_of_lineages - ), "New package-generated step should be attached under the queue step" - - packages = [ - lin.child_instance - for lin in created_step.parent_of_lineages - if ( - not lin.is_deleted - and lin.child_instance.category == "container" - and lin.child_instance.type == "package" - ) - ] - assert packages, "Register Package should create and link a package container" - package_props = ( - packages[0].json_addl.get("properties", {}) - if isinstance(packages[0].json_addl, dict) - else {} - ) - assert package_props.get("Tracking Number") == "TEST-TRACK-1001" - - -def test_register_package_output_step_can_advance_accessioning_flow(bdb_function): - bwf = BloomWorkflow(bdb_function) - bwfs = BloomWorkflowStep(bdb_function) - workflow = _get_accessioning_assay_workflow(bwf) - created_step = _run_register_package_action(bwf, workflow) - - step_obj = bwfs.get_by_euid(created_step.euid) - action_group, action_name, action_ds = _find_action_by_method( - step_obj, "do_action_create_child_container_and_link_child_workflow_step" - ) - child_step = bwfs.do_action( - created_step.euid, - action=action_name, - action_group=action_group, - action_ds=action_ds, - ) - assert hasattr(child_step, "euid") - - child_step = bwfs.get_by_euid(child_step.euid) - assert any( - not lin.is_deleted and lin.child_instance.category == "container" - for lin in child_step.parent_of_lineages - ), "Follow-up accessioning step should have a linked container" - - -def test_assays_endpoint_handles_missing_status_buckets(bdb_function): - bwf = BloomWorkflow(bdb_function) - _get_accessioning_assay_workflow(bwf) - - client = TestClient(app, raise_server_exceptions=False) - response = client.get("/assays") - assert response.status_code == 200 - - -def test_workflow_details_renders_descendant_category_chips(bdb_function): - bwf = BloomWorkflow(bdb_function) - workflow = _get_accessioning_assay_workflow(bwf) - _run_register_package_action(bwf, workflow) - - client = TestClient(app, raise_server_exceptions=False) - response = client.get(f"/workflow_details?workflow_euid={workflow.euid}") - assert response.status_code == 200 - assert "descendant-chip" in response.text - assert "chip-label\">total" in response.text - assert 'data-child-category="container"' in response.text - container_icon_chip = re.compile( - r'data-child-category="container"[^>]*>.*?fa-box', - re.S, - ) - assert container_icon_chip.search(response.text) - - -def test_workflow_details_descendant_chips_include_recursive_counts(bdb_function): - bwf = BloomWorkflow(bdb_function) - bwfs = BloomWorkflowStep(bdb_function) - workflow = _get_accessioning_assay_workflow(bwf) - created_step = _run_register_package_action(bwf, workflow) - - step_obj = bwfs.get_by_euid(created_step.euid) - action_group, action_name, action_ds = _find_action_by_method( - step_obj, "do_action_create_child_container_and_link_child_workflow_step" - ) - child_step = bwfs.do_action( - created_step.euid, - action=action_name, - action_group=action_group, - action_ds=action_ds, - ) - - client = TestClient(app, raise_server_exceptions=False) - response = client.get(f"/workflow_details?workflow_euid={workflow.euid}") - assert response.status_code == 200 - - # The parent queue step summary should include recursive tube descendants - # in the type breakdown tooltip. - parent_step_recursive_tube_summary = re.compile( - rf'id="accordion-{re.escape(created_step.euid)}".*?title="[^"]*tube:\s*[0-9]+', - re.S, - ) - assert parent_step_recursive_tube_summary.search(response.text) - - # Parent queue total descendants should be >= child step total descendants. - parent_total_match = re.search( - rf'id="accordion-{re.escape(created_step.euid)}".*?descendant-chip total.*?(\d+)', - response.text, - re.S, - ) - child_total_match = re.search( - rf'id="accordion-{re.escape(child_step.euid)}".*?descendant-chip total.*?(\d+)', - response.text, - re.S, - ) - assert parent_total_match and child_total_match - assert int(parent_total_match.group(1)) >= int(child_total_match.group(1)) - - -def test_euid_details_renders_dynamic_assay_dropdown_from_live_instances(bdb_function): - bobj = BloomObj(bdb_function) - tri = _get_or_create_test_requisition_with_assay_action(bobj) - assay = _get_or_create_assay_workflow_with_step_one(BloomWorkflowStep(bdb_function)) - - client = TestClient(app, raise_server_exceptions=False) - response = client.get(f"/euid_details?euid={tri.euid}") - assert response.status_code == 200 - - action_payloads = re.findall(r'data-action-json="([^"]+)"', response.text) - add_to_queue_action = None - for encoded_payload in action_payloads: - payload = json.loads(html.unescape(encoded_payload)) - if payload.get("method_name") == "do_action_add_container_to_assay_q": - add_to_queue_action = payload - break - - assert add_to_queue_action is not None, "Expected Add Specimen to Assay Queue action payload" - select_markup = add_to_queue_action.get("captured_data", {}).get("___workflow/assay/", "") - assert 'name="assay_selection"' in select_markup - assert f'value="{assay.euid}"' in select_markup - - -def test_add_specimen_to_assay_queue_accepts_assay_euid_selection(bdb_function): - bobj = BloomWorkflowStep(bdb_function) - tri = _get_or_create_test_requisition_with_assay_action(bobj) - assay = _get_or_create_assay_workflow_with_step_one(bobj) - - container_templates = bobj.query_template_by_component_v2( - category="container", - type="tube", - ) - if not container_templates: - pytest.skip("Missing container/tube template for assay queue action test") - container = bobj.create_instances(container_templates[0].euid)[0][0] - - # Required by the action: the container must be child of a clinical test requisition. - bobj.create_generic_instance_lineage_by_euids(tri.euid, container.euid) - bobj.session.commit() - - action_group, action_name, action_ds = _find_action_by_method( - tri, "do_action_add_container_to_assay_q" - ) - action_ds.setdefault("captured_data", {}) - action_ds["captured_data"]["Container EUID"] = container.euid - action_ds["captured_data"]["assay_selection"] = assay.euid - - queued_step = bobj.do_action( - tri.euid, - action=action_name, - action_group=action_group, - action_ds=action_ds, - ) - assert hasattr(queued_step, "euid") - queued_step = bobj.get_by_euid(queued_step.euid) - assert any( - not lin.is_deleted and lin.child_instance.euid == container.euid - for lin in queued_step.parent_of_lineages - ), "Container should be linked into selected assay queue step" - - -def test_add_specimen_to_assay_queue_creates_queue_step_when_assay_has_no_steps( - bdb_function, -): - bobj = BloomWorkflowStep(bdb_function) - tri = _get_or_create_test_requisition_with_assay_action(bobj) - assay = _get_assay_workflow_without_steps(bobj) - - container_templates = bobj.query_template_by_component_v2( - category="container", - type="tube", - ) - if not container_templates: - pytest.skip("Missing container/tube template for assay queue action test") - container = bobj.create_instances(container_templates[0].euid)[0][0] - - bobj.create_generic_instance_lineage_by_euids(tri.euid, container.euid) - bobj.session.commit() - - action_group, action_name, action_ds = _find_action_by_method( - tri, "do_action_add_container_to_assay_q" - ) - action_ds.setdefault("captured_data", {}) - action_ds["captured_data"]["Container EUID"] = container.euid - action_ds["captured_data"]["assay_selection"] = assay.euid - - bobj.do_action( - tri.euid, - action=action_name, - action_group=action_group, - action_ds=action_ds, - ) - assay = bobj.get_by_euid(assay.euid) - queue_steps = [ - lin.child_instance - for lin in assay.parent_of_lineages - if ( - not lin.is_deleted - and lin.child_instance.category == "workflow_step" - and lin.child_instance.type == "queue" - and not lin.child_instance.is_deleted - ) - ] - assert queue_steps, "Fallback queue step should be linked under selected assay workflow" - - queued_step = None - for step in queue_steps: - if any( - not lin.is_deleted and lin.child_instance.euid == container.euid - for lin in step.parent_of_lineages - ): - queued_step = step - break - - assert queued_step is not None, "Container should be linked into fallback queue step" - assert str(queued_step.json_addl.get("properties", {}).get("step_number", "")) == "1" - - -def test_assay_templates_define_extraction_pipeline_layout(bdb_function): - project_root = Path(__file__).resolve().parent.parent - assay_config = json.loads( - (project_root / "bloom_lims/config/workflow/assay.json").read_text(encoding="utf-8") - ) - expected_subtypes = [ - "extraction-batch-eligible", - "blood-to-gdna-extraction-eligible", - "buccal-to-gdna-extraction-eligible", - "input-gdna-normalization-eligible", - "illumina-novaseq-libprep-eligible", - "ont-libprep-eligible", - ] - - for assay_subtype, assay_version in (("hla-typing", "1.2"), ("carrier-screen", "3.9")): - assay_def = assay_config[assay_subtype][assay_version] - layout = assay_def.get("instantiation_layouts", [[]])[0] - queue_subtypes = [] - for entry in layout: - key = list(entry.keys())[0] - parts = key.strip("/").split("/") - if len(parts) >= 4 and parts[0] == "workflow_step" and parts[1] == "queue": - queue_subtypes.append(parts[2]) - - assert queue_subtypes[: len(expected_subtypes)] == expected_subtypes - props = assay_def.get("properties", {}) - assert props.get("library_prep_queue_subtypes") == [ - "illumina-novaseq-libprep-eligible", - "ont-libprep-eligible", - ] - - -def test_add_specimen_routes_to_extraction_batch_queue(bdb_function): - bobj = BloomWorkflowStep(bdb_function) - _require_queue_template_or_skip(bobj, "extraction-batch-eligible") - tri = _get_or_create_test_requisition_with_assay_action(bobj) - assay = _get_assay_instance(bobj, "hla-typing", "1.2") - - container_templates = bobj.query_template_by_component_v2( - category="container", - type="tube", - ) - if not container_templates: - pytest.skip("Missing container/tube template for assay queue routing test") - container = bobj.create_instances(container_templates[0].euid)[0][0] - bobj.create_generic_instance_lineage_by_euids(tri.euid, container.euid) - bobj.session.commit() - - action_group, action_name, action_ds = _find_action_by_method( - tri, "do_action_add_container_to_assay_q" - ) - action_ds.setdefault("captured_data", {}) - action_ds["captured_data"]["Container EUID"] = container.euid - action_ds["captured_data"]["assay_selection"] = assay.euid - - bobj.do_action( - tri.euid, - action=action_name, - action_group=action_group, - action_ds=action_ds, - ) - assay = bobj.get_by_euid(assay.euid) - queued_step = _get_active_queue_step_by_subtype(assay, "extraction-batch-eligible") - assert queued_step is not None - assert any( - not lin.is_deleted and lin.child_instance.euid == container.euid - for lin in queued_step.parent_of_lineages - ) diff --git a/tests/test_action_schema_coverage.py b/tests/test_action_schema_coverage.py index e7e57d7..8fcaad7 100644 --- a/tests/test_action_schema_coverage.py +++ b/tests/test_action_schema_coverage.py @@ -17,15 +17,15 @@ def _iter_action_templates(): for version, data in versions.items(): if not isinstance(data, dict): continue - action_template = data.get("action_template") - if isinstance(action_template, dict): - yield path, action_name, version, action_template + action_definition = data.get("action_definition") + if isinstance(action_definition, dict): + yield path, action_name, version, action_definition def test_every_action_template_has_ui_schema_fields(): missing = [] - for path, action_name, version, action_template in _iter_action_templates(): - ui_schema = action_template.get("ui_schema") + for path, action_name, version, action_definition in _iter_action_templates(): + ui_schema = action_definition.get("ui_schema") fields = ui_schema.get("fields") if isinstance(ui_schema, dict) else None if not isinstance(fields, list): missing.append(f"{path}:{action_name}:{version}: missing ui_schema.fields") @@ -44,8 +44,8 @@ def test_every_action_template_has_ui_schema_fields(): def test_no_legacy_html_captured_data_fields_remain(): offenders = [] - for path, action_name, version, action_template in _iter_action_templates(): - captured_data = action_template.get("captured_data") + for path, action_name, version, action_definition in _iter_action_templates(): + captured_data = action_definition.get("captured_data") if not isinstance(captured_data, dict): continue diff --git a/tests/test_api_actions_execute.py b/tests/test_api_actions_execute.py index 56125db..433d4c4 100644 --- a/tests/test_api_actions_execute.py +++ b/tests/test_api_actions_execute.py @@ -222,6 +222,22 @@ def test_execute_action_missing_required_field_returns_400(client, write_user_ov assert required_field in payload.get("error_fields", []) +def test_execute_action_rejects_legacy_ds_payload(client, write_user_override): + euid, action_group, action_key = _find_any_action_target() + response = client.post( + "/api/v1/actions/execute", + json={ + "euid": euid, + "action_group": action_group, + "action_key": action_key, + "ds": {"captured_data": {}}, + }, + ) + assert response.status_code == 400 + payload = response.json() + assert "captured_data" in payload.get("error_fields", []) + + def test_execute_action_set_object_status_success(client, write_user_override): euid, action_group, action_key, target_status = _find_set_status_target() diff --git a/tests/test_atlas_lookup_resilience.py b/tests/test_atlas_lookup_resilience.py index 3210262..107f23e 100644 --- a/tests/test_atlas_lookup_resilience.py +++ b/tests/test_atlas_lookup_resilience.py @@ -38,23 +38,26 @@ def fake_get(url, headers=None, timeout=None, verify=None): assert calls[0].endswith("/api/integrations/bloom/v1/lookups/orders/ORD-100") -def test_atlas_client_falls_back_to_legacy_lookup_path(monkeypatch): +def test_atlas_client_does_not_fall_back_to_legacy_lookup_path(monkeypatch): calls = [] def fake_get(url, headers=None, timeout=None, verify=None): calls.append(url) - if url.endswith("/api/integrations/bloom/v1/lookups/orders/ORD-101"): - return _FakeResponse(404, {"detail": "not found"}) - return _FakeResponse(200, {"order_number": "ORD-101"}) + return _FakeResponse(404, {"detail": "not found"}) monkeypatch.setattr(atlas_client_mod.requests, "get", fake_get) client = AtlasClient(base_url="https://atlas.example.org", token="tok") - payload = client.get_order("ORD-101") - assert payload["order_number"] == "ORD-101" - assert len(calls) == 2 + try: + client.get_order("ORD-101") + except AtlasClientError as exc: + assert exc.status_code == 404 + assert exc.path == "/api/integrations/bloom/v1/lookups/orders/ORD-101" + else: # pragma: no cover - defensive + raise AssertionError("Expected AtlasClientError for 404 lookup response") + + assert len(calls) == 1 assert calls[0].endswith("/api/integrations/bloom/v1/lookups/orders/ORD-101") - assert calls[1].endswith("/api/orders/ORD-101") def test_atlas_service_returns_stale_cached_payload_on_upstream_error(monkeypatch): diff --git a/tests/test_atlas_workflow_contract.py b/tests/test_atlas_workflow_contract.py index 167574f..baccbe1 100644 --- a/tests/test_atlas_workflow_contract.py +++ b/tests/test_atlas_workflow_contract.py @@ -209,6 +209,8 @@ def test_create_specimen_with_existing_container_contract(monkeypatch): assert specimen["specimen_euid"] assert specimen["container_euid"] == container["euid"] assert specimen["atlas_refs"]["order_number"].startswith("ORD-") + assert "atlas_refs" not in specimen["properties"] + assert "atlas_validation" not in specimen["properties"] def test_container_context_validation_mismatch_returns_400(monkeypatch): @@ -255,7 +257,7 @@ def get_container_trf_context(self, container_euid: str, *, tenant_id: str | Non assert "mismatch" in response.json()["detail"].lower() -def test_container_context_summary_persisted_in_validation_metadata(monkeypatch): +def test_container_context_summary_is_projected_through_explicit_reference_objects(monkeypatch): class _ContextMatchingAtlasService(_FakeAtlasLookupService): def get_container_trf_context(self, container_euid: str, *, tenant_id: str | None = None): _ = container_euid @@ -266,7 +268,7 @@ def get_container_trf_context(self, container_euid: str, *, tenant_id: str | Non "order": {"order_number": "ORD-MATCH"}, "patient": {"patient_id": "PAT-MATCH"}, "test_orders": [{"test_order_id": "TO-1"}, {"test_order_id": "TO-2"}], - "links": {"testkit_barcode": "KIT-MATCH", "package_number": "PKG-MATCH"}, + "links": {"testkit_barcode": "KIT-MATCH", "shipment_number": "SHIP-MATCH"}, }, from_cache=False, stale=False, @@ -290,18 +292,18 @@ def get_container_trf_context(self, container_euid: str, *, tenant_id: str | Non "order_number": "ORD-MATCH", "patient_id": "PAT-MATCH", "kit_barcode": "KIT-MATCH", - "package_number": "PKG-MATCH", + "shipment_number": "SHIP-MATCH", }, }, idempotency_key=_opaque("idem", 16), ) - validation = created["properties"].get("atlas_validation", {}) - assert "container_trf_context" in validation - summary = validation["container_trf_context"]["summary"] - assert summary["order_number"] == "ORD-MATCH" - assert summary["patient_id"] == "PAT-MATCH" - assert summary["test_order_count"] == 2 + assert created["atlas_refs"]["order_number"] == "ORD-MATCH" + assert created["atlas_refs"]["patient_id"] == "PAT-MATCH" + assert created["atlas_refs"]["kit_barcode"] == "KIT-MATCH" + assert created["atlas_refs"]["shipment_number"] == "SHIP-MATCH" + assert "atlas_refs" not in created["properties"] + assert "atlas_validation" not in created["properties"] def test_create_specimen_auto_container_contract(monkeypatch): diff --git a/tests/test_create_acc_workflows.py b/tests/test_create_acc_workflows.py deleted file mode 100644 index 21d6a07..0000000 --- a/tests/test_create_acc_workflows.py +++ /dev/null @@ -1,318 +0,0 @@ -import pytest -from bloom_lims.db import BLOOMdb3 -from bloom_lims.bobjs import BloomWorkflow, BloomWorkflowStep -import sys - - -def set_status(b, obj, status): - action = "action/core/set_object_status/1.0" - action_group = "core" - - action_ds = obj.json_addl["action_groups"][action_group]["actions"][action] - action_ds["captured_data"]["object_status"] = status - b.do_action(obj.euid, action, action_group, action_ds) - - def create_tubes(n=1): - ctr = 0 - while ctr < n: - - #records = ( - # bob_wfs.session.query(bob_wfs.Base.classes.workflow_template) - # .filter( - # bob_wfs.Base.classes.workflow_template.subtype - # == "accession-package-kit-tubes-testreq" - # ) - # .all() - #) - - #wf = bob_wfs.create_instances(records[0].euid)[0][0] - - # Clinical ACC Queue - # accessioning-RnD - - #from IPython import embed; embed() - #raise - # - # - wf = bob_wf.query_instance_by_component_v2("workflow", "assay", "accessioning-RnD", "1.0")[0] - - action_group = "accessioning" - action = "action/accessioning-ay/create_package_and_first_workflow_step_assay_root/1.0" - action_data = wf.json_addl["action_groups"][action_group]["actions"][action] - # action_data = wf.json_addl["actions"]["create_package_and_first_workflow_step"] - action_data["captured_data"]["Tracking Number"] = "1001897582860000245100773464327825" - action_data["captured_data"]["Fedex Tracking Data"] = {} - - - wfs=bob_wf.do_action(wf.euid, action, action_group, action_data) - set_status(bob_wfs, wfs, "in_progress") - - #wfs = bob_wf.do_action_create_package_and_first_workflow_step_assay(wf.euid, action_data) - - assert hasattr(wfs, "euid") == True - - b_action_group = "create_child" # change to child - #create_package_and_first_workflow_step_assay - b_action = "action/workflow_step_accessioning/create_child_container_and_link_child_workflow_step/1.0" # "action/workflow_step_accessioning/create_child_container_and_link_child_workflow_step/1.0" - wfs_action_data = wfs.json_addl["action_groups"][b_action_group]["actions"][b_action] - - wfs_action_data["captured_data"]["Tracking Number"] = "1001897582860000245100773464327825" - wfs_action_data["captured_data"]["Fedex Tracking Data"] = {} - bob_wfs.do_action(wfs.euid, b_action, b_action_group, wfs_action_data) - - - child_wfs = "" - for i in wfs.parent_of_lineages: - if i.child_instance.type == "accessioning-steps": - child_wfs = i.child_instance - - assert hasattr(child_wfs, "euid") == True - c_action_group = "specimen_actions" # change to child - c_action = "action/workflow_step_accessioning/create_child_container_and_link_child_workflow_step_specimen/1.0" - c_wfs_action_data = child_wfs.json_addl["action_groups"][c_action_group]["actions"][ - c_action - ] - - bob_wfs.do_action(child_wfs.euid, c_action, c_action_group, c_wfs_action_data) - set_status(bob_wfs, wfs, "complete") - - set_status(bob_wfs, child_wfs, "in_progress") - - new_child_wfs = "" - for i in child_wfs.parent_of_lineages: - if i.child_instance.category == "workflow_step": - new_child_wfs = i.child_instance - assert hasattr(new_child_wfs, "euid") == True - - - - - trf_wfs = bob_wfs.do_action( - new_child_wfs.euid, - "action/workflow_step_accessioning/create_test_req_and_link_child_workflow_step_dup/1.0", - "test_req", - new_child_wfs.json_addl["action_groups"]["test_req"]["actions"][ - "action/workflow_step_accessioning/create_test_req_and_link_child_workflow_step_dup/1.0" - ], - ) - set_status(bob_wfs, new_child_wfs, "in_progress") - set_status(bob_wfs, child_wfs, "complete") - - - assert hasattr(trf_wfs, "euid") == True - - - trf_child_wfs = "" - trf_child_cont = "" - for i in trf_wfs.parent_of_lineages: - if i.child_instance.category == "workflow_step": - trf_child_wfs = i.child_instance - if i.child_instance.category == "container": - trf_child_cont = i.child_instance - - trf = "" - for i in trf_child_cont.child_of_lineages: - if i.parent_instance.category == "test_requisition": - trf = i.parent_instance - - - trf_assay_data = trf.json_addl["action_groups"]["test_requisitions"]["actions"][ - "action/test_requisitions/add_container_to_assay_q/1.0" - ] - # This is super brittle, how I am currently linking Assay to TestReq... - # = tr.json_addl["actions"]["add_container_to_assay_q"] - trf_assay_data["captured_data"]["assay_selection"] = ASSAY - trf_assay_data["captured_data"]["Container EUID"] = trf_child_cont.euid - - wfs_queue = bob_wfs.do_action( - trf.euid, - action_group="test_requisitions", - action="action/test_requisitions/add_container_to_assay_q/1.0", - action_ds=trf_assay_data, - ) - assert hasattr(wfs_queue, "euid") == True - set_status(bob_wfs, wfs_queue, "in_progress") - - - scanned_bcs = trf_child_cont.euid - - ctr = ctr + 1 - print('CCCCCCCCCCC',ctr) - TUBES.append(trf_child_cont.euid) - - - - wset_q = wfs - wset_q_axn = "action/move-queues/move-among-ay-top-queues/1.0" - wset_q_axn_grp = "acc-queue-move" - wset_q_ad = wset_q.json_addl["action_groups"][wset_q_axn_grp][ - "actions"][wset_q_axn] - wset_q_ad["captured_data"]["q_selection"] = "workflow_step/queue/plasma-isolation-queue-exception/1.0" if randint(0,13) > 10 else "workflow_step/queue/plasma-isolation-queue-removed/1.0" - bob_wfs.do_action( - wset_q.euid, - action_group=wset_q_axn_grp, - action=wset_q_axn, - action_ds=wset_q_ad - ) - - - def fill_plates(tubes=[]): - # Create some controls to add to the plate! - giab_cx, giab_mx = bob_wf.create_container_with_content( - ("container", "tube", "tube-generic-10ml", "1.0"), - ("content", "control", "giab-HG002", "1.0") - ) - - cfsynctl_cx, cfsynctl_mx = bob_wf.create_container_with_content( - ("container", "tube", "tube-10ml-glass", "1.0"), - ("content", "control", "synthetic-cfdna", "1.0") - ) - - ntc_cx, ntc_mx = bob_wf.create_container_with_content( - ("container", "tube", "tube-eppi-1.5ml", "1.0"), - ("content", "control", "water-ntc", "1.0") - ) - trf_child_cont = bob_wf.get_by_euid(tubes[-1]) - tubes.append(giab_cx.euid) - tubes.append(cfsynctl_cx.euid) - tubes.append(ntc_cx.euid) - - scanned_bcs = "\n".join(tubes) - q_wfs = "" - for i in trf_child_cont.child_of_lineages: - if i.parent_instance.subtype == "plasma-isolation-queue-available": - q_wfs = i.parent_instance - - piso_q_action_data = q_wfs.json_addl["action_groups"]["tube_xfer"]["actions"][ - "action/workflow_step_queue/link_tubes_auto/1.0" - ] - piso_q_action_data["captured_data"]["discard_barcodes"] = scanned_bcs - wfs_plasma = bob_wfs.do_action( - q_wfs.euid, - action_group="tube_xfer", - action="action/workflow_step_queue/link_tubes_auto/1.0", - action_ds=piso_q_action_data, - ) # _link_tubes_auto(wfs_queue.euid, piso_q_action_data) - - plasma_cont = None - for i in trf_child_cont.parent_of_lineages: - if i.child_instance.subtype == "tube-generic-10ml": - plasma_cont = i.child_instance - scanned_bcs_plasma = plasma_cont.euid - - for i in plasma_cont.child_of_lineages: - if i.parent_instance.category == "workflow_step": - pi_wfs = i.parent_instance - - wfset_wf = pi_wfs.child_of_lineages[0].parent_instance - - action_ds_plasma = pi_wfs.json_addl["action_groups"]["fill_plate"]["actions"][ - "action/workflow_step_queue/fill_plate_undirected/1.0" - ] - # wfs_plasma.json_addl["actions"]["fill_plate_undirected"] - action_ds_plasma["captured_data"]["discard_barcodes"] = scanned_bcs_plasma - wfs_plt = bob_wfs.do_action( - pi_wfs.euid, - action_group="fill_plate", - action="action/workflow_step_queue/fill_plate_undirected/1.0", - action_ds=action_ds_plasma, - ) - set_status(bob_wfs, pi_wfs, "complete") - - ### ENDING WITH ANEXTRACTION PLATE! Need to check quant and add more from here. - - plt_fill_wfs = "" - for i in wfs_plt.parent_of_lineages: - if i.child_instance.category == "workflow_step": - plt_fill_wfs = i.child_instance - - action_data_dat = plt_fill_wfs.json_addl["action_groups"]["plate_operations"][ - "actions" - ]["action/workflow_step_plate_operations/cfdna_quant/1.0"] - # action_data_dat = wfs_plt.json_addl["actions"]["cfdna_quant"] - action_data_dat["captured_data"]["gdna_quant"] = "" - yy = bob_wfs.do_action( - plt_fill_wfs.euid, - action_group="plate_operations", - action="action/workflow_step_plate_operations/cfdna_quant/1.0", - action_ds=action_data_dat, - ) - set_status(bob_wfs, plt_fill_wfs, "complete") - - eplt = None - for i in plt_fill_wfs.parent_of_lineages: - if i.child_instance.category == "workflow_step": - eplt = i.child_instance - set_status(bob_wfs, eplt, "in_progress") - - next_plate = "" - for i in plt_fill_wfs.parent_of_lineages: - if i.child_instance.type == "plate": - next_plate = i.child_instance.euid - - stamp_action_data = plt_fill_wfs.json_addl["action_groups"]["plate_operations"][ - "actions" - ]["action/workflow_step_plate_operations/stamp_copy_plate/1.0"] - stamp_action_data["captured_data"]["plate_euid"] = next_plate - - xx = bob_wfs.do_action( - plt_fill_wfs.euid, - action_group="plate_operations", - action="action/workflow_step_plate_operations/stamp_copy_plate/1.0", - action_ds=stamp_action_data, - ) - - for i in plt_fill_wfs.parent_of_lineages: - if i.child_instance.type == "plate-operations": - sec_stamp_wfs = i.child_instance - - for i in sec_stamp_wfs.parent_of_lineages: - if i.child_instance.category == "container": - next_plate2 = i.child_instance.euid - - stamp_action_data2 = sec_stamp_wfs.json_addl["action_groups"]["plate_operations"][ - "actions" - ]["action/workflow_step_plate_operations/stamp_copy_plate/1.0"] - stamp_action_data2["captured_data"]["plate_euid"] = next_plate2 - stamp_wfs2 = bob_wfs.do_action( - sec_stamp_wfs.euid, - action_group="plate_operations", - action="action/workflow_step_plate_operations/stamp_copy_plate/1.0", - action_ds=stamp_action_data2, - ) - - - # make a control - rgnt = bob_wfs.create_instance_by_template_components( - "content", "control", "giab-HG002", "1.0", "active" - )[0][0] - # put in a tube - tube = bob_wfs.create_instance_by_template_components( - "container", "tube", "tube-generic-10ml", "1.0", "active" - )[0][0] - # put the reagent in the tube - - bob_wfs.create_generic_instance_lineage_by_euids(tube.euid, rgnt.euid) - - - wset_q = wfset_wf - wset_q_axn = "action/move-queues/move-among-ay-top-queues/1.0" - wset_q_axn_grp = "queue-move" - wset_q_ad = wset_q.json_addl["action_groups"][wset_q_axn_grp][ - "actions"][wset_q_axn] - wset_q_ad["captured_data"]["q_selection"] = "workflow_step/queue/plasma-isolation-queue-exception/1.0" if randint(0,5) > 4 else "workflow_step/queue/plasma-isolation-queue-removed/1.0" - bob_wfs.do_action( - wset_q.euid, - action_group=wset_q_axn_grp, - action=wset_q_axn, - action_ds=wset_q_ad - ) - - - n_tubes = 1 # no more than 20 - - create_tubes(n_tubes) - - fill_plates(tubes=TUBES) - - assert 1 == 1 \ No newline at end of file