From f67af25ca0852c8fcc61c440b34667c8b80dddbc Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Tue, 4 Nov 2025 14:48:11 -0600 Subject: [PATCH 01/28] working madsci rest node actions, debug needed --- .pre-commit-config.yaml | 11 +- README.md | 36 +- default.node.info.yaml | 179 ++++ default.node.yaml | 7 + definitions/bmg.info.yaml | 0 definitions/bmg.node.yaml | 7 + pdm.lock | 2127 +++++++++++++++++++++++++++++++++++++ pyproject.toml | 138 +-- ruff.toml | 125 +++ src/bmg_interface.py | 114 +- src/bmg_rest_node.py | 234 ++-- src/bmg_rest_node_WEI.py | 121 +++ 12 files changed, 2815 insertions(+), 284 deletions(-) create mode 100644 default.node.info.yaml create mode 100644 default.node.yaml create mode 100644 definitions/bmg.info.yaml create mode 100644 definitions/bmg.node.yaml create mode 100644 pdm.lock create mode 100644 ruff.toml create mode 100644 src/bmg_rest_node_WEI.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5142564..036896b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,8 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-yaml - - id: check-json - id: check-toml - id: check-ast - id: check-merge-conflict @@ -12,15 +11,19 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/kynan/nbstripout - rev: 0.7.1 + rev: 0.8.1 hooks: - id: nbstripout - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.6.9 + rev: v0.14.2 hooks: # Run the linter. - id: ruff args: [--fix] # Run the formatter. - id: ruff-format + - repo: https://gitlab.com/bmares/check-json5 + rev: v1.0.0 + hooks: + - id: check-json5 diff --git a/README.md b/README.md index 4138816..d21c8f8 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ -# BMG module +# BMG module A WEI-powered module for controlling BMG Microplate Readers, currently tested with the VANTAstar model. -Contains a BMG driver (bmg_driver.py) and BMG REST Node (bmg_rest_node.py). +Contains a BMG driver (bmg_driver.py) and BMG REST Node (bmg_rest_node.py). ### Assay Setup on BMG microplate reader -In order to run an assay on the BMG plate reader, you first need to create the assay using the BMG Smart Control software (not the Voyager software). Once the assay is created and saved, it needs to be exported and the .TCS file should be placed inside the BMG directory that contains the assay database .db file ("C:\Program Files (x86)\BMG\CLARIOstar\User\Definit" with default BMG Smart Control on Windows). Once the .TCS file for your assay has been saved into the database directory, it can be accessed by our BMG driver and REST Node by assay name. +In order to run an assay on the BMG plate reader, you first need to create the assay using the BMG Smart Control software (not the Voyager software). Once the assay is created and saved, it needs to be exported and the .TCS file should be placed inside the BMG directory that contains the assay database .db file ("C:\Program Files (x86)\BMG\CLARIOstar\User\Definit" with default BMG Smart Control on Windows). Once the .TCS file for your assay has been saved into the database directory, it can be accessed by our BMG driver and REST Node by assay name. ### Running instructions -The BMG driver and REST Node can only connect to the device if run with **32-bit python**. In the following commands, be sure that you're running the correct python version, replacing 'python' with the complete path to your 32-bit python .exe file if necessary. +The BMG driver and REST Node can only connect to the device if run with **32-bit python**. In the following commands, be sure that you're running the correct python version, replacing 'python' with the complete path to your 32-bit python .exe file if necessary. #### Installation @@ -18,10 +18,10 @@ When running the last line of these install instructions, be sure to pip install python.exe -m pip install -e . -General install instructions: +General install instructions: git clone https://github.com/AD-SDL/bmg_module.git - cd bmg_module + cd bmg_module pip install -e . If you're having trouble installing the requirements or wei due to an issue installing httptools, use the Visual Studio Installer (download this if you don't have it already), and either modify or install Visual Studio Community 2022 to include "Desktop development with C++". @@ -49,7 +49,7 @@ You can also use the driver in other programs. See the below python program uses data_output_file_name = "assya_data.txt", ) -Be sure to replace the protocol_database_path and data_output_directory with your correct paths. +Be sure to replace the protocol_database_path and data_output_directory with your correct paths. #### Running the REST Node @@ -62,13 +62,13 @@ The REST Node can be started with a command in the format below --db_directory_path will default to "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" unless specified \ and -- output_path will default to "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" unless specified. -Example usage with no optional arguments (remember to use 32-bit python): +Example usage with no optional arguments (remember to use 32-bit python): python.exe bmg_rest_node.py --port 3003 -Example usage with all optional arguments: +Example usage with all optional arguments: python.exe bmg_rest_node.py --port 3003 --db_directory_path "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" --output_path "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" @@ -76,10 +76,10 @@ Example usage with all optional arguments: ### Example Usage in WEI Workflow YAML file -Below is an example of a YAML WEI Workflow file that could interact with the BMG REST Node. +Below is an example of a YAML WEI Workflow file that could interact with the BMG REST Node. name: BMG Example - author: RPL + author: RPL info: An example WEI workflow to show available BMG actions version: '0.1' @@ -101,18 +101,6 @@ Below is an example of a YAML WEI Workflow file that could interact with the BMG - name: Run bmg module: bmg action: run_assay - args: + args: assay_name: Assay_name data_output_file_name: assay_data.txt - - - - - - - - - - - - diff --git a/default.node.info.yaml b/default.node.info.yaml new file mode 100644 index 0000000..694353f --- /dev/null +++ b/default.node.info.yaml @@ -0,0 +1,179 @@ +node_name: default.node +node_id: 01K981VA84PV8SJ0Z8AGKAJ0QK +node_description: null +node_type: device +module_name: bmg_node +module_version: 0.0.1 +capabilities: + get_info: true + get_state: true + get_status: true + send_action: true + get_action_status: true + get_action_result: true + get_action_history: true + action_files: true + send_admin_commands: true + set_config: true + get_resources: false + get_log: true + admin_commands: + - lock + - reset + - shutdown + - unlock +node_url: null +actions: + open: + name: open + description: Opens the BMG plate tray + args: {} + locations: {} + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + close: + name: close + description: Closes the BMG plate tray + args: {} + locations: {} + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + set_temp: + name: set_temp + description: Sets the temperature on the BMG microplate reader + args: + temp: + name: temp + description: '' + argument_type: float + required: true + default: null + locations: {} + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + run_assay: + name: run_assay + description: Runs an assay on the BMG plate reader + args: + assay_name: + name: assay_name + description: '' + argument_type: str + required: true + default: null + data_output_file_name: + name: data_output_file_name + description: data output file name (ex. data.txt). Will default to .txt + (ex. 1731706249.txt) if no file name is entered. + argument_type: str + required: false + default: null + locations: {} + files: {} + results: + file: + result_label: file + description: null + result_type: file + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null +config: + node_definition: default.node.yaml + node_info_path: null + update_node_files: true + status_update_interval: 2.0 + state_update_interval: 2.0 + node_url: http://suestorm.cels.anl.gov:3003/ + uvicorn_kwargs: {} + output_path: C:\Users\RPL\TEST + db_directory_path: C:\Program Files (x86)\BMG\CLARIOstar\User\Definit +config_schema: + description: Configuration for the BMG node. + properties: + node_definition: + anyOf: + - type: string + - format: path + type: string + - type: 'null' + default: default.node.yaml + description: Path to the node definition file to use. If set, the node will + load the definition from this file on startup. Otherwise, a default configuration + will be created. + title: Node Definition File + node_info_path: + anyOf: + - type: string + - format: path + type: string + - type: 'null' + default: null + description: Path to export the generated node info file. If not set, will use + the node name and the node_definition's path. + title: Node Info Path + update_node_files: + default: true + description: Whether to update the node definition and info files on startup. + If set to False, the node will not update the files even if they are out of + date. + title: Update Node Files + type: boolean + status_update_interval: + anyOf: + - type: number + - type: 'null' + default: 2.0 + description: The interval in seconds at which the node should update its status. + title: Status Update Interval + state_update_interval: + anyOf: + - type: number + - type: 'null' + default: 2.0 + description: The interval in seconds at which the node should update its state. + title: State Update Interval + node_url: + default: http://127.0.0.1:2000/ + description: The URL used to communicate with the node. This is the base URL + for the REST API. + format: uri + minLength: 1 + title: Node URL + type: string + uvicorn_kwargs: + additionalProperties: true + description: Configuration for the Uvicorn server that runs the REST API. + title: Uvicorn Configuration + type: object + output_path: + default: C:\Users\RPL\TEST + title: Output Path + type: string + db_directory_path: + default: C:\Program Files (x86)\BMG\CLARIOstar\User\Definit + title: Db Directory Path + type: string + title: BMGNodeConfig + type: object diff --git a/default.node.yaml b/default.node.yaml new file mode 100644 index 0000000..e016a6b --- /dev/null +++ b/default.node.yaml @@ -0,0 +1,7 @@ +node_name: default.node +node_id: 01K981VA84PV8SJ0Z8AGKAJ0QK +node_description: null +node_type: device +module_name: bmg_node +module_version: 0.0.1 +capabilities: null diff --git a/definitions/bmg.info.yaml b/definitions/bmg.info.yaml new file mode 100644 index 0000000..e69de29 diff --git a/definitions/bmg.node.yaml b/definitions/bmg.node.yaml new file mode 100644 index 0000000..bee566e --- /dev/null +++ b/definitions/bmg.node.yaml @@ -0,0 +1,7 @@ +node_name: bmg +node_id: +node_description: A node definition for BMG plate reader +node_type: device +module_name: bmg_module +module_version: 1.1.0 +capabilities: null \ No newline at end of file diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 0000000..bc7628d --- /dev/null +++ b/pdm.lock @@ -0,0 +1,2127 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "dev"] +strategy = ["inherit_metadata"] +lock_version = "4.5.0" +content_hash = "sha256:5d4e07bab5161a059640bc2b8f9298d342985676c3fb69315d25fd181d5d56d5" + +[[metadata.targets]] +requires_python = ">=3.9.1" + +[[package]] +name = "aenum" +version = "3.1.16" +summary = "Advanced Enumerations (compatible with Python's stdlib Enum), NamedTuples, and NamedConstants" +groups = ["default"] +files = [ + {file = "aenum-3.1.16-py3-none-any.whl", hash = "sha256:9035092855a98e41b66e3d0998bd7b96280e85ceb3a04cc035636138a1943eaf"}, +] + +[[package]] +name = "annotated-doc" +version = "0.0.3" +requires_python = ">=3.8" +summary = "Document parameters, class attributes, return types, and variables inline, with Annotated." +groups = ["default"] +files = [ + {file = "annotated_doc-0.0.3-py3-none-any.whl", hash = "sha256:348ec6664a76f1fd3be81f43dffbee4c7e8ce931ba71ec67cc7f4ade7fbbb580"}, + {file = "annotated_doc-0.0.3.tar.gz", hash = "sha256:e18370014c70187422c33e945053ff4c286f453a984eba84d0dbfa0c935adeda"}, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +requires_python = ">=3.8" +summary = "Reusable constraint types to use with typing.Annotated" +groups = ["default"] +dependencies = [ + "typing-extensions>=4.0.0; python_version < \"3.9\"", +] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.11.0" +requires_python = ">=3.9" +summary = "High-level concurrency and networking framework on top of asyncio or Trio" +groups = ["default"] +dependencies = [ + "exceptiongroup>=1.0.2; python_version < \"3.11\"", + "idna>=2.8", + "sniffio>=1.1", + "typing-extensions>=4.5; python_version < \"3.13\"", +] +files = [ + {file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}, + {file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"}, +] + +[[package]] +name = "argon2-cffi" +version = "25.1.0" +requires_python = ">=3.8" +summary = "Argon2 for Python" +groups = ["default"] +dependencies = [ + "argon2-cffi-bindings", +] +files = [ + {file = "argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741"}, + {file = "argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1"}, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +requires_python = ">=3.9" +summary = "Low-level CFFI bindings for Argon2" +groups = ["default"] +dependencies = [ + "cffi>=1.0.1; python_version < \"3.14\"", + "cffi>=2.0.0b1; python_version >= \"3.14\"", +] +files = [ + {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f"}, + {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b"}, + {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a"}, + {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44"}, + {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb"}, + {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92"}, + {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85"}, + {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f"}, + {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6"}, + {file = "argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623"}, + {file = "argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500"}, + {file = "argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44"}, + {file = "argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0"}, + {file = "argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6"}, + {file = "argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a"}, + {file = "argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d"}, + {file = "argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99"}, + {file = "argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2"}, + {file = "argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98"}, + {file = "argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94"}, + {file = "argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6dca33a9859abf613e22733131fc9194091c1fa7cb3e131c143056b4856aa47e"}, + {file = "argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:21378b40e1b8d1655dd5310c84a40fc19a9aa5e6366e835ceb8576bf0fea716d"}, + {file = "argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d588dec224e2a83edbdc785a5e6f3c6cd736f46bfd4b441bbb5aa1f5085e584"}, + {file = "argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5acb4e41090d53f17ca1110c3427f0a130f944b896fc8c83973219c97f57b690"}, + {file = "argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520"}, + {file = "argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d"}, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +requires_python = ">=3.7" +summary = "Python package for providing Mozilla's CA Bundle." +groups = ["default"] +files = [ + {file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}, + {file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"}, +] + +[[package]] +name = "cffi" +version = "2.0.0" +requires_python = ">=3.9" +summary = "Foreign Function Interface for Python calling C code." +groups = ["default"] +dependencies = [ + "pycparser; implementation_name != \"PyPy\"", +] +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +requires_python = ">=3.8" +summary = "Validate configuration and produce human readable error messages." +groups = ["dev"] +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +requires_python = ">=3.7" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +groups = ["default"] +files = [ + {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, + {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, + {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, +] + +[[package]] +name = "classy-fastapi" +version = "0.7.0" +requires_python = ">=3.9" +summary = "Class based routing for FastAPI" +groups = ["default"] +dependencies = [ + "fastapi<1.0.0,>=0.73.0", + "pydantic<3.0.0,>=2.8", +] +files = [ + {file = "classy_fastapi-0.7.0-py3-none-any.whl", hash = "sha256:eea211181d065fecb543d68b75a6a6ab93393bb7ca48551887fa898ba7d40ec3"}, + {file = "classy_fastapi-0.7.0.tar.gz", hash = "sha256:4ab934fc309b134cd8dde5f7d6f98e1303a111cf1341ebd5bf900a630d20b084"}, +] + +[[package]] +name = "click" +version = "8.1.8" +requires_python = ">=3.7" +summary = "Composable command line interface toolkit" +groups = ["default"] +dependencies = [ + "colorama; platform_system == \"Windows\"", + "importlib-metadata; python_version < \"3.8\"", +] +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +groups = ["default", "dev"] +marker = "sys_platform == \"win32\" or platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "comtypes" +version = "1.4.13" +requires_python = ">=3.9" +summary = "Pure Python COM package" +groups = ["default"] +files = [ + {file = "comtypes-1.4.13-py3-none-any.whl", hash = "sha256:21546210748ba52e839e52112124b16ffab7d7fb68096493165fbc249e9023ad"}, + {file = "comtypes-1.4.13.zip", hash = "sha256:fc573997ae32b374891cfa8d79ebb8289809ed3a4a3f789d5348371099c7788a"}, +] + +[[package]] +name = "dill" +version = "0.4.0" +requires_python = ">=3.8" +summary = "serialize all of Python" +groups = ["default"] +files = [ + {file = "dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}, + {file = "dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0"}, +] + +[[package]] +name = "distlib" +version = "0.4.0" +summary = "Distribution utilities" +groups = ["dev"] +files = [ + {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, + {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +requires_python = ">=3.9" +summary = "DNS toolkit" +groups = ["default"] +files = [ + {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"}, + {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +requires_python = ">=3.7" +summary = "Backport of PEP 654 (exception groups)" +groups = ["default", "dev"] +marker = "python_version < \"3.11\"" +dependencies = [ + "typing-extensions>=4.6.0; python_version < \"3.13\"", +] +files = [ + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, +] + +[[package]] +name = "fastapi" +version = "0.121.0" +requires_python = ">=3.8" +summary = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +groups = ["default"] +dependencies = [ + "annotated-doc>=0.0.2", + "pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4", + "starlette<0.50.0,>=0.40.0", + "typing-extensions>=4.8.0", +] +files = [ + {file = "fastapi-0.121.0-py3-none-any.whl", hash = "sha256:8bdf1b15a55f4e4b0d6201033da9109ea15632cb76cf156e7b8b4019f2172106"}, + {file = "fastapi-0.121.0.tar.gz", hash = "sha256:06663356a0b1ee93e875bbf05a31fb22314f5bed455afaaad2b2dad7f26e98fa"}, +] + +[[package]] +name = "filelock" +version = "3.19.1" +requires_python = ">=3.9" +summary = "A platform independent file lock." +groups = ["dev"] +files = [ + {file = "filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d"}, + {file = "filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58"}, +] + +[[package]] +name = "greenlet" +version = "3.2.4" +requires_python = ">=3.9" +summary = "Lightweight in-process concurrent programming" +groups = ["default"] +marker = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\"" +files = [ + {file = "greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c"}, + {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590"}, + {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c"}, + {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b"}, + {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31"}, + {file = "greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d"}, + {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5"}, + {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f"}, + {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7"}, + {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8"}, + {file = "greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c"}, + {file = "greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2"}, + {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246"}, + {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3"}, + {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633"}, + {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079"}, + {file = "greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8"}, + {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52"}, + {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa"}, + {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c"}, + {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5"}, + {file = "greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9"}, + {file = "greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd"}, + {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb"}, + {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968"}, + {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9"}, + {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6"}, + {file = "greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0"}, + {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0"}, + {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f"}, + {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0"}, + {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d"}, + {file = "greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02"}, + {file = "greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31"}, + {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945"}, + {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc"}, + {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a"}, + {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504"}, + {file = "greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671"}, + {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b"}, + {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae"}, + {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b"}, + {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929"}, + {file = "greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b"}, + {file = "greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0"}, + {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f"}, + {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5"}, + {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1"}, + {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735"}, + {file = "greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337"}, + {file = "greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269"}, + {file = "greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681"}, + {file = "greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01"}, + {file = "greenlet-3.2.4-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c"}, + {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d"}, + {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58"}, + {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4"}, + {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433"}, + {file = "greenlet-3.2.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df"}, + {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594"}, + {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98"}, + {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:28a3c6b7cd72a96f61b0e4b2a36f681025b60ae4779cc73c1535eb5f29560b10"}, + {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52206cd642670b0b320a1fd1cbfd95bca0e043179c1d8a045f2c6109dfe973be"}, + {file = "greenlet-3.2.4-cp39-cp39-win32.whl", hash = "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b"}, + {file = "greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb"}, + {file = "greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d"}, +] + +[[package]] +name = "h11" +version = "0.16.0" +requires_python = ">=3.8" +summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +groups = ["default"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "httptools" +version = "0.7.1" +requires_python = ">=3.9" +summary = "A collection of framework independent HTTP protocol utils." +groups = ["default"] +files = [ + {file = "httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78"}, + {file = "httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4"}, + {file = "httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05"}, + {file = "httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed"}, + {file = "httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a"}, + {file = "httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b"}, + {file = "httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568"}, + {file = "httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657"}, + {file = "httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70"}, + {file = "httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df"}, + {file = "httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e"}, + {file = "httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274"}, + {file = "httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec"}, + {file = "httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb"}, + {file = "httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5"}, + {file = "httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5"}, + {file = "httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03"}, + {file = "httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2"}, + {file = "httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362"}, + {file = "httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c"}, + {file = "httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321"}, + {file = "httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3"}, + {file = "httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca"}, + {file = "httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c"}, + {file = "httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66"}, + {file = "httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346"}, + {file = "httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650"}, + {file = "httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6"}, + {file = "httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270"}, + {file = "httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3"}, + {file = "httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1"}, + {file = "httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b"}, + {file = "httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60"}, + {file = "httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca"}, + {file = "httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96"}, + {file = "httptools-0.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ac50afa68945df63ec7a2707c506bd02239272288add34539a2ef527254626a4"}, + {file = "httptools-0.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de987bb4e7ac95b99b805b99e0aae0ad51ae61df4263459d36e07cf4052d8b3a"}, + {file = "httptools-0.7.1-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d169162803a24425eb5e4d51d79cbf429fd7a491b9e570a55f495ea55b26f0bf"}, + {file = "httptools-0.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49794f9250188a57fa73c706b46cb21a313edb00d337ca4ce1a011fe3c760b28"}, + {file = "httptools-0.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aeefa0648362bb97a7d6b5ff770bfb774930a327d7f65f8208394856862de517"}, + {file = "httptools-0.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0d92b10dbf0b3da4823cde6a96d18e6ae358a9daa741c71448975f6a2c339cad"}, + {file = "httptools-0.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:5ddbd045cfcb073db2449563dd479057f2c2b681ebc232380e63ef15edc9c023"}, + {file = "httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9"}, +] + +[[package]] +name = "identify" +version = "2.6.15" +requires_python = ">=3.9" +summary = "File identification library for Python" +groups = ["dev"] +files = [ + {file = "identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757"}, + {file = "identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf"}, +] + +[[package]] +name = "idna" +version = "3.11" +requires_python = ">=3.8" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["default"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +requires_python = ">=3.8" +summary = "brain-dead simple config-ini parsing" +groups = ["default", "dev"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "linkify-it-py" +version = "2.0.3" +requires_python = ">=3.7" +summary = "Links recognition library with FULL unicode support." +groups = ["default"] +dependencies = [ + "uc-micro-py", +] +files = [ + {file = "linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048"}, + {file = "linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79"}, +] + +[[package]] +name = "madsci-client" +version = "0.5.0" +requires_python = ">=3.9.1" +summary = "The Modular Autonomous Discovery for Science (MADSci) Python Client and CLI." +groups = ["default"] +dependencies = [ + "click>=8.1.7", + "madsci-common", + "minio>=7.1.0", + "trogon>=0.6.0", +] +files = [ + {file = "madsci_client-0.5.0-py3-none-any.whl", hash = "sha256:350f63132a8a64a0c238e0b9d946b4a65cf03e744c2971d4fb3ff4e58d9f36fb"}, + {file = "madsci_client-0.5.0.tar.gz", hash = "sha256:1f7eb89ff83d20042f0d6b5d22a0a893cc535a3948c2e7bf487bbea259d3b946"}, +] + +[[package]] +name = "madsci-common" +version = "0.5.0" +requires_python = ">=3.9.1" +summary = "The Modular Autonomous Discovery for Science (MADSci) Common Definitions and Utilities." +groups = ["default"] +dependencies = [ + "PyYAML>=6.0.2", + "aenum>=3.1.15", + "classy-fastapi>=0.7.0", + "fastapi>=0.115.4", + "multiprocess>=0.70.17", + "pydantic-extra-types>=2.10.2", + "pydantic-settings-export>=1.0.2", + "pydantic-settings>=2.9.1", + "pydantic>=2.10", + "pymongo>=4.10.1", + "python-dotenv>=1.0.1", + "python-multipart>=0.0.17", + "python-ulid[pydantic]>=3.0.0", + "regex>=2025.7.34", + "requests>=2.32.3", + "semver>=3.0.4", + "sqlmodel>=0.0.22", + "uvicorn[standard]>=0.32.0", +] +files = [ + {file = "madsci_common-0.5.0-py3-none-any.whl", hash = "sha256:69f123576a414c046cff5b238ac9b2ab5f3ae895bde69688f1bb3b56bcada3d5"}, + {file = "madsci_common-0.5.0.tar.gz", hash = "sha256:68c7a2fbd27262ea0d228c1a20c18173d68e653908ebb672bbcdd9fe019f2835"}, +] + +[[package]] +name = "madsci-node-module" +version = "0.5.0" +requires_python = ">=3.9.1" +summary = "The Modular Autonomous Discovery for Science (MADSci) Node Module Helper Classes." +groups = ["default"] +dependencies = [ + "madsci-client", + "madsci-common", + "regex", +] +files = [ + {file = "madsci_node_module-0.5.0-py3-none-any.whl", hash = "sha256:b7353d8633740bf62cfad05ad1a3bd2e9c190f997fac7fed93919361c503c36e"}, + {file = "madsci_node_module-0.5.0.tar.gz", hash = "sha256:77bdea14bf1ce01bc46fe99865cb3294f947e81e613d7b79c4097c6ed8462087"}, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +requires_python = ">=3.8" +summary = "Python port of markdown-it. Markdown parsing, done right!" +groups = ["default"] +dependencies = [ + "mdurl~=0.1", +] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +extras = ["linkify"] +requires_python = ">=3.8" +summary = "Python port of markdown-it. Markdown parsing, done right!" +groups = ["default"] +dependencies = [ + "linkify-it-py<3,>=1", + "markdown-it-py==3.0.0", +] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.4.2" +requires_python = ">=3.8" +summary = "Collection of plugins for markdown-it-py" +groups = ["default"] +dependencies = [ + "markdown-it-py<4.0.0,>=1.0.0", +] +files = [ + {file = "mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636"}, + {file = "mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +requires_python = ">=3.7" +summary = "Markdown URL utilities" +groups = ["default"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "minio" +version = "7.2.18" +requires_python = ">=3.9" +summary = "MinIO Python SDK for Amazon S3 Compatible Cloud Storage" +groups = ["default"] +dependencies = [ + "argon2-cffi", + "certifi", + "pycryptodome", + "typing-extensions", + "urllib3", +] +files = [ + {file = "minio-7.2.18-py3-none-any.whl", hash = "sha256:f23a6edbff8d0bc4b5c1a61b2628a01c5a3342aefc613ff9c276012e6321108f"}, + {file = "minio-7.2.18.tar.gz", hash = "sha256:173402a5716099159c5659f9de75be204ebe248557b9f1cc9cf45aa70e9d3024"}, +] + +[[package]] +name = "multiprocess" +version = "0.70.18" +requires_python = ">=3.8" +summary = "better multiprocessing and multithreading in Python" +groups = ["default"] +dependencies = [ + "dill>=0.4.0", +] +files = [ + {file = "multiprocess-0.70.18-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:25d4012dcaaf66b9e8e955f58482b42910c2ee526d532844d8bcf661bbc604df"}, + {file = "multiprocess-0.70.18-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:06b19433de0d02afe5869aec8931dd5c01d99074664f806c73896b0d9e527213"}, + {file = "multiprocess-0.70.18-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6fa1366f994373aaf2d4738b0f56e707caeaa05486e97a7f71ee0853823180c2"}, + {file = "multiprocess-0.70.18-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b8940ae30139e04b076da6c5b83e9398585ebdf0f2ad3250673fef5b2ff06d6"}, + {file = "multiprocess-0.70.18-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0929ba95831adb938edbd5fb801ac45e705ecad9d100b3e653946b7716cb6bd3"}, + {file = "multiprocess-0.70.18-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4d77f8e4bfe6c6e2e661925bbf9aed4d5ade9a1c6502d5dfc10129b9d1141797"}, + {file = "multiprocess-0.70.18-pp39-pypy39_pp73-macosx_10_13_arm64.whl", hash = "sha256:9fd8d662f7524a95a1be7cbea271f0b33089fe792baabec17d93103d368907da"}, + {file = "multiprocess-0.70.18-pp39-pypy39_pp73-macosx_10_13_x86_64.whl", hash = "sha256:3fbba48bfcd932747c33f0b152b26207c4e0840c35cab359afaff7a8672b1031"}, + {file = "multiprocess-0.70.18-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5f9be0342e597dde86152c10442c5fb6c07994b1c29de441b7a3a08b0e6be2a0"}, + {file = "multiprocess-0.70.18-py310-none-any.whl", hash = "sha256:60c194974c31784019c1f459d984e8f33ee48f10fcf42c309ba97b30d9bd53ea"}, + {file = "multiprocess-0.70.18-py311-none-any.whl", hash = "sha256:5aa6eef98e691281b3ad923be2832bf1c55dd2c859acd73e5ec53a66aae06a1d"}, + {file = "multiprocess-0.70.18-py312-none-any.whl", hash = "sha256:9b78f8e5024b573730bfb654783a13800c2c0f2dfc0c25e70b40d184d64adaa2"}, + {file = "multiprocess-0.70.18-py313-none-any.whl", hash = "sha256:871743755f43ef57d7910a38433cfe41319e72be1bbd90b79c7a5ac523eb9334"}, + {file = "multiprocess-0.70.18-py39-none-any.whl", hash = "sha256:e78ca805a72b1b810c690b6b4cc32579eba34f403094bbbae962b7b5bf9dfcb8"}, + {file = "multiprocess-0.70.18.tar.gz", hash = "sha256:f9597128e6b3e67b23956da07cf3d2e5cba79e2f4e0fba8d7903636663ec6d0d"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Node.js virtual environment builder" +groups = ["dev"] +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "packaging" +version = "25.0" +requires_python = ">=3.8" +summary = "Core utilities for Python packages" +groups = ["default", "dev"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +requires_python = ">=3.9" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +groups = ["default", "dev"] +files = [ + {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, + {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +requires_python = ">=3.9" +summary = "plugin and hook calling mechanisms for python" +groups = ["default", "dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[[package]] +name = "pre-commit" +version = "4.3.0" +requires_python = ">=3.9" +summary = "A framework for managing and maintaining multi-language pre-commit hooks." +groups = ["dev"] +dependencies = [ + "cfgv>=2.0.0", + "identify>=1.0.0", + "nodeenv>=0.11.1", + "pyyaml>=5.1", + "virtualenv>=20.10.0", +] +files = [ + {file = "pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8"}, + {file = "pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16"}, +] + +[[package]] +name = "pycparser" +version = "2.23" +requires_python = ">=3.8" +summary = "C parser in Python" +groups = ["default"] +marker = "implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, + {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, +] + +[[package]] +name = "pycryptodome" +version = "3.23.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cryptographic library for Python" +groups = ["default"] +files = [ + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:865d83c906b0fc6a59b510deceee656b6bc1c4fa0d82176e2b77e97a420a996a"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d4d56153efc4d81defe8b65fd0821ef8b2d5ddf8ed19df31ba2f00872b8002"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64093fc334c1eccfd3933c134c4457c34eaca235eeae49d69449dc4728079339"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ce64e84a962b63a47a592690bdc16a7eaf709d2c2697ababf24a0def566899a6"}, + {file = "pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef"}, +] + +[[package]] +name = "pydantic" +version = "2.12.3" +requires_python = ">=3.9" +summary = "Data validation using Python type hints" +groups = ["default"] +dependencies = [ + "annotated-types>=0.6.0", + "pydantic-core==2.41.4", + "typing-extensions>=4.14.1", + "typing-inspection>=0.4.2", +] +files = [ + {file = "pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf"}, + {file = "pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74"}, +] + +[[package]] +name = "pydantic-core" +version = "2.41.4" +requires_python = ">=3.9" +summary = "Core functionality for Pydantic validation and serialization" +groups = ["default"] +dependencies = [ + "typing-extensions>=4.14.1", +] +files = [ + {file = "pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e"}, + {file = "pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b"}, + {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd"}, + {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945"}, + {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706"}, + {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba"}, + {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b"}, + {file = "pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d"}, + {file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700"}, + {file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6"}, + {file = "pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9"}, + {file = "pydantic_core-2.41.4-cp310-cp310-win32.whl", hash = "sha256:0cf2a1f599efe57fa0051312774280ee0f650e11152325e41dfd3018ef2c1b57"}, + {file = "pydantic_core-2.41.4-cp310-cp310-win_amd64.whl", hash = "sha256:a8c2e340d7e454dc3340d3d2e8f23558ebe78c98aa8f68851b04dcb7bc37abdc"}, + {file = "pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80"}, + {file = "pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae"}, + {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827"}, + {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f"}, + {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def"}, + {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2"}, + {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8"}, + {file = "pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265"}, + {file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c"}, + {file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a"}, + {file = "pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e"}, + {file = "pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03"}, + {file = "pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e"}, + {file = "pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db"}, + {file = "pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887"}, + {file = "pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2"}, + {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999"}, + {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4"}, + {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f"}, + {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b"}, + {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47"}, + {file = "pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970"}, + {file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed"}, + {file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8"}, + {file = "pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431"}, + {file = "pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd"}, + {file = "pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff"}, + {file = "pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8"}, + {file = "pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746"}, + {file = "pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced"}, + {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a"}, + {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02"}, + {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1"}, + {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2"}, + {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84"}, + {file = "pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d"}, + {file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d"}, + {file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2"}, + {file = "pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab"}, + {file = "pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c"}, + {file = "pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4"}, + {file = "pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564"}, + {file = "pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4"}, + {file = "pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2"}, + {file = "pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf"}, + {file = "pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2"}, + {file = "pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89"}, + {file = "pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1"}, + {file = "pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac"}, + {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554"}, + {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e"}, + {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616"}, + {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af"}, + {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12"}, + {file = "pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d"}, + {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad"}, + {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a"}, + {file = "pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025"}, + {file = "pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e"}, + {file = "pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894"}, + {file = "pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d"}, + {file = "pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da"}, + {file = "pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e"}, + {file = "pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa"}, + {file = "pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d"}, + {file = "pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0"}, + {file = "pydantic_core-2.41.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:646e76293345954acea6966149683047b7b2ace793011922208c8e9da12b0062"}, + {file = "pydantic_core-2.41.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cc8e85a63085a137d286e2791037f5fdfff0aabb8b899483ca9c496dd5797338"}, + {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:692c622c8f859a17c156492783902d8370ac7e121a611bd6fe92cc71acf9ee8d"}, + {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1e2906efb1031a532600679b424ef1d95d9f9fb507f813951f23320903adbd7"}, + {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04e2f7f8916ad3ddd417a7abdd295276a0bf216993d9318a5d61cc058209166"}, + {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df649916b81822543d1c8e0e1d079235f68acdc7d270c911e8425045a8cfc57e"}, + {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c529f862fdba70558061bb936fe00ddbaaa0c647fd26e4a4356ef1d6561891"}, + {file = "pydantic_core-2.41.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3b4c5a1fd3a311563ed866c2c9b62da06cb6398bee186484ce95c820db71cb"}, + {file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6e0fc40d84448f941df9b3334c4b78fe42f36e3bf631ad54c3047a0cdddc2514"}, + {file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:44e7625332683b6c1c8b980461475cde9595eff94447500e80716db89b0da005"}, + {file = "pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:170ee6835f6c71081d031ef1c3b4dc4a12b9efa6a9540f93f95b82f3c7571ae8"}, + {file = "pydantic_core-2.41.4-cp39-cp39-win32.whl", hash = "sha256:3adf61415efa6ce977041ba9745183c0e1f637ca849773afa93833e04b163feb"}, + {file = "pydantic_core-2.41.4-cp39-cp39-win_amd64.whl", hash = "sha256:a238dd3feee263eeaeb7dc44aea4ba1364682c4f9f9467e6af5596ba322c2332"}, + {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00"}, + {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9"}, + {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2"}, + {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258"}, + {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347"}, + {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa"}, + {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a"}, + {file = "pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3619320641fd212aaf5997b6ca505e97540b7e16418f4a241f44cdf108ffb50d"}, + {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5"}, + {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2"}, + {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd"}, + {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c"}, + {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405"}, + {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8"}, + {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308"}, + {file = "pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f"}, + {file = "pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5"}, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.10.6" +requires_python = ">=3.8" +summary = "Extra Pydantic types." +groups = ["default"] +dependencies = [ + "pydantic>=2.5.2", + "typing-extensions", +] +files = [ + {file = "pydantic_extra_types-2.10.6-py3-none-any.whl", hash = "sha256:6106c448316d30abf721b5b9fecc65e983ef2614399a24142d689c7546cc246a"}, + {file = "pydantic_extra_types-2.10.6.tar.gz", hash = "sha256:c63d70bf684366e6bbe1f4ee3957952ebe6973d41e7802aea0b770d06b116aeb"}, +] + +[[package]] +name = "pydantic-settings" +version = "2.11.0" +requires_python = ">=3.9" +summary = "Settings management using Pydantic" +groups = ["default"] +dependencies = [ + "pydantic>=2.7.0", + "python-dotenv>=0.21.0", + "typing-inspection>=0.4.0", +] +files = [ + {file = "pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c"}, + {file = "pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180"}, +] + +[[package]] +name = "pydantic-settings-export" +version = "1.0.3" +requires_python = ">=3.9" +summary = "Export your Pydantic settings to documentation with ease!" +groups = ["default"] +dependencies = [ + "pydantic-settings>=2.3", + "pydantic>=2.7", + "tomli>=2.2.1; python_full_version < \"3.11\"", + "typing-extensions>=4.12.2; python_full_version < \"3.11\"", +] +files = [ + {file = "pydantic_settings_export-1.0.3-py3-none-any.whl", hash = "sha256:86c804d837c26ca2d786080b27036818c816b0963e01ea8f473727aa98b84c07"}, + {file = "pydantic_settings_export-1.0.3.tar.gz", hash = "sha256:8b4f0b5daab0113fdfe9018b5684b7fb6be6157275604f2f22cd8cdd7d7b8f7b"}, +] + +[[package]] +name = "pygments" +version = "2.19.2" +requires_python = ">=3.8" +summary = "Pygments is a syntax highlighting package written in Python." +groups = ["default", "dev"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[[package]] +name = "pymongo" +version = "4.15.3" +requires_python = ">=3.9" +summary = "PyMongo - the Official MongoDB Python driver" +groups = ["default"] +dependencies = [ + "dnspython<3.0.0,>=1.16.0", +] +files = [ + {file = "pymongo-4.15.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:482ca9b775747562ce1589df10c97a0e62a604ce5addf933e5819dd967c5e23c"}, + {file = "pymongo-4.15.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7eb497519f42ac89c30919a51f80e68a070cfc2f3b0543cac74833cd45a6b9c"}, + {file = "pymongo-4.15.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4a0a054e9937ec8fdb465835509b176f6b032851c8648f6a5d1b19932d0eacd6"}, + {file = "pymongo-4.15.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49fd6e158cf75771b2685a8a221a40ab96010ae34dd116abd06371dc6c38ab60"}, + {file = "pymongo-4.15.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82a490f1ade4ec6a72068e3676b04c126e3043e69b38ec474a87c6444cf79098"}, + {file = "pymongo-4.15.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:982107c667921e896292f4be09c057e2f1a40c645c9bfc724af5dd5fb8398094"}, + {file = "pymongo-4.15.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:45aebbd369ca79b7c46eaea5b04d2e4afca4eda117b68965a07a9da05d774e4d"}, + {file = "pymongo-4.15.3-cp310-cp310-win32.whl", hash = "sha256:90ad56bd1d769d2f44af74f0fd0c276512361644a3c636350447994412cbc9a1"}, + {file = "pymongo-4.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:8bd6dd736f5d07a825caf52c38916d5452edc0fac7aee43ec67aba6f61c2dbb7"}, + {file = "pymongo-4.15.3-cp310-cp310-win_arm64.whl", hash = "sha256:300eaf83ad053e51966be1839324341b08eaf880d3dc63ada7942d5912e09c49"}, + {file = "pymongo-4.15.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a13d8f7141294404ce46dfbabb2f2d17e9b1192456651ae831fa351f86fbeb"}, + {file = "pymongo-4.15.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:17d13458baf4a6a9f2e787d95adf8ec50d412accb9926a044bd1c41029c323b2"}, + {file = "pymongo-4.15.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fe4bcb8acfb288e238190397d4a699aeb4adb70e8545a6f4e44f99d4e8096ab1"}, + {file = "pymongo-4.15.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d09d895c7f08bcbed4d2e96a00e52e9e545ae5a37b32d2dc10099b205a21fc6d"}, + {file = "pymongo-4.15.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:21c0a95a4db72562fd0805e2f76496bf432ba2e27a5651f4b9c670466260c258"}, + {file = "pymongo-4.15.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89e45d7fa987f4e246cdf43ff001e3f911f73eb19ba9dabc2a6d80df5c97883b"}, + {file = "pymongo-4.15.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1246a82fa6dd73ac2c63aa7e463752d5d1ca91e0c7a23396b78f21273befd3a7"}, + {file = "pymongo-4.15.3-cp311-cp311-win32.whl", hash = "sha256:9483521c03f6017336f54445652ead3145154e8d3ea06418e52cea57fee43292"}, + {file = "pymongo-4.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:c57dad9f289d72af1d7c47a444c4d9fa401f951cedbbcc54c7dd0c2107d6d786"}, + {file = "pymongo-4.15.3-cp311-cp311-win_arm64.whl", hash = "sha256:2fd3b99520f2bb013960ac29dece1b43f2f1b6d94351ca33ba1b1211ecf79a09"}, + {file = "pymongo-4.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bd0497c564b0ae34fb816464ffc09986dd9ca29e2772a0f7af989e472fecc2ad"}, + {file = "pymongo-4.15.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:292fd5a3f045751a823a54cdea75809b2216a62cc5f74a1a96b337db613d46a8"}, + {file = "pymongo-4.15.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:959ef69c5e687b6b749fbf2140c7062abdb4804df013ae0507caabf30cba6875"}, + {file = "pymongo-4.15.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de3bc878c3be54ae41c2cabc9e9407549ed4fec41f4e279c04e840dddd7c630c"}, + {file = "pymongo-4.15.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07bcc36d11252f24fe671e7e64044d39a13d997b0502c6401161f28cc144f584"}, + {file = "pymongo-4.15.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b63bac343b79bd209e830aac1f5d9d552ff415f23a924d3e51abbe3041265436"}, + {file = "pymongo-4.15.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b33d59bf6fa1ca1d7d96d4fccff51e41312358194190d53ef70a84c070f5287e"}, + {file = "pymongo-4.15.3-cp312-cp312-win32.whl", hash = "sha256:b3a0ec660d61efb91c16a5962ec937011fe3572c4338216831f102e53d294e5c"}, + {file = "pymongo-4.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:f6b0513e5765fdde39f36e6a29a36c67071122b5efa748940ae51075beb5e4bc"}, + {file = "pymongo-4.15.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4fdd8e6eab8ff77c1c8041792b5f760d48508623cd10b50d5639e73f1eec049"}, + {file = "pymongo-4.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a47a3218f7900f65bf0f36fcd1f2485af4945757360e7e143525db9d715d2010"}, + {file = "pymongo-4.15.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:09440e78dff397b2f34a624f445ac8eb44c9756a2688b85b3bf344d351d198e1"}, + {file = "pymongo-4.15.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:97f9babdb98c31676f97d468f7fe2dc49b8a66fb6900effddc4904c1450196c8"}, + {file = "pymongo-4.15.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71413cd8f091ae25b1fec3af7c2e531cf9bdb88ce4079470e64835f6a664282a"}, + {file = "pymongo-4.15.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:76a8d4de8dceb69f6e06736198ff6f7e1149515ef946f192ff2594d2cc98fc53"}, + {file = "pymongo-4.15.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:77353978be9fc9e5fe56369682efed0aac5f92a2a1570704d62b62a3c9e1a24f"}, + {file = "pymongo-4.15.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9897a837677e3814873d0572f7e5d53c23ce18e274f3b5b87f05fb6eea22615b"}, + {file = "pymongo-4.15.3-cp313-cp313-win32.whl", hash = "sha256:d66da207ccb0d68c5792eaaac984a0d9c6c8ec609c6bcfa11193a35200dc5992"}, + {file = "pymongo-4.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:52f40c4b8c00bc53d4e357fe0de13d031c4cddb5d201e1a027db437e8d2887f8"}, + {file = "pymongo-4.15.3-cp313-cp313-win_arm64.whl", hash = "sha256:fb384623ece34db78d445dd578a52d28b74e8319f4d9535fbaff79d0eae82b3d"}, + {file = "pymongo-4.15.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dcff15b9157c16bc796765d4d3d151df669322acfb0357e4c3ccd056153f0ff4"}, + {file = "pymongo-4.15.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1f681722c9f27e86c49c2e8a838e61b6ecf2285945fd1798bd01458134257834"}, + {file = "pymongo-4.15.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2c96dde79bdccd167b930a709875b0cd4321ac32641a490aebfa10bdcd0aa99b"}, + {file = "pymongo-4.15.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2d4ca446348d850ac4a5c3dc603485640ae2e7805dbb90765c3ba7d79129b37"}, + {file = "pymongo-4.15.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0fd3de3a12ff0a8113a3f64cedb01f87397ab8eaaffa88d7f18ca66cd39385"}, + {file = "pymongo-4.15.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e84dec392cf5f72d365e0aac73f627b0a3170193ebb038c3f7e7df11b7983ee7"}, + {file = "pymongo-4.15.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d4b01a48369ea6d5bc83fea535f56279f806aa3e4991189f0477696dd736289"}, + {file = "pymongo-4.15.3-cp314-cp314-win32.whl", hash = "sha256:3561fa96c3123275ec5ccf919e595547e100c412ec0894e954aa0da93ecfdb9e"}, + {file = "pymongo-4.15.3-cp314-cp314-win_amd64.whl", hash = "sha256:9df2db6bd91b07400879b6ec89827004c0c2b55fc606bb62db93cafb7677c340"}, + {file = "pymongo-4.15.3-cp314-cp314-win_arm64.whl", hash = "sha256:ff99864085d2c7f4bb672c7167680ceb7d273e9a93c1a8074c986a36dbb71cc6"}, + {file = "pymongo-4.15.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ffe217d2502f3fba4e2b0dc015ce3b34f157b66dfe96835aa64432e909dd0d95"}, + {file = "pymongo-4.15.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:390c4954c774eda280898e73aea36482bf20cba3ecb958dbb86d6a68b9ecdd68"}, + {file = "pymongo-4.15.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7dd2a49f088890ca08930bbf96121443b48e26b02b84ba0a3e1ae2bf2c5a9b48"}, + {file = "pymongo-4.15.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f6feb678f26171f2a6b2cbb340949889154c7067972bd4cc129b62161474f08"}, + {file = "pymongo-4.15.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:446417a34ff6c2411ce3809e17ce9a67269c9f1cb4966b01e49e0c590cc3c6b3"}, + {file = "pymongo-4.15.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cfa4a0a0f024a0336640e1201994e780a17bda5e6a7c0b4d23841eb9152e868b"}, + {file = "pymongo-4.15.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b03db2fe37c950aff94b29ded5c349b23729bccd90a0a5907bbf807d8c77298"}, + {file = "pymongo-4.15.3-cp314-cp314t-win32.whl", hash = "sha256:e7cde58ef6470c0da922b65e885fb1ffe04deef81e526bd5dea429290fa358ca"}, + {file = "pymongo-4.15.3-cp314-cp314t-win_amd64.whl", hash = "sha256:fae552767d8e5153ed498f1bca92d905d0d46311d831eefb0f06de38f7695c95"}, + {file = "pymongo-4.15.3-cp314-cp314t-win_arm64.whl", hash = "sha256:47ffb068e16ae5e43580d5c4e3b9437f05414ea80c32a1e5cac44a835859c259"}, + {file = "pymongo-4.15.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:58d0f4123855f05c0649f9b8ee083acc5b26e7f4afde137cd7b8dc03e9107ff3"}, + {file = "pymongo-4.15.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9bc9f99e7702fdb0dcc3ff1dd490adc5d20b3941ad41e58f887d4998b9922a14"}, + {file = "pymongo-4.15.3-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:86b1b5b63f4355adffc329733733a9b71fdad88f37a9dc41e163aed2130f9abc"}, + {file = "pymongo-4.15.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a054d282dd922ac400b6f47ea3ef58d8b940968d76d855da831dc739b7a04de"}, + {file = "pymongo-4.15.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc583a1130e2516440b93bb2ecb55cfdac6d5373615ae472a9d1f26801f58749"}, + {file = "pymongo-4.15.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c78237e878e0296130e398151b0d4aa6c9eaf82e38fb6e0aaae2029bc7ef0ce"}, + {file = "pymongo-4.15.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c85a4c72b7965033f95c94c42dac27d886c01dbc23fe337ccb14f052a0ccc29"}, + {file = "pymongo-4.15.3-cp39-cp39-win32.whl", hash = "sha256:17fc94d1e067556b122eeb09e25c003268e8c0ea1f2f78e745b33bb59a1209c4"}, + {file = "pymongo-4.15.3-cp39-cp39-win_amd64.whl", hash = "sha256:5bf879a6ed70264574d4d8fb5a467c2a64dc76ecd72c0cb467c4464f849c8c77"}, + {file = "pymongo-4.15.3-cp39-cp39-win_arm64.whl", hash = "sha256:2f3d66f7c495efc3cfffa611b36075efe86da1860a7df75522a6fe499ee10383"}, + {file = "pymongo-4.15.3.tar.gz", hash = "sha256:7a981271347623b5319932796690c2d301668ac3a1965974ac9f5c3b8a22cea5"}, +] + +[[package]] +name = "pyserial" +version = "3.5" +summary = "Python Serial Port Extension" +groups = ["default"] +files = [ + {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, + {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, +] + +[[package]] +name = "pytest" +version = "8.4.2" +requires_python = ">=3.9" +summary = "pytest: simple powerful testing with Python" +groups = ["default", "dev"] +dependencies = [ + "colorama>=0.4; sys_platform == \"win32\"", + "exceptiongroup>=1; python_version < \"3.11\"", + "iniconfig>=1", + "packaging>=20", + "pluggy<2,>=1.5", + "pygments>=2.7.2", + "tomli>=1; python_version < \"3.11\"", +] +files = [ + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +requires_python = ">=3.9" +summary = "Read key-value pairs from a .env file and set them as environment variables" +groups = ["default"] +files = [ + {file = "python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61"}, + {file = "python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6"}, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +requires_python = ">=3.8" +summary = "A streaming multipart parser for Python" +groups = ["default"] +files = [ + {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, + {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, +] + +[[package]] +name = "python-ulid" +version = "3.1.0" +requires_python = ">=3.9" +summary = "Universally unique lexicographically sortable identifier" +groups = ["default"] +files = [ + {file = "python_ulid-3.1.0-py3-none-any.whl", hash = "sha256:e2cdc979c8c877029b4b7a38a6fba3bc4578e4f109a308419ff4d3ccf0a46619"}, + {file = "python_ulid-3.1.0.tar.gz", hash = "sha256:ff0410a598bc5f6b01b602851a3296ede6f91389f913a5d5f8c496003836f636"}, +] + +[[package]] +name = "python-ulid" +version = "3.1.0" +extras = ["pydantic"] +requires_python = ">=3.9" +summary = "Universally unique lexicographically sortable identifier" +groups = ["default"] +dependencies = [ + "pydantic>=2.0", + "python-ulid==3.1.0", +] +files = [ + {file = "python_ulid-3.1.0-py3-none-any.whl", hash = "sha256:e2cdc979c8c877029b4b7a38a6fba3bc4578e4f109a308419ff4d3ccf0a46619"}, + {file = "python_ulid-3.1.0.tar.gz", hash = "sha256:ff0410a598bc5f6b01b602851a3296ede6f91389f913a5d5f8c496003836f636"}, +] + +[[package]] +name = "pywin32" +version = "311" +summary = "Python for Window Extensions" +groups = ["default"] +files = [ + {file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"}, + {file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"}, + {file = "pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b"}, + {file = "pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151"}, + {file = "pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503"}, + {file = "pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2"}, + {file = "pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31"}, + {file = "pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067"}, + {file = "pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852"}, + {file = "pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d"}, + {file = "pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d"}, + {file = "pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a"}, + {file = "pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee"}, + {file = "pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87"}, + {file = "pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42"}, + {file = "pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b"}, + {file = "pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91"}, + {file = "pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +requires_python = ">=3.8" +summary = "YAML parser and emitter for Python" +groups = ["default", "dev"] +files = [ + {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, + {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, + {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, + {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, + {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, + {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, + {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, + {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, + {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, + {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, +] + +[[package]] +name = "regex" +version = "2025.11.3" +requires_python = ">=3.9" +summary = "Alternative regular expression module, to replace re." +groups = ["default"] +files = [ + {file = "regex-2025.11.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2b441a4ae2c8049106e8b39973bfbddfb25a179dda2bdb99b0eeb60c40a6a3af"}, + {file = "regex-2025.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2fa2eed3f76677777345d2f81ee89f5de2f5745910e805f7af7386a920fa7313"}, + {file = "regex-2025.11.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8b4a27eebd684319bdf473d39f1d79eed36bf2cd34bd4465cdb4618d82b3d56"}, + {file = "regex-2025.11.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cf77eac15bd264986c4a2c63353212c095b40f3affb2bc6b4ef80c4776c1a28"}, + {file = "regex-2025.11.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7f9ee819f94c6abfa56ec7b1dbab586f41ebbdc0a57e6524bd5e7f487a878c7"}, + {file = "regex-2025.11.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:838441333bc90b829406d4a03cb4b8bf7656231b84358628b0406d803931ef32"}, + {file = "regex-2025.11.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe6d3f0c9e3b7e8c0c694b24d25e677776f5ca26dce46fd6b0489f9c8339391"}, + {file = "regex-2025.11.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2ab815eb8a96379a27c3b6157fcb127c8f59c36f043c1678110cea492868f1d5"}, + {file = "regex-2025.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:728a9d2d173a65b62bdc380b7932dd8e74ed4295279a8fe1021204ce210803e7"}, + {file = "regex-2025.11.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:509dc827f89c15c66a0c216331260d777dd6c81e9a4e4f830e662b0bb296c313"}, + {file = "regex-2025.11.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:849202cd789e5f3cf5dcc7822c34b502181b4824a65ff20ce82da5524e45e8e9"}, + {file = "regex-2025.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b6f78f98741dcc89607c16b1e9426ee46ce4bf31ac5e6b0d40e81c89f3481ea5"}, + {file = "regex-2025.11.3-cp310-cp310-win32.whl", hash = "sha256:149eb0bba95231fb4f6d37c8f760ec9fa6fabf65bab555e128dde5f2475193ec"}, + {file = "regex-2025.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:ee3a83ce492074c35a74cc76cf8235d49e77b757193a5365ff86e3f2f93db9fd"}, + {file = "regex-2025.11.3-cp310-cp310-win_arm64.whl", hash = "sha256:38af559ad934a7b35147716655d4a2f79fcef2d695ddfe06a06ba40ae631fa7e"}, + {file = "regex-2025.11.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eadade04221641516fa25139273505a1c19f9bf97589a05bc4cfcd8b4a618031"}, + {file = "regex-2025.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:feff9e54ec0dd3833d659257f5c3f5322a12eee58ffa360984b716f8b92983f4"}, + {file = "regex-2025.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b30bc921d50365775c09a7ed446359e5c0179e9e2512beec4a60cbcef6ddd50"}, + {file = "regex-2025.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f99be08cfead2020c7ca6e396c13543baea32343b7a9a5780c462e323bd8872f"}, + {file = "regex-2025.11.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6dd329a1b61c0ee95ba95385fb0c07ea0d3fe1a21e1349fa2bec272636217118"}, + {file = "regex-2025.11.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c5238d32f3c5269d9e87be0cf096437b7622b6920f5eac4fd202468aaeb34d2"}, + {file = "regex-2025.11.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10483eefbfb0adb18ee9474498c9a32fcf4e594fbca0543bb94c48bac6183e2e"}, + {file = "regex-2025.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78c2d02bb6e1da0720eedc0bad578049cad3f71050ef8cd065ecc87691bed2b0"}, + {file = "regex-2025.11.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b49cd2aad93a1790ce9cffb18964f6d3a4b0b3dbdbd5de094b65296fce6e58"}, + {file = "regex-2025.11.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:885b26aa3ee56433b630502dc3d36ba78d186a00cc535d3806e6bfd9ed3c70ab"}, + {file = "regex-2025.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddd76a9f58e6a00f8772e72cff8ebcff78e022be95edf018766707c730593e1e"}, + {file = "regex-2025.11.3-cp311-cp311-win32.whl", hash = "sha256:3e816cc9aac1cd3cc9a4ec4d860f06d40f994b5c7b4d03b93345f44e08cc68bf"}, + {file = "regex-2025.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:087511f5c8b7dfbe3a03f5d5ad0c2a33861b1fc387f21f6f60825a44865a385a"}, + {file = "regex-2025.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:1ff0d190c7f68ae7769cd0313fe45820ba07ffebfddfaa89cc1eb70827ba0ddc"}, + {file = "regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41"}, + {file = "regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36"}, + {file = "regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1"}, + {file = "regex-2025.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7"}, + {file = "regex-2025.11.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69"}, + {file = "regex-2025.11.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48"}, + {file = "regex-2025.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c"}, + {file = "regex-2025.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695"}, + {file = "regex-2025.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98"}, + {file = "regex-2025.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74"}, + {file = "regex-2025.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0"}, + {file = "regex-2025.11.3-cp312-cp312-win32.whl", hash = "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204"}, + {file = "regex-2025.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9"}, + {file = "regex-2025.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26"}, + {file = "regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4"}, + {file = "regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76"}, + {file = "regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a"}, + {file = "regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361"}, + {file = "regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160"}, + {file = "regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe"}, + {file = "regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850"}, + {file = "regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc"}, + {file = "regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9"}, + {file = "regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b"}, + {file = "regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7"}, + {file = "regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c"}, + {file = "regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5"}, + {file = "regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467"}, + {file = "regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281"}, + {file = "regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39"}, + {file = "regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7"}, + {file = "regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed"}, + {file = "regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19"}, + {file = "regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b"}, + {file = "regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a"}, + {file = "regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6"}, + {file = "regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce"}, + {file = "regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd"}, + {file = "regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2"}, + {file = "regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a"}, + {file = "regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c"}, + {file = "regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e"}, + {file = "regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6"}, + {file = "regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4"}, + {file = "regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73"}, + {file = "regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f"}, + {file = "regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d"}, + {file = "regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be"}, + {file = "regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db"}, + {file = "regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62"}, + {file = "regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f"}, + {file = "regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02"}, + {file = "regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed"}, + {file = "regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4"}, + {file = "regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad"}, + {file = "regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f"}, + {file = "regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc"}, + {file = "regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49"}, + {file = "regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536"}, + {file = "regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95"}, + {file = "regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009"}, + {file = "regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9"}, + {file = "regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d"}, + {file = "regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6"}, + {file = "regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154"}, + {file = "regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267"}, + {file = "regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379"}, + {file = "regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38"}, + {file = "regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de"}, + {file = "regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801"}, + {file = "regex-2025.11.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:81519e25707fc076978c6143b81ea3dc853f176895af05bf7ec51effe818aeec"}, + {file = "regex-2025.11.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3bf28b1873a8af8bbb58c26cc56ea6e534d80053b41fb511a35795b6de507e6a"}, + {file = "regex-2025.11.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:856a25c73b697f2ce2a24e7968285579e62577a048526161a2c0f53090bea9f9"}, + {file = "regex-2025.11.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a3d571bd95fade53c86c0517f859477ff3a93c3fde10c9e669086f038e0f207"}, + {file = "regex-2025.11.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:732aea6de26051af97b94bc98ed86448821f839d058e5d259c72bf6d73ad0fc0"}, + {file = "regex-2025.11.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:51c1c1847128238f54930edb8805b660305dca164645a9fd29243f5610beea34"}, + {file = "regex-2025.11.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22dd622a402aad4558277305350699b2be14bc59f64d64ae1d928ce7d072dced"}, + {file = "regex-2025.11.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f3b5a391c7597ffa96b41bd5cbd2ed0305f515fcbb367dfa72735679d5502364"}, + {file = "regex-2025.11.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:cc4076a5b4f36d849fd709284b4a3b112326652f3b0466f04002a6c15a0c96c1"}, + {file = "regex-2025.11.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a295ca2bba5c1c885826ce3125fa0b9f702a1be547d821c01d65f199e10c01e2"}, + {file = "regex-2025.11.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b4774ff32f18e0504bfc4e59a3e71e18d83bc1e171a3c8ed75013958a03b2f14"}, + {file = "regex-2025.11.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22e7d1cdfa88ef33a2ae6aa0d707f9255eb286ffbd90045f1088246833223aee"}, + {file = "regex-2025.11.3-cp39-cp39-win32.whl", hash = "sha256:74d04244852ff73b32eeede4f76f51c5bcf44bc3c207bc3e6cf1c5c45b890708"}, + {file = "regex-2025.11.3-cp39-cp39-win_amd64.whl", hash = "sha256:7a50cd39f73faa34ec18d6720ee25ef10c4c1839514186fcda658a06c06057a2"}, + {file = "regex-2025.11.3-cp39-cp39-win_arm64.whl", hash = "sha256:43b4fb020e779ca81c1b5255015fe2b82816c76ec982354534ad9ec09ad7c9e3"}, + {file = "regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01"}, +] + +[[package]] +name = "requests" +version = "2.32.5" +requires_python = ">=3.9" +summary = "Python HTTP for Humans." +groups = ["default"] +dependencies = [ + "certifi>=2017.4.17", + "charset-normalizer<4,>=2", + "idna<4,>=2.5", + "urllib3<3,>=1.21.1", +] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[[package]] +name = "rich" +version = "14.2.0" +requires_python = ">=3.8.0" +summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +groups = ["default"] +dependencies = [ + "markdown-it-py>=2.2.0", + "pygments<3.0.0,>=2.13.0", +] +files = [ + {file = "rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd"}, + {file = "rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4"}, +] + +[[package]] +name = "ruff" +version = "0.14.3" +requires_python = ">=3.7" +summary = "An extremely fast Python linter and code formatter, written in Rust." +groups = ["dev"] +files = [ + {file = "ruff-0.14.3-py3-none-linux_armv6l.whl", hash = "sha256:876b21e6c824f519446715c1342b8e60f97f93264012de9d8d10314f8a79c371"}, + {file = "ruff-0.14.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6fd8c79b457bedd2abf2702b9b472147cd860ed7855c73a5247fa55c9117654"}, + {file = "ruff-0.14.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:71ff6edca490c308f083156938c0c1a66907151263c4abdcb588602c6e696a14"}, + {file = "ruff-0.14.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:786ee3ce6139772ff9272aaf43296d975c0217ee1b97538a98171bf0d21f87ed"}, + {file = "ruff-0.14.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cd6291d0061811c52b8e392f946889916757610d45d004e41140d81fb6cd5ddc"}, + {file = "ruff-0.14.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a497ec0c3d2c88561b6d90f9c29f5ae68221ac00d471f306fa21fa4264ce5fcd"}, + {file = "ruff-0.14.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e231e1be58fc568950a04fbe6887c8e4b85310e7889727e2b81db205c45059eb"}, + {file = "ruff-0.14.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:469e35872a09c0e45fecf48dd960bfbce056b5db2d5e6b50eca329b4f853ae20"}, + {file = "ruff-0.14.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d6bc90307c469cb9d28b7cfad90aaa600b10d67c6e22026869f585e1e8a2db0"}, + {file = "ruff-0.14.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2f8a0bbcffcfd895df39c9a4ecd59bb80dca03dc43f7fb63e647ed176b741e"}, + {file = "ruff-0.14.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:678fdd7c7d2d94851597c23ee6336d25f9930b460b55f8598e011b57c74fd8c5"}, + {file = "ruff-0.14.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1ec1ac071e7e37e0221d2f2dbaf90897a988c531a8592a6a5959f0603a1ecf5e"}, + {file = "ruff-0.14.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afcdc4b5335ef440d19e7df9e8ae2ad9f749352190e96d481dc501b753f0733e"}, + {file = "ruff-0.14.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7bfc42f81862749a7136267a343990f865e71fe2f99cf8d2958f684d23ce3dfa"}, + {file = "ruff-0.14.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a65e448cfd7e9c59fae8cf37f9221585d3354febaad9a07f29158af1528e165f"}, + {file = "ruff-0.14.3-py3-none-win32.whl", hash = "sha256:f3d91857d023ba93e14ed2d462ab62c3428f9bbf2b4fbac50a03ca66d31991f7"}, + {file = "ruff-0.14.3-py3-none-win_amd64.whl", hash = "sha256:d7b7006ac0756306db212fd37116cce2bd307e1e109375e1c6c106002df0ae5f"}, + {file = "ruff-0.14.3-py3-none-win_arm64.whl", hash = "sha256:26eb477ede6d399d898791d01961e16b86f02bc2486d0d1a7a9bb2379d055dc1"}, + {file = "ruff-0.14.3.tar.gz", hash = "sha256:4ff876d2ab2b161b6de0aa1f5bd714e8e9b4033dc122ee006925fbacc4f62153"}, +] + +[[package]] +name = "semver" +version = "3.0.4" +requires_python = ">=3.7" +summary = "Python helper for Semantic Versioning (https://semver.org)" +groups = ["default"] +files = [ + {file = "semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746"}, + {file = "semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +requires_python = ">=3.7" +summary = "Sniff out which async library your code is running under" +groups = ["default"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.44" +requires_python = ">=3.7" +summary = "Database Abstraction Library" +groups = ["default"] +dependencies = [ + "greenlet>=1; platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\"", + "importlib-metadata; python_version < \"3.8\"", + "typing-extensions>=4.6.0", +] +files = [ + {file = "sqlalchemy-2.0.44-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce"}, + {file = "sqlalchemy-2.0.44-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985"}, + {file = "sqlalchemy-2.0.44-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0"}, + {file = "sqlalchemy-2.0.44-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e"}, + {file = "sqlalchemy-2.0.44-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749"}, + {file = "sqlalchemy-2.0.44-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2"}, + {file = "sqlalchemy-2.0.44-cp310-cp310-win32.whl", hash = "sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165"}, + {file = "sqlalchemy-2.0.44-cp310-cp310-win_amd64.whl", hash = "sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5"}, + {file = "sqlalchemy-2.0.44-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd"}, + {file = "sqlalchemy-2.0.44-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa"}, + {file = "sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e"}, + {file = "sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e"}, + {file = "sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399"}, + {file = "sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b"}, + {file = "sqlalchemy-2.0.44-cp311-cp311-win32.whl", hash = "sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3"}, + {file = "sqlalchemy-2.0.44-cp311-cp311-win_amd64.whl", hash = "sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5"}, + {file = "sqlalchemy-2.0.44-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250"}, + {file = "sqlalchemy-2.0.44-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29"}, + {file = "sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44"}, + {file = "sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1"}, + {file = "sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7"}, + {file = "sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d"}, + {file = "sqlalchemy-2.0.44-cp312-cp312-win32.whl", hash = "sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4"}, + {file = "sqlalchemy-2.0.44-cp312-cp312-win_amd64.whl", hash = "sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e"}, + {file = "sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1"}, + {file = "sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45"}, + {file = "sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976"}, + {file = "sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c"}, + {file = "sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d"}, + {file = "sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40"}, + {file = "sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73"}, + {file = "sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e"}, + {file = "sqlalchemy-2.0.44-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7027414f2b88992877573ab780c19ecb54d3a536bef3397933573d6b5068be4"}, + {file = "sqlalchemy-2.0.44-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fe166c7d00912e8c10d3a9a0ce105569a31a3d0db1a6e82c4e0f4bf16d5eca9"}, + {file = "sqlalchemy-2.0.44-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3caef1ff89b1caefc28f0368b3bde21a7e3e630c2eddac16abd9e47bd27cc36a"}, + {file = "sqlalchemy-2.0.44-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc2856d24afa44295735e72f3c75d6ee7fdd4336d8d3a8f3d44de7aa6b766df2"}, + {file = "sqlalchemy-2.0.44-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:11bac86b0deada30b6b5f93382712ff0e911fe8d31cb9bf46e6b149ae175eff0"}, + {file = "sqlalchemy-2.0.44-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4d18cd0e9a0f37c9f4088e50e3839fcb69a380a0ec957408e0b57cff08ee0a26"}, + {file = "sqlalchemy-2.0.44-cp39-cp39-win32.whl", hash = "sha256:9e9018544ab07614d591a26c1bd4293ddf40752cc435caf69196740516af7100"}, + {file = "sqlalchemy-2.0.44-cp39-cp39-win_amd64.whl", hash = "sha256:8e0e4e66fd80f277a8c3de016a81a554e76ccf6b8d881ee0b53200305a8433f6"}, + {file = "sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05"}, + {file = "sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22"}, +] + +[[package]] +name = "sqlmodel" +version = "0.0.27" +requires_python = ">=3.8" +summary = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness." +groups = ["default"] +dependencies = [ + "SQLAlchemy<2.1.0,>=2.0.14", + "pydantic<3.0.0,>=1.10.13", +] +files = [ + {file = "sqlmodel-0.0.27-py3-none-any.whl", hash = "sha256:667fe10aa8ff5438134668228dc7d7a08306f4c5c4c7e6ad3ad68defa0e7aa49"}, + {file = "sqlmodel-0.0.27.tar.gz", hash = "sha256:ad1227f2014a03905aef32e21428640848ac09ff793047744a73dfdd077ff620"}, +] + +[[package]] +name = "starlette" +version = "0.49.3" +requires_python = ">=3.9" +summary = "The little ASGI library that shines." +groups = ["default"] +dependencies = [ + "anyio<5,>=3.6.2", + "typing-extensions>=4.10.0; python_version < \"3.13\"", +] +files = [ + {file = "starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f"}, + {file = "starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284"}, +] + +[[package]] +name = "textual" +version = "6.5.0" +requires_python = "<4.0,>=3.9" +summary = "Modern Text User Interface framework" +groups = ["default"] +dependencies = [ + "markdown-it-py[linkify]>=2.1.0", + "mdit-py-plugins", + "platformdirs<5,>=3.6.0", + "pygments<3.0.0,>=2.19.2", + "rich>=14.2.0", + "typing-extensions<5.0.0,>=4.4.0", +] +files = [ + {file = "textual-6.5.0-py3-none-any.whl", hash = "sha256:c5505be7fe606b8054fb88431279885f88352bddca64832f6acd293ef7d9b54f"}, + {file = "textual-6.5.0.tar.gz", hash = "sha256:e5f152cdd47db48a635d23b839721bae4d0e8b6d855e3fede7285218289294e3"}, +] + +[[package]] +name = "tomli" +version = "2.3.0" +requires_python = ">=3.8" +summary = "A lil' TOML parser" +groups = ["default", "dev"] +marker = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, + {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, + {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}, + {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}, + {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}, + {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}, + {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}, + {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}, + {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}, + {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}, + {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}, + {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}, + {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}, + {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}, + {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}, + {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}, + {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}, + {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}, + {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}, + {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}, + {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}, + {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}, + {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}, + {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}, + {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}, + {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}, + {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}, + {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}, + {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}, + {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}, + {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}, + {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}, + {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}, + {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}, + {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}, + {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}, + {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}, + {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}, + {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}, + {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}, + {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, + {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, +] + +[[package]] +name = "trogon" +version = "0.6.0" +requires_python = "<4.0.0,>=3.8.1" +summary = "Automatically generate a Textual TUI for your Click CLI" +groups = ["default"] +dependencies = [ + "click>=8.0.0", + "textual>=0.61.0", +] +files = [ + {file = "trogon-0.6.0-py3-none-any.whl", hash = "sha256:fb5b6c25acd7a0eaba8d2cd32a57f1d80c26413cea737dad7a4eebcda56060e0"}, + {file = "trogon-0.6.0.tar.gz", hash = "sha256:fd1abfeb7b15d79d6e6cfc9e724aad2a2728812e4713a744d975f133e7ec73a4"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +requires_python = ">=3.9" +summary = "Backported and Experimental Type Hints for Python 3.9+" +groups = ["default", "dev"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +requires_python = ">=3.9" +summary = "Runtime typing introspection tools" +groups = ["default"] +dependencies = [ + "typing-extensions>=4.12.0", +] +files = [ + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, +] + +[[package]] +name = "uc-micro-py" +version = "1.0.3" +requires_python = ">=3.7" +summary = "Micro subset of unicode data files for linkify-it-py projects." +groups = ["default"] +files = [ + {file = "uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a"}, + {file = "uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5"}, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +requires_python = ">=3.9" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +groups = ["default"] +files = [ + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, +] + +[[package]] +name = "uvicorn" +version = "0.38.0" +requires_python = ">=3.9" +summary = "The lightning-fast ASGI server." +groups = ["default"] +dependencies = [ + "click>=7.0", + "h11>=0.8", + "typing-extensions>=4.0; python_version < \"3.11\"", +] +files = [ + {file = "uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02"}, + {file = "uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d"}, +] + +[[package]] +name = "uvicorn" +version = "0.38.0" +extras = ["standard"] +requires_python = ">=3.9" +summary = "The lightning-fast ASGI server." +groups = ["default"] +dependencies = [ + "colorama>=0.4; sys_platform == \"win32\"", + "httptools>=0.6.3", + "python-dotenv>=0.13", + "pyyaml>=5.1", + "uvicorn==0.38.0", + "uvloop>=0.15.1; (sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"", + "watchfiles>=0.13", + "websockets>=10.4", +] +files = [ + {file = "uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02"}, + {file = "uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d"}, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +requires_python = ">=3.8.1" +summary = "Fast implementation of asyncio event loop on top of libuv" +groups = ["default"] +marker = "(sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"" +files = [ + {file = "uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c"}, + {file = "uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792"}, + {file = "uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86"}, + {file = "uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd"}, + {file = "uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2"}, + {file = "uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec"}, + {file = "uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9"}, + {file = "uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77"}, + {file = "uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21"}, + {file = "uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702"}, + {file = "uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733"}, + {file = "uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473"}, + {file = "uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42"}, + {file = "uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6"}, + {file = "uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370"}, + {file = "uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4"}, + {file = "uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2"}, + {file = "uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0"}, + {file = "uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705"}, + {file = "uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8"}, + {file = "uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d"}, + {file = "uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e"}, + {file = "uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e"}, + {file = "uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad"}, + {file = "uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142"}, + {file = "uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74"}, + {file = "uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35"}, + {file = "uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25"}, + {file = "uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6"}, + {file = "uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079"}, + {file = "uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289"}, + {file = "uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3"}, + {file = "uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c"}, + {file = "uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21"}, + {file = "uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88"}, + {file = "uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e"}, + {file = "uvloop-0.22.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4"}, + {file = "uvloop-0.22.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c"}, + {file = "uvloop-0.22.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54"}, + {file = "uvloop-0.22.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659"}, + {file = "uvloop-0.22.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743"}, + {file = "uvloop-0.22.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7"}, + {file = "uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f"}, +] + +[[package]] +name = "virtualenv" +version = "20.35.4" +requires_python = ">=3.8" +summary = "Virtual Python Environment builder" +groups = ["dev"] +dependencies = [ + "distlib<1,>=0.3.7", + "filelock<4,>=3.12.2", + "importlib-metadata>=6.6; python_version < \"3.8\"", + "platformdirs<5,>=3.9.1", + "typing-extensions>=4.13.2; python_version < \"3.11\"", +] +files = [ + {file = "virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b"}, + {file = "virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c"}, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +requires_python = ">=3.9" +summary = "Simple, modern and high performance file watching and code reload in python." +groups = ["default"] +dependencies = [ + "anyio>=3.0.0", +] +files = [ + {file = "watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c"}, + {file = "watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43"}, + {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31"}, + {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac"}, + {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d"}, + {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d"}, + {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863"}, + {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab"}, + {file = "watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82"}, + {file = "watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4"}, + {file = "watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844"}, + {file = "watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e"}, + {file = "watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5"}, + {file = "watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741"}, + {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6"}, + {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b"}, + {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14"}, + {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d"}, + {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff"}, + {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606"}, + {file = "watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701"}, + {file = "watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10"}, + {file = "watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849"}, + {file = "watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4"}, + {file = "watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e"}, + {file = "watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d"}, + {file = "watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610"}, + {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af"}, + {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6"}, + {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce"}, + {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa"}, + {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb"}, + {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803"}, + {file = "watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94"}, + {file = "watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43"}, + {file = "watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9"}, + {file = "watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9"}, + {file = "watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404"}, + {file = "watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18"}, + {file = "watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a"}, + {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219"}, + {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428"}, + {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0"}, + {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150"}, + {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae"}, + {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d"}, + {file = "watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b"}, + {file = "watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374"}, + {file = "watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0"}, + {file = "watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42"}, + {file = "watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18"}, + {file = "watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da"}, + {file = "watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051"}, + {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e"}, + {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70"}, + {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261"}, + {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620"}, + {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04"}, + {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77"}, + {file = "watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef"}, + {file = "watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf"}, + {file = "watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5"}, + {file = "watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd"}, + {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb"}, + {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5"}, + {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3"}, + {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33"}, + {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510"}, + {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05"}, + {file = "watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6"}, + {file = "watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81"}, + {file = "watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b"}, + {file = "watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a"}, + {file = "watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02"}, + {file = "watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21"}, + {file = "watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5"}, + {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7"}, + {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101"}, + {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44"}, + {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c"}, + {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc"}, + {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c"}, + {file = "watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099"}, + {file = "watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01"}, + {file = "watchfiles-1.1.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c882d69f6903ef6092bedfb7be973d9319940d56b8427ab9187d1ecd73438a70"}, + {file = "watchfiles-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6ff426a7cb54f310d51bfe83fe9f2bbe40d540c741dc974ebc30e6aa238f52e"}, + {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79ff6c6eadf2e3fc0d7786331362e6ef1e51125892c75f1004bd6b52155fb956"}, + {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c1f5210f1b8fc91ead1283c6fd89f70e76fb07283ec738056cf34d51e9c1d62c"}, + {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9c4702f29ca48e023ffd9b7ff6b822acdf47cb1ff44cb490a3f1d5ec8987e9c"}, + {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acb08650863767cbc58bca4813b92df4d6c648459dcaa3d4155681962b2aa2d3"}, + {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08af70fd77eee58549cd69c25055dc344f918d992ff626068242259f98d598a2"}, + {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c3631058c37e4a0ec440bf583bc53cdbd13e5661bb6f465bc1d88ee9a0a4d02"}, + {file = "watchfiles-1.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cf57a27fb986c6243d2ee78392c503826056ffe0287e8794503b10fb51b881be"}, + {file = "watchfiles-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d7e7067c98040d646982daa1f37a33d3544138ea155536c2e0e63e07ff8a7e0f"}, + {file = "watchfiles-1.1.1-cp39-cp39-win32.whl", hash = "sha256:6c9c9262f454d1c4d8aaa7050121eb4f3aea197360553699520767daebf2180b"}, + {file = "watchfiles-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:74472234c8370669850e1c312490f6026d132ca2d396abfad8830b4f1c096957"}, + {file = "watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3"}, + {file = "watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2"}, + {file = "watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d"}, + {file = "watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b"}, + {file = "watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88"}, + {file = "watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336"}, + {file = "watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24"}, + {file = "watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49"}, + {file = "watchfiles-1.1.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdab464fee731e0884c35ae3588514a9bcf718d0e2c82169c1c4a85cc19c3c7f"}, + {file = "watchfiles-1.1.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3dbd8cbadd46984f802f6d479b7e3afa86c42d13e8f0f322d669d79722c8ec34"}, + {file = "watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5524298e3827105b61951a29c3512deb9578586abf3a7c5da4a8069df247cccc"}, + {file = "watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b943d3668d61cfa528eb949577479d3b077fd25fb83c641235437bc0b5bc60e"}, + {file = "watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2"}, +] + +[[package]] +name = "websockets" +version = "15.0.1" +requires_python = ">=3.9" +summary = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +groups = ["default"] +files = [ + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, + {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, + {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, + {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, + {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, + {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, + {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, + {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, + {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, + {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, + {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, + {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, + {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, +] diff --git a/pyproject.toml b/pyproject.toml index bbaad64..434e8ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,127 +1,43 @@ [project] -name = "bmg_module" -version = "0.0.1" -description = "Module for BMG microplate readers" -authors = [{name = "Casey Stone", email="cstone@anl.gov"}] +name = "brooks_xpeel_module" +version = "1.1.0" +description = "Software for automatting a A4S sealer device" +authors = [ + {name = "Doga Ozgulbas", email="dozgulbas@anl.gov"}, + {name = "Ryan D. Lewis", email="ryan.lewis@anl.gov"}, + {name = "Tobias Ginsburg", email = "tginsburg@anl.gov"}, +] dependencies = [ "fastapi>=0.103", "uvicorn>=0.14.0", - "ad_sdl.wei>=0.7.2,<8.0", + "pyserial", + "madsci.node_module~=0.5.0", + "madsci.client~=0.5.0", + "madsci.common~=0.5.0", "pytest", "pywin32", "comtypes", "starlette", ] + requires-python = ">=3.9.1" readme = "README.md" license = {text = "MIT"} [project.urls] -homepage = "https://github.com/AD-SDL/python_template_module" - -##################### -# Development Tools # -##################### - -[tool.ruff] -# https://docs.astral.sh/ruff/configuration/ - -# Exclude a variety of commonly ignored directories. -exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".mypy_cache", - ".nox", - ".pants.d", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - "__pypackages__", - "_build", - "buck-out", - "build", - "dist", - "node_modules", - "venv", - "docs", -] - -# Same as Black. -line-length = 88 -indent-width = 4 - -# Assume Python 3.8 -target-version = "py38" - -[tool.ruff.lint] -# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. -select = [ - # pycodestyle - "E", - # Pyflakes - "F", - # pyupgrade - # "UP", - # flake8-bugbear - "B", - # flake8-simplify - # "SIM", - # isort - "I", - # Warning - "W", - # pydocstyle - "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", - # ruff - # "RUF" -] -ignore = [ - "E501" # Line too long -] - -# Allow fix for all enabled rules (when `--fix`) is provided. -fixable = ["ALL"] -unfixable = [] - -# Allow unused variables when underscore-prefixed. -dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" - -[tool.ruff.format] -# Like Black, use double quotes for strings. -quote-style = "double" - -# Like Black, indent with spaces, rather than tabs. -indent-style = "space" - -# Like Black, respect magic trailing commas. -skip-magic-trailing-comma = false - -# Like Black, automatically detect the appropriate line ending. -line-ending = "auto" - -[tool.pytest.ini_options] -# https://docs.pytest.org/en/stable/customize.html -addopts = "-x" -junit_family="xunit1" -filterwarnings = [ - "ignore::DeprecationWarning", - "ignore::pottery.exceptions.InefficientAccessWarning", +homepage = "https://github.com/AD-SDL/brooks_xpeel_module" + +###################### +# Build Info + Tools # +###################### +[build-system] +requires = ["setuptools>=61", "wheel"] +build-backend = "setuptools.build_meta" + +[dependency-groups] +dev = [ + "pre-commit>=4.2.0", + "ruff>=0.11.7", + "pytest", ] - -[tool.mypy] -# https://mypy.readthedocs.io/en/stable/config_file.html#using-a-pyproject-toml -show_error_codes = true -check_untyped_defs = true -follow_imports = "normal" -strict_optional = true -plugins = ["pydantic.mypy"] -strict = true -disallow_untyped_defs = true -implicit_reexport = true diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..6f14b29 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,125 @@ +# https://docs.astral.sh/ruff/configuration/ + +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + ".venv", + "docs", +] + +# Same as Black. +line-length = 88 +indent-width = 4 + +# Assume Python 3.9 +target-version = "py39" + +[lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +select = [ + # pycodestyle + "E", + # Pyflakes + "F", + # pyupgrade + "UP", + # flake8-bugbear + "B", + # flake8-simplify + "SIM", + # isort + "I", + # Warning + "W", + # pydocstyle + "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", + # ruff + "RUF", + # McCabe Complexity + "C90", + # pep8-naming (N) + "N", + # flake8-2020 (YTT) + "YTT", + # flake8-annotations (ANN) + "ANN", + # flake8-async + "ASYNC", + # flake8-bandit + "S", + # flake8-commas + "COM", + # flake8-use-pathlib + "PTH", + # FastAPI + "FAST", + # flake8-logging + "LOG", + # flake8-comprehensions + "C4", + # flake8-pie + "PIE", + # flake8-return + "RET", + # Pylint + "PL", + # eradicate + "ERA", + # refurb + "FURB", + # flake-8-unused-arguments + "ARG", + # flake-8-print + "T20", +] +ignore = [ + "E501", # Line too long + "B006", # Do not use mutable data structures for argument defaults + "ANN401", # Don't flag Any types + "COM812", # Don't fight over trailing commas + "PLR0913", # Allow for a reasonable number of arguments + "PLR2004", # Magic value used in comparison +] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[lint.per-file-ignores] +"**/tests/**/*.py" = ["S101"] + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" \ No newline at end of file diff --git a/src/bmg_interface.py b/src/bmg_interface.py index 88110ce..845e641 100644 --- a/src/bmg_interface.py +++ b/src/bmg_interface.py @@ -3,68 +3,91 @@ """ import ctypes +import threading import time from pathlib import Path +from typing import Any, Optional import comtypes.client import pythoncom +from madsci.client.event_client import EventClient +from madsci.client.resource_client import ResourceClient +from madsci.common.types.resource_types import Slot + +"""TODO: +- when to edit resource client and plate carrier? - there's not really a state change of the plate like in the peeler""" class BmgCom: - def __init__(self, control_name=None): + """Class to communicate with BMG microplate readers via ActiveX COM interface.""" + + def __init__( + self, + control_name: str, + resource_client: ResourceClient = None, + plate_carrier: Optional[Slot] = None, + logger: EventClient = None, + ) -> None: """Initializes and opens the connection the BMG plate reader""" + + self.control_name = control_name + self.resource_client = resource_client + self.plate_carrier = plate_carrier + self.logger = logger or EventClient() + self.serial_lock = threading.Lock() + self._device_lock = threading.Lock() # TODO: what is this for? + pythoncom.CoInitialize() self.com = comtypes.client.CreateObject("BMG_ActiveX.BMGRemoteControl") if control_name: - self.open_connection(control_name) + self.open_connection() - def open_connection(self, control_name): + def open_connection(self) -> None: """Open a connection to the BMG plate reader""" - ep = ctypes.c_char_p(control_name.encode('ascii')) + ep = ctypes.c_char_p(self.control_name.encode("ascii")) res = self.com.OpenConnection(ep) if res: raise Exception(f"OpenConnection failed: {res}") - def close_connection(self): + def close_connection(self) -> None: """Close the connection to the BMG plate reader""" res = self.com.CloseConnection() if res: raise Exception(f"CloseConnection failed: {res}") - def version(self): + def version(self) -> str: """Returns the BMG instrument version""" - version = self.com.GetVersion() - return version + return self.com.GetVersion() - def dummy(self): + def dummy(self) -> None: """Use this to test if a connection to a BMG plate reader is active""" - self.exec('Dummy') + self.exec("Dummy") - def status(self): + def status(self) -> str: """Returns the current status of the BMG plate reader""" item = ctypes.c_char_p(b"Status") status = self.com.GetInfo(item) - return status.strip() if isinstance(status, str) else 'unknown' + return status.strip() if isinstance(status, str) else "unknown" - def error(self): + def error(self) -> str: """Returns any errors on the BMG plate reader""" item = ctypes.c_char_p(b"Error") status = self.com.GetInfo(item) - return status.strip() if isinstance(status, str) else 'unknown' + return status.strip() if isinstance(status, str) else "unknown" - def init(self): + def init(self) -> None: """Initializes the BMG plate reader""" - self.exec('Init') + self.exec("Init") - def plate_in(self): + def plate_in(self) -> None: """Closes the plate tray on the BMG plate reader""" - self.exec('PlateIn') + self.exec("PlateIn") - def plate_out(self): + def plate_out(self) -> None: """Opens the plate tray on the BMG plate reader""" - self.exec('PlateOut') + self.exec("PlateOut") - def set_temp(self, temp:float): + def set_temp(self, temp: float) -> None: """Sets the temperature on the BMG plate reader. Allowed values: @@ -79,19 +102,18 @@ def set_temp(self, temp:float): - If more than one decimal point are included, will round to nearest valid temp input. """ nominal_temp = str(temp) - self.exec('Temp', nominal_temp) + self.exec("Temp", nominal_temp) def run_assay( self, - protocol_name:str, - protocol_database_path:str, - data_output_directory:str, - data_output_file_name:str = None, - plate_id1:int = 1, # these plate IDs are optional - plate_id2:int = 2, # but why? what do they do? - plate_id3:int = 3, # and why are there three? curious. - - ): + protocol_name: str, + protocol_database_path: str, + data_output_directory: str, + data_output_file_name: Optional[str] = None, + plate_id1: int = 1, # these plate IDs are optional + plate_id2: int = 2, # but why? what do they do? + plate_id3: int = 3, # and why are there three? curious. + ) -> str: """Runs an assay on the BMG plate reader""" # give the data file a unique name if no name is specified @@ -103,7 +125,7 @@ def run_assay( data_file_path = data_dir / data_output_file_name self.exec( - 'Run', + "Run", protocol_name, protocol_database_path, data_output_directory, @@ -111,28 +133,24 @@ def run_assay( plate_id2, plate_id3, data_output_directory, - data_output_file_name + data_output_file_name, ) return data_file_path - def isBusy(self): + def is_busy(self) -> bool: """Returns True if BMG is busy, False if not busy""" - return self.status() == 'Busy' + return self.status() == "Busy" - def exec(self, cmd, *args): + def exec(self, cmd: str, *args: Any) -> None: """Executed a command over the established connection with the BMG plate reader""" - args = tuple((cmd, *args)) - print(args) - res = self.com.ExecuteAndWait(args) - if res: - raise Exception(f"command {cmd} failed: {res}") - -if __name__ == '__main__': - com = BmgCom("CLARIOstar") - print(f"BMG LABTECH Remote Control Version: {com.version()}") - - - + with self._device_lock: + args = (cmd, *args) + res = self.com.ExecuteAndWait(args) + if res: + raise Exception(f"command {cmd} failed: {res}") +if __name__ == "__main__": + com = BmgCom("CLARIOstar") + print(f"BMG LABTECH Remote Control Version: {com.version()}") # noqa: T201 diff --git a/src/bmg_rest_node.py b/src/bmg_rest_node.py index de47144..46edda8 100644 --- a/src/bmg_rest_node.py +++ b/src/bmg_rest_node.py @@ -2,120 +2,160 @@ REST-based node for BMG microplate readers that interfaces with WEI """ -from typing import Annotated - -from starlette.datastructures import State -from wei.modules.rest_module import RESTModule -from wei.types.step_types import ( - ActionRequest, - StepFileResponse, - StepResponse, - StepStatus, +from pathlib import Path +from typing import Annotated, Optional + +from madsci.common.types.action_types import ActionFailed +from madsci.common.types.node_types import RestNodeConfig +from madsci.common.types.resource_types import ( + Slot, ) +from madsci.node_module.helpers import action +from madsci.node_module.rest_node_module import RestNode -from bmg_interface import BmgCom # import the bmg interface +from bmg_interface import BmgCom -rest_module = RESTModule( - name="bmg_module", - version="0.0.1", - description="A REST node to control the BMG VANTAstar microplate reader", - model="bmg", -) -# add arguments -rest_module.arg_parser.add_argument( - "--output_path", type=str, help="data output directory path for bmg data", default="C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" -) -rest_module.arg_parser.add_argument( - "--db_directory_path", type=str, help="path to directory where assay protocol files are stored", default="C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" -) -# parse the arguments -args = rest_module.arg_parser.parse_args() +""" +TODOs: +- figure out pdm dependencies with 32 bit python + Q: why does creating a .venv with the 32-bit python then pip installing not work? + only using ' -m pip install ... seems to work but it's not in the activated .venv .... +- always says ready even though workflow step shows it's still running for the correct amount of time -# OPEN TRAY ACTION -@rest_module.action( - name="open", description="Open the bmg plate tray" -) -def open( - state: State, - action: ActionRequest, -) -> StepResponse: - """Opens the BMG plate tray""" +- MADSci second workflow step sent always fails after first one works + # NOTE: can't close connection after each step becuase closing the connection closes the device door +- MADSci: clicking show editable workflow step causes Squid dashboard page to freeze +- MADSci: something is wrong with passing paths in through the command line args. Only works when default paths are set in BMGNodeConfig +""" - state.bmg = BmgCom("CLARIOstar") - state.bmg.plate_out() - return StepResponse.step_succeeded() +class BMGNodeConfig(RestNodeConfig): + """Configuration for the BMG node.""" -# CLOSE TRAY ACTION -@rest_module.action( - name="close", description="Close the BMG plate tray" -) -def close( - state: State, - action: ActionRequest, -) -> StepResponse: - """Closes the BMG plate tray""" + output_path: str = "C:\\Users\\RPL\\TEST" + """Data output directory path for bmg data""" + db_directory_path: str = "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" + """Path to directory where assay protocol files are stored""" - state.bmg = BmgCom("CLARIOstar") - state.bmg.plate_in() - return StepResponse.step_succeeded() +class BMGNode(RestNode): + """A node to control the BMG VANTAstar microplate reader""" -# SET TEMP ACTION -@rest_module.action( - name="set_temp", description="Set the temperature" -) -def set_temp( - state: State, - action: ActionRequest, - temp: Annotated[float, "temperature in celsius. 00.0 (off), 00.1 (off with temp monitoring), and 25.0-45.0 deg C are valid inputs"] -) -> StepResponse: - """Sets the temperature on the BMG microplate reader""" - - temp = float(temp) - if temp in {0.0, 0.1} or 25.0 <= temp <= 45.0: - # temp input is valid - state.bmg = BmgCom("CLARIOstar") - state.bmg.set_temp(temp=temp) - return StepResponse.step_succeeded() - else: - # temp input is not valid - return StepResponse.step_failed(error="Invalid temperature input value") - - -# RUN ASSAY ACTION -@rest_module.action( - name="run_assay", description="run an assay on the BMG VANTAstar plate reader" -) -def run_assay( - state: State, - action: ActionRequest, - assay_name: Annotated[str, "assay to run"], - data_output_file_name: Annotated[str, "data output file name (ex. data.txt). Will default to .txt (ex. 1731706249.txt) if no file name is entered."] = None, -) -> StepFileResponse: - """Runs an assay on the BMG plate reader""" - - # run the assay - state.bmg = BmgCom("CLARIOstar") - data_file_path = state.bmg.run_assay( - protocol_name=assay_name, - protocol_database_path=args.db_directory_path, - data_output_directory=args.output_path, - data_output_file_name=data_output_file_name, - ) + bmg: BmgCom = None + config_model = BMGNodeConfig + config: BMGNodeConfig = BMGNodeConfig() + module_version = "0.0.1" + + def startup_handler(self) -> None: + """Called to (re)initialize the node. Should be used to open connections to devices or initialize any other resources.""" + + self.init_resource_templates() + self.create_resources() + self.bmg = None - # return the assay results file - return StepFileResponse( - StepStatus.SUCCEEDED, - files={"assay_result": str(data_file_path)} - ) + def init_resource_templates(self) -> None: + """Initialize resource templates for the node module.""" + + self.resource_client.create_template( + resource=Slot( + resource_class="bmg_plate_nest", + resource_description="The plate nest for a BMG microplate reader", + ), + template_name="bmg_plate_nest", + description="Template of a BMG microplate reader plate nest", + tags=["PlateNest", "ANSI/SLAS"], + ) + def create_resources(self) -> None: + """Create resources for the node module.""" -rest_module.start() + self.plate_carrier = self.resource_client.create_resource_from_template( + "bmg_plate_nest", + resource_name=f"{self.node_definition.node_name}_plate_nest", + ) + def shutdown_handler(self) -> None: + """Called to clean up resources before the node is shut down.""" + try: + if self.bmg: + self.bmg.close_connection() + self.bmg = None + except Exception as err: + self.logger.log_error(f"Error during BMGNode shutdown: {err}") + + @action(name="open") + def open(self) -> None: + """Opens the BMG plate tray""" + + self.bmg = BmgCom( + "CLARIOstar", + resource_client=self.resource_client, + plate_carrier=self.plate_carrier, + logger=self.logger, + ) + self.bmg.plate_out() + @action(name="close") + def close(self) -> None: + """Closes the BMG plate tray""" + self.bmg = BmgCom( + "CLARIOstar", + resource_client=self.resource_client, + plate_carrier=self.plate_carrier, + logger=self.logger, + ) + self.bmg.plate_in() + + @action(name="set_temp") + def set_temp(self, temp: float) -> None | ActionFailed: + """Sets the temperature on the BMG microplate reader""" + + temp = float(temp) + if temp in {0.0, 0.1} or 25.0 <= temp <= 45.0: + # temp input is valid + self.bmg = BmgCom( + "CLARIOstar", + resource_client=self.resource_client, + plate_carrier=self.plate_carrier, + logger=self.logger, + ) + self.bmg.set_temp(temp=temp) + return None + # temp input is not valid (fail action, don't put node in error state) + return ActionFailed(errors=["Invalid temperature input value"]) + + @action(name="run_assay") + def run_assay( + self, + assay_name: str, + data_output_file_name: Annotated[ + Optional[str], + "data output file name (ex. data.txt). Will default to .txt (ex. 1731706249.txt) if no file name is entered.", + ] = None, + ) -> Annotated[Path, "Data .txt file returned by the BMG microplate reader"]: + """Runs an assay on the BMG plate reader""" + + data_file_path = None + + # run the assay + self.bmg = BmgCom( + "CLARIOstar", + resource_client=self.resource_client, + plate_carrier=self.plate_carrier, + logger=self.logger, + ) + data_file_path = self.bmg.run_assay( + protocol_name=assay_name, + protocol_database_path=self.config.db_directory_path, + data_output_directory=self.config.output_path, + data_output_file_name=data_output_file_name, + ) + return Path(data_file_path) +if __name__ == "__main__": + bmg_node = BMGNode() + bmg_node.start_node() diff --git a/src/bmg_rest_node_WEI.py b/src/bmg_rest_node_WEI.py new file mode 100644 index 0000000..de47144 --- /dev/null +++ b/src/bmg_rest_node_WEI.py @@ -0,0 +1,121 @@ +""" +REST-based node for BMG microplate readers that interfaces with WEI +""" + +from typing import Annotated + +from starlette.datastructures import State +from wei.modules.rest_module import RESTModule +from wei.types.step_types import ( + ActionRequest, + StepFileResponse, + StepResponse, + StepStatus, +) + +from bmg_interface import BmgCom # import the bmg interface + +rest_module = RESTModule( + name="bmg_module", + version="0.0.1", + description="A REST node to control the BMG VANTAstar microplate reader", + model="bmg", +) +# add arguments +rest_module.arg_parser.add_argument( + "--output_path", type=str, help="data output directory path for bmg data", default="C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" +) +rest_module.arg_parser.add_argument( + "--db_directory_path", type=str, help="path to directory where assay protocol files are stored", default="C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" +) +# parse the arguments +args = rest_module.arg_parser.parse_args() + + +# OPEN TRAY ACTION +@rest_module.action( + name="open", description="Open the bmg plate tray" +) +def open( + state: State, + action: ActionRequest, +) -> StepResponse: + """Opens the BMG plate tray""" + + state.bmg = BmgCom("CLARIOstar") + state.bmg.plate_out() + return StepResponse.step_succeeded() + + +# CLOSE TRAY ACTION +@rest_module.action( + name="close", description="Close the BMG plate tray" +) +def close( + state: State, + action: ActionRequest, +) -> StepResponse: + """Closes the BMG plate tray""" + + state.bmg = BmgCom("CLARIOstar") + state.bmg.plate_in() + return StepResponse.step_succeeded() + + +# SET TEMP ACTION +@rest_module.action( + name="set_temp", description="Set the temperature" +) +def set_temp( + state: State, + action: ActionRequest, + temp: Annotated[float, "temperature in celsius. 00.0 (off), 00.1 (off with temp monitoring), and 25.0-45.0 deg C are valid inputs"] +) -> StepResponse: + """Sets the temperature on the BMG microplate reader""" + + temp = float(temp) + if temp in {0.0, 0.1} or 25.0 <= temp <= 45.0: + # temp input is valid + state.bmg = BmgCom("CLARIOstar") + state.bmg.set_temp(temp=temp) + return StepResponse.step_succeeded() + else: + # temp input is not valid + return StepResponse.step_failed(error="Invalid temperature input value") + + +# RUN ASSAY ACTION +@rest_module.action( + name="run_assay", description="run an assay on the BMG VANTAstar plate reader" +) +def run_assay( + state: State, + action: ActionRequest, + assay_name: Annotated[str, "assay to run"], + data_output_file_name: Annotated[str, "data output file name (ex. data.txt). Will default to .txt (ex. 1731706249.txt) if no file name is entered."] = None, +) -> StepFileResponse: + """Runs an assay on the BMG plate reader""" + + # run the assay + state.bmg = BmgCom("CLARIOstar") + data_file_path = state.bmg.run_assay( + protocol_name=assay_name, + protocol_database_path=args.db_directory_path, + data_output_directory=args.output_path, + data_output_file_name=data_output_file_name, + ) + + # return the assay results file + return StepFileResponse( + StepStatus.SUCCEEDED, + files={"assay_result": str(data_file_path)} + ) + + +rest_module.start() + + + + + + From bfae30cdda756739206c5922da06b1be6f88eb5a Mon Sep 17 00:00:00 2001 From: caseystone Date: Tue, 4 Nov 2025 20:48:43 +0000 Subject: [PATCH 02/28] Renamed module --- Dockerfile | 14 +++++++------- compose.yaml | 28 ++++++++++++++-------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Dockerfile b/Dockerfile index 18e5b85..1f33ec4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM ghcr.io/ad-sdl/wei # TODO: update labels, if neccessary -LABEL org.opencontainers.image.source=https://github.com/AD-SDL/python_template_module +LABEL org.opencontainers.image.source=https://github.com/AD-SDL/bmg_module LABEL org.opencontainers.image.description="A template python module that demonstrates basic WEI module functionality." LABEL org.opencontainers.image.licenses=MIT @@ -9,17 +9,17 @@ LABEL org.opencontainers.image.licenses=MIT # Module specific logic goes below here # ######################################### -RUN mkdir -p python_template_module +RUN mkdir -p bmg_module -COPY ./src python_template_module/src -COPY ./README.md python_template_module/README.md -COPY ./pyproject.toml python_template_module/pyproject.toml +COPY ./src bmg_module/src +COPY ./README.md bmg_module/README.md +COPY ./pyproject.toml bmg_module/pyproject.toml RUN --mount=type=cache,target=/root/.cache \ - pip install ./python_template_module + pip install ./bmg_module # TODO: Add any device-specific container configuration/setup here -CMD ["python", "python_template_module.py"] +CMD ["python", "bmg_module.py"] ######################################### diff --git a/compose.yaml b/compose.yaml index a644043..621ce28 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,25 +1,25 @@ -name: python_template_module +name: bmg_module services: - python_template_module: - container_name: python_template_module - image: ghcr.io/ad-sdl/python_template_module + bmg_module: + container_name: bmg_module + image: ghcr.io/ad-sdl/bmg_module build: context: . tags: - - ghcr.io/ad-sdl/python_template_module:latest - - ghcr.io/ad-sdl/python_template_module:dev + - ghcr.io/ad-sdl/bmg_module:latest + - ghcr.io/ad-sdl/bmg_module:dev volumes: - - ./src:/home/app/python_template_module/src - - ./tests:/home/app/python_template_module/tests - command: python -m python_template_module --port 2000 + - ./src:/home/app/bmg_module/src + - ./tests:/home/app/bmg_module/tests + command: python -m bmg_module --port 2000 ports: - 2000:2000 ##################### # WEI Core Services # ##################### - python_template_wei_server: + bmg_wei_server: image: ghcr.io/ad-sdl/wei - container_name: python_template_wei_server + container_name: bmg_wei_server ports: - 8000:8000 environment: @@ -30,10 +30,10 @@ services: - ./workcells:/workcells command: python3 -m wei.server --workcell /workcells/test.workcell.yaml depends_on: - - python_template_wei_redis - python_template_wei_redis: + - bmg_wei_redis + bmg_wei_redis: image: redis - container_name: python_template_wei_redis + container_name: bmg_wei_redis ports: - 6379:6379 command: redis-server --save 60 1 --loglevel warning From add042eecccaeeac75e997821c6eb9df19454923 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Tue, 4 Nov 2025 14:59:44 -0600 Subject: [PATCH 03/28] cleanup --- definitions/bmg.node.yaml | 6 +- ruff.toml | 2 +- src/bmg_rest_node.py | 4 +- src/bmg_rest_node_WEI.py | 121 -------------------------------------- 4 files changed, 7 insertions(+), 126 deletions(-) delete mode 100644 src/bmg_rest_node_WEI.py diff --git a/definitions/bmg.node.yaml b/definitions/bmg.node.yaml index bee566e..8f3be69 100644 --- a/definitions/bmg.node.yaml +++ b/definitions/bmg.node.yaml @@ -1,7 +1,7 @@ node_name: bmg -node_id: -node_description: A node definition for BMG plate reader +node_id: +node_description: A node definition for BMG plate reader node_type: device module_name: bmg_module module_version: 1.1.0 -capabilities: null \ No newline at end of file +capabilities: null diff --git a/ruff.toml b/ruff.toml index 6f14b29..db2463c 100644 --- a/ruff.toml +++ b/ruff.toml @@ -122,4 +122,4 @@ indent-style = "space" skip-magic-trailing-comma = false # Like Black, automatically detect the appropriate line ending. -line-ending = "auto" \ No newline at end of file +line-ending = "auto" diff --git a/src/bmg_rest_node.py b/src/bmg_rest_node.py index 46edda8..740ae52 100644 --- a/src/bmg_rest_node.py +++ b/src/bmg_rest_node.py @@ -23,6 +23,8 @@ - always says ready even though workflow step shows it's still running for the correct amount of time +- add temperature monitoring to custom state function and also interface? + - MADSci second workflow step sent always fails after first one works # NOTE: can't close connection after each step becuase closing the connection closes the device door - MADSci: clicking show editable workflow step causes Squid dashboard page to freeze @@ -109,7 +111,7 @@ def close(self) -> None: self.bmg.plate_in() @action(name="set_temp") - def set_temp(self, temp: float) -> None | ActionFailed: + def set_temp(self, temp: float) -> None: """Sets the temperature on the BMG microplate reader""" temp = float(temp) diff --git a/src/bmg_rest_node_WEI.py b/src/bmg_rest_node_WEI.py deleted file mode 100644 index de47144..0000000 --- a/src/bmg_rest_node_WEI.py +++ /dev/null @@ -1,121 +0,0 @@ -""" -REST-based node for BMG microplate readers that interfaces with WEI -""" - -from typing import Annotated - -from starlette.datastructures import State -from wei.modules.rest_module import RESTModule -from wei.types.step_types import ( - ActionRequest, - StepFileResponse, - StepResponse, - StepStatus, -) - -from bmg_interface import BmgCom # import the bmg interface - -rest_module = RESTModule( - name="bmg_module", - version="0.0.1", - description="A REST node to control the BMG VANTAstar microplate reader", - model="bmg", -) -# add arguments -rest_module.arg_parser.add_argument( - "--output_path", type=str, help="data output directory path for bmg data", default="C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" -) -rest_module.arg_parser.add_argument( - "--db_directory_path", type=str, help="path to directory where assay protocol files are stored", default="C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" -) -# parse the arguments -args = rest_module.arg_parser.parse_args() - - -# OPEN TRAY ACTION -@rest_module.action( - name="open", description="Open the bmg plate tray" -) -def open( - state: State, - action: ActionRequest, -) -> StepResponse: - """Opens the BMG plate tray""" - - state.bmg = BmgCom("CLARIOstar") - state.bmg.plate_out() - return StepResponse.step_succeeded() - - -# CLOSE TRAY ACTION -@rest_module.action( - name="close", description="Close the BMG plate tray" -) -def close( - state: State, - action: ActionRequest, -) -> StepResponse: - """Closes the BMG plate tray""" - - state.bmg = BmgCom("CLARIOstar") - state.bmg.plate_in() - return StepResponse.step_succeeded() - - -# SET TEMP ACTION -@rest_module.action( - name="set_temp", description="Set the temperature" -) -def set_temp( - state: State, - action: ActionRequest, - temp: Annotated[float, "temperature in celsius. 00.0 (off), 00.1 (off with temp monitoring), and 25.0-45.0 deg C are valid inputs"] -) -> StepResponse: - """Sets the temperature on the BMG microplate reader""" - - temp = float(temp) - if temp in {0.0, 0.1} or 25.0 <= temp <= 45.0: - # temp input is valid - state.bmg = BmgCom("CLARIOstar") - state.bmg.set_temp(temp=temp) - return StepResponse.step_succeeded() - else: - # temp input is not valid - return StepResponse.step_failed(error="Invalid temperature input value") - - -# RUN ASSAY ACTION -@rest_module.action( - name="run_assay", description="run an assay on the BMG VANTAstar plate reader" -) -def run_assay( - state: State, - action: ActionRequest, - assay_name: Annotated[str, "assay to run"], - data_output_file_name: Annotated[str, "data output file name (ex. data.txt). Will default to .txt (ex. 1731706249.txt) if no file name is entered."] = None, -) -> StepFileResponse: - """Runs an assay on the BMG plate reader""" - - # run the assay - state.bmg = BmgCom("CLARIOstar") - data_file_path = state.bmg.run_assay( - protocol_name=assay_name, - protocol_database_path=args.db_directory_path, - data_output_directory=args.output_path, - data_output_file_name=data_output_file_name, - ) - - # return the assay results file - return StepFileResponse( - StepStatus.SUCCEEDED, - files={"assay_result": str(data_file_path)} - ) - - -rest_module.start() - - - - - - From 4b43a8c43c175958d8820cbfd607dc24956cc049 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Tue, 4 Nov 2025 17:33:36 -0600 Subject: [PATCH 04/28] after debug session, task to add persistent thread --- src/bmg_interface.py | 9 ++++----- src/bmg_rest_node.py | 38 ++++++++++++++++++++++---------------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/bmg_interface.py b/src/bmg_interface.py index 845e641..666e02b 100644 --- a/src/bmg_interface.py +++ b/src/bmg_interface.py @@ -144,11 +144,10 @@ def is_busy(self) -> bool: def exec(self, cmd: str, *args: Any) -> None: """Executed a command over the established connection with the BMG plate reader""" - with self._device_lock: - args = (cmd, *args) - res = self.com.ExecuteAndWait(args) - if res: - raise Exception(f"command {cmd} failed: {res}") + args = (cmd, *args) + res = self.com.ExecuteAndWait(args) + if res: + raise Exception(f"command {cmd} failed: {res}") if __name__ == "__main__": diff --git a/src/bmg_rest_node.py b/src/bmg_rest_node.py index 740ae52..5155927 100644 --- a/src/bmg_rest_node.py +++ b/src/bmg_rest_node.py @@ -22,13 +22,17 @@ only using ' -m pip install ... seems to work but it's not in the activated .venv .... - always says ready even though workflow step shows it's still running for the correct amount of time + - TASK: open an issue for this. Node status remains ready when it should be busy - add temperature monitoring to custom state function and also interface? - MADSci second workflow step sent always fails after first one works # NOTE: can't close connection after each step becuase closing the connection closes the device door -- MADSci: clicking show editable workflow step causes Squid dashboard page to freeze -- MADSci: something is wrong with passing paths in through the command line args. Only works when default paths are set in BMGNodeConfig +- MADSci: clicking show editable workflow step causes Squid dashboard page to freeze -- TASK: open an issue on MADsci repo +- MADSci: something is wrong with passing paths in through the command line args. Only works when default paths are set in BMGNodeConfig + TASK: make these into Path types, not string and test. Probably the double slash when passing in string is the issue + +TASK: spin up thread in rest node init that all actions can talk to. Kill the thread on shutdown. """ @@ -40,6 +44,7 @@ class BMGNodeConfig(RestNodeConfig): db_directory_path: str = "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" """Path to directory where assay protocol files are stored""" + class BMGNode(RestNode): """A node to control the BMG VANTAstar microplate reader""" @@ -49,12 +54,22 @@ class BMGNode(RestNode): config: BMGNodeConfig = BMGNodeConfig() module_version = "0.0.1" + def __init__(self) -> None: + """Initializes the BMG node.""" + super().__init__() + self.bmg = BmgCom( + "CLARIOstar", + resource_client=self.resource_client, + # plate_carrier=self.plate_carrier, + logger=self.logger, + ) + def startup_handler(self) -> None: """Called to (re)initialize the node. Should be used to open connections to devices or initialize any other resources.""" self.init_resource_templates() self.create_resources() - self.bmg = None + def init_resource_templates(self) -> None: """Initialize resource templates for the node module.""" @@ -89,26 +104,17 @@ def shutdown_handler(self) -> None: @action(name="open") def open(self) -> None: """Opens the BMG plate tray""" - - self.bmg = BmgCom( - "CLARIOstar", - resource_client=self.resource_client, - plate_carrier=self.plate_carrier, - logger=self.logger, - ) + self.logger.log_info("Opening BMG plate tray") self.bmg.plate_out() + self.logger.log_info("BMG plate tray opened") @action(name="close") def close(self) -> None: """Closes the BMG plate tray""" - self.bmg = BmgCom( - "CLARIOstar", - resource_client=self.resource_client, - plate_carrier=self.plate_carrier, - logger=self.logger, - ) + self.logger.log_info("Closing BMG plate tray") self.bmg.plate_in() + self.logger.log_info("BMG plate tray closed") @action(name="set_temp") def set_temp(self, temp: float) -> None: From fb87571927fda00534c33136af647c962a13ea07 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Wed, 5 Nov 2025 12:26:00 -0600 Subject: [PATCH 05/28] progress made on threads, debugging needed --- src/bmg_object_thread.py | 115 +++++++++++++++++++++++++++++++++++++++ src/bmg_rest_node.py | 54 +++++++++++++----- 2 files changed, 156 insertions(+), 13 deletions(-) create mode 100644 src/bmg_object_thread.py diff --git a/src/bmg_object_thread.py b/src/bmg_object_thread.py new file mode 100644 index 0000000..6e54995 --- /dev/null +++ b/src/bmg_object_thread.py @@ -0,0 +1,115 @@ +import threading +import queue + +from bmg_interface import BmgCom + +class BMGThread(threading.Thread): + """Dedicated thread for BMG communication.""" + + def __init__(self, resource_client, plate_carrier, logger): + super().__init__(daemon=True) + self.resource_client = resource_client + self.plate_carrier = plate_carrier + self.logger = logger + + # communication queue + self.command_queue = queue.Queue() + self.response_queue = queue.Queue() + self.shutdown_event = threading.Event() + + self.bmg = None + + def run(self): + """Main thread loop for BMG communication.""" + try: + # initialize BMG communication + self.bmg = BmgCom( + "CLARIOstar", + resource_client=self.resource_client, + plate_carrier=self.plate_carrier, + logger=self.logger, + ) + self.logger.log_info("BMG communication thread started.") + + while not self.shutdown_event.is_set(): + # self.logger.log_info("Waiting for command...") # TESTING + try: + command = self.command_queue.get(timeout=1) # wait for command + self.logger.log_info(f"Processing command: {command}") + + # process command + action = command.get("action") + # TESTING + self.logger.log_info(f"ACTION FOUND IN THREAD RUN FUNCTION: {action}") + result = {"success": False, "data": None, "error": None} + + try: + if action == "plate_out": + self.bmg.plate_out() + result["success"] = True + elif action == "plate_in": + self.bmg.plate_in() + result["success"] = True + else: + result["error"] = f"Unknown action: {action}" + + except Exception as e: + result["error"] = str(e) + self.logger.log_error(f"Error processing command {action}: {e}") + + # Send response back + self.response_queue.put(result) + + except queue.Empty: + continue # no command received, loop again + + + except Exception as e: + self.logger.log_error(f"Error in BMG thread: {e}") + return # TODO: should I be returning here? - kills the thread if there's an error during init? + + finally: + # clean up BMG communication + if self.bmg: + try: + self.bmg.close_connection() + self.bmg = None + self.logger.log_info("BMG connection closed.") + except Exception as e: + self.logger.log_error(f"Error closing BMG connection: {e}") + + def send_command(self, command: dict, timeout: float = 300.0) -> dict: + """Send a command to the BMG thread and wait for a response. + + TODO: how long should the timeout be? + + Args: + command (dict): Command to send to the BMG thread. + timeout (float): Time to wait for a response. + + Returns: + dict: Response from the BMG thread. + + """ + # TESTING + self.logger.log_info(f"COMMAND IN THREAD SEND COMMAND FUNCTION: {command}") + + self.command_queue.put(command) + try: + response = self.response_queue.get(timeout=timeout) + return response + except queue.Empty: + self.logger.log_error("Timeout waiting for BMG response.") + return {"success": False, "data": None, "error": "Timeout waiting for BMG response."} + + + def stop(self): + """Signal thread to stop and wait for it to finish.""" + self.shutdown_event.set() + self.join(timeout=5) + + if self.is_alive(): + self.logger.log_warning("BMG communication thread did not terminate in time.") + else: + self.logger.log_info("BMG communication thread terminated.") + \ No newline at end of file diff --git a/src/bmg_rest_node.py b/src/bmg_rest_node.py index 5155927..a22e5e6 100644 --- a/src/bmg_rest_node.py +++ b/src/bmg_rest_node.py @@ -4,6 +4,7 @@ from pathlib import Path from typing import Annotated, Optional +import threading from madsci.common.types.action_types import ActionFailed from madsci.common.types.node_types import RestNodeConfig @@ -14,6 +15,7 @@ from madsci.node_module.rest_node_module import RestNode from bmg_interface import BmgCom +from bmg_object_thread import BMGThread """ TODOs: @@ -57,18 +59,23 @@ class BMGNode(RestNode): def __init__(self) -> None: """Initializes the BMG node.""" super().__init__() - self.bmg = BmgCom( - "CLARIOstar", - resource_client=self.resource_client, - # plate_carrier=self.plate_carrier, - logger=self.logger, - ) + self.bmg_thread = None + def startup_handler(self) -> None: """Called to (re)initialize the node. Should be used to open connections to devices or initialize any other resources.""" self.init_resource_templates() self.create_resources() + + # start the BMG thread after resources are initialized + self.bmg_thread = BMGThread( + resource_client=self.resource_client, + plate_carrier=self.plate_carrier, + logger=self.logger, + ) + self.bmg_thread.start() + def init_resource_templates(self) -> None: @@ -95,9 +102,13 @@ def create_resources(self) -> None: def shutdown_handler(self) -> None: """Called to clean up resources before the node is shut down.""" try: - if self.bmg: - self.bmg.close_connection() - self.bmg = None + if self.bmg_thread: + self.bmg_thread.stop() + # self.bmg_thread.join(timeout=5) + # self.logger.log_info("BMG communication thread stopped.") + # if self.bmg: + # self.bmg.close_connection() + # self.bmg = None except Exception as err: self.logger.log_error(f"Error during BMGNode shutdown: {err}") @@ -105,16 +116,33 @@ def shutdown_handler(self) -> None: def open(self) -> None: """Opens the BMG plate tray""" self.logger.log_info("Opening BMG plate tray") - self.bmg.plate_out() - self.logger.log_info("BMG plate tray opened") + # self.bmg.plate_out() + + # send command to BMG thread + response = self.bmg_thread.send_command({action: "plate_out"}) + if not response["success"]: + raise Exception(f"Failed to open BMG plate tray: {response['error']}") + else: + self.logger.log_info("BMG plate tray opened") @action(name="close") def close(self) -> None: """Closes the BMG plate tray""" self.logger.log_info("Closing BMG plate tray") - self.bmg.plate_in() - self.logger.log_info("BMG plate tray closed") + # self.bmg.plate_in() + + + # send command to BMG thread + command = {"action": "plate_in"} + # TESTING + self.logger.log_info(f"COMMAND IN REST NODE CLOSE ACTION: {command}") + response = self.bmg_thread.send_command(command=command) + + if not response["success"]: + raise Exception(f"Failed to close BMG plate tray: {response['error']}") + else: + self.logger.log_info("BMG plate tray closed") @action(name="set_temp") def set_temp(self, temp: float) -> None: From 61217c5a4d8c349ec57290d4f96424608cf3a863 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Wed, 5 Nov 2025 12:34:39 -0600 Subject: [PATCH 06/28] attempted bug fix --- src/bmg_rest_node.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/bmg_rest_node.py b/src/bmg_rest_node.py index a22e5e6..5d318cf 100644 --- a/src/bmg_rest_node.py +++ b/src/bmg_rest_node.py @@ -24,7 +24,7 @@ only using ' -m pip install ... seems to work but it's not in the activated .venv .... - always says ready even though workflow step shows it's still running for the correct amount of time - - TASK: open an issue for this. Node status remains ready when it should be busy + - TASK: open an issue for this. Node status remains ready when it should be busy - ISSUE OPENED - add temperature monitoring to custom state function and also interface? @@ -116,10 +116,11 @@ def shutdown_handler(self) -> None: def open(self) -> None: """Opens the BMG plate tray""" self.logger.log_info("Opening BMG plate tray") - # self.bmg.plate_out() # send command to BMG thread - response = self.bmg_thread.send_command({action: "plate_out"}) + response = self.bmg_thread.send_command({"action": "plate_out"}) + + # interpret response if not response["success"]: raise Exception(f"Failed to open BMG plate tray: {response['error']}") else: @@ -130,15 +131,11 @@ def close(self) -> None: """Closes the BMG plate tray""" self.logger.log_info("Closing BMG plate tray") - # self.bmg.plate_in() - # send command to BMG thread - command = {"action": "plate_in"} - # TESTING - self.logger.log_info(f"COMMAND IN REST NODE CLOSE ACTION: {command}") - response = self.bmg_thread.send_command(command=command) - + response = self.bmg_thread.send_command({"action": "plate_in"}) + + # interpret response if not response["success"]: raise Exception(f"Failed to close BMG plate tray: {response['error']}") else: From 5468baed0f7fe460bf38d8e94c77986655ffcb68 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Wed, 5 Nov 2025 15:50:36 -0600 Subject: [PATCH 07/28] threading and temp state working --- default.node.info.yaml | 5 +-- src/bmg_interface.py | 40 +++++++++++++++-- src/bmg_object_thread.py | 65 ++++++++++++++++++++------- src/bmg_rest_node.py | 96 +++++++++++++++++++++++++++++++--------- 4 files changed, 163 insertions(+), 43 deletions(-) diff --git a/default.node.info.yaml b/default.node.info.yaml index 694353f..a3b338e 100644 --- a/default.node.info.yaml +++ b/default.node.info.yaml @@ -104,7 +104,7 @@ config: node_info_path: null update_node_files: true status_update_interval: 2.0 - state_update_interval: 2.0 + state_update_interval: 5.0 node_url: http://suestorm.cels.anl.gov:3003/ uvicorn_kwargs: {} output_path: C:\Users\RPL\TEST @@ -151,8 +151,7 @@ config_schema: anyOf: - type: number - type: 'null' - default: 2.0 - description: The interval in seconds at which the node should update its state. + default: 5.0 title: State Update Interval node_url: default: http://127.0.0.1:2000/ diff --git a/src/bmg_interface.py b/src/bmg_interface.py index 666e02b..0a2e643 100644 --- a/src/bmg_interface.py +++ b/src/bmg_interface.py @@ -34,8 +34,7 @@ def __init__( self.resource_client = resource_client self.plate_carrier = plate_carrier self.logger = logger or EventClient() - self.serial_lock = threading.Lock() - self._device_lock = threading.Lock() # TODO: what is this for? + # self.lock = threading.Lock() pythoncom.CoInitialize() self.com = comtypes.client.CreateObject("BMG_ActiveX.BMGRemoteControl") @@ -104,6 +103,37 @@ def set_temp(self, temp: float) -> None: nominal_temp = str(temp) self.exec("Temp", nominal_temp) + def read_temps(self) -> dict: + """Reads the temperature at three locations in the BMG plate reader + + Returns: + + """ + temps = {} + temp1_formatted = ctypes.c_char_p(b"Temp1") + temp1 = self.com.GetInfo(temp1_formatted) + temp2_formatted = ctypes.c_char_p(b"Temp2") + temp2 = self.com.GetInfo(temp2_formatted) + temp3_formatted = ctypes.c_char_p(b"Temp3") + temp3 = self.com.GetInfo(temp3_formatted) + + try: + # convert to floats in celsius + temp1 = float(temp1)/10 + temp2 = float(temp2)/10 + temp3 = float(temp3)/10 + temps = { + "Temp1" : temp1, + "Temp2": temp2, + "Temp3": temp3, + } + except Exception as e: + # Error collecting temperatures, don't fail anything + pass + + return temps + + def run_assay( self, protocol_name: str, @@ -140,10 +170,12 @@ def run_assay( def is_busy(self) -> bool: """Returns True if BMG is busy, False if not busy""" - return self.status() == "Busy" + # return self.status() == "Busy" + return bool(self.lock.locked()) def exec(self, cmd: str, *args: Any) -> None: """Executed a command over the established connection with the BMG plate reader""" + # with self.lock: args = (cmd, *args) res = self.com.ExecuteAndWait(args) if res: @@ -153,3 +185,5 @@ def exec(self, cmd: str, *args: Any) -> None: if __name__ == "__main__": com = BmgCom("CLARIOstar") print(f"BMG LABTECH Remote Control Version: {com.version()}") # noqa: T201 + + diff --git a/src/bmg_object_thread.py b/src/bmg_object_thread.py index 6e54995..6f2373f 100644 --- a/src/bmg_object_thread.py +++ b/src/bmg_object_thread.py @@ -11,6 +11,7 @@ def __init__(self, resource_client, plate_carrier, logger): self.resource_client = resource_client self.plate_carrier = plate_carrier self.logger = logger + self.lock = threading.Lock() # communication queue self.command_queue = queue.Queue() @@ -39,23 +40,51 @@ def run(self): # process command action = command.get("action") + # TESTING self.logger.log_info(f"ACTION FOUND IN THREAD RUN FUNCTION: {action}") - result = {"success": False, "data": None, "error": None} - - try: - if action == "plate_out": - self.bmg.plate_out() - result["success"] = True - elif action == "plate_in": - self.bmg.plate_in() - result["success"] = True - else: - result["error"] = f"Unknown action: {action}" - - except Exception as e: - result["error"] = str(e) - self.logger.log_error(f"Error processing command {action}: {e}") + result = { + "success": False, + "data": None, + "error": None + } + + with self.lock: + try: + if action == "plate_out": + self.bmg.plate_out() + result["success"] = True + elif action == "plate_in": + self.bmg.plate_in() + result["success"] = True + elif action == "set_temp": + temp = command.get("temp") + self.bmg.set_temp(temp=temp) + result["success"] = True + elif action == "read_temps": + temps = self.bmg.read_temps() + result["success"] = True + result["data"] = temps + elif action == "run_assay": + data_filename = self.bmg.run_assay( + protocol_name=command.get("protocol_name"), + protocol_database_path=command.get("protocol_database_path"), + data_output_directory=command.get("data_output_directory"), + data_output_file_name=command.get("data_output_file_name"), + ) + if data_filename: + result["data"] = data_filename + + # TODO: do we want to fail the action if no filename is returned? + # presumably there's a backup on the windows machine if this happens + result["success"] = True + + else: + result["error"] = f"Unknown action: {action}" + + except Exception as e: + result["error"] = str(e) + self.logger.log_error(f"Error processing command {action}: {e}") # Send response back self.response_queue.put(result) @@ -112,4 +141,10 @@ def stop(self): self.logger.log_warning("BMG communication thread did not terminate in time.") else: self.logger.log_info("BMG communication thread terminated.") + + @property + def is_busy(self) -> bool: + """Returns True if the BMG thread is handling a command""" + return bool(self.lock.locked()) + \ No newline at end of file diff --git a/src/bmg_rest_node.py b/src/bmg_rest_node.py index 5d318cf..07ad30c 100644 --- a/src/bmg_rest_node.py +++ b/src/bmg_rest_node.py @@ -34,7 +34,6 @@ - MADSci: something is wrong with passing paths in through the command line args. Only works when default paths are set in BMGNodeConfig TASK: make these into Path types, not string and test. Probably the double slash when passing in string is the issue -TASK: spin up thread in rest node init that all actions can talk to. Kill the thread on shutdown. """ @@ -45,6 +44,7 @@ class BMGNodeConfig(RestNodeConfig): """Data output directory path for bmg data""" db_directory_path: str = "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" """Path to directory where assay protocol files are stored""" + state_update_interval: Optional[float] = 5.0 @@ -56,6 +56,7 @@ class BMGNode(RestNode): config: BMGNodeConfig = BMGNodeConfig() module_version = "0.0.1" + def __init__(self) -> None: """Initializes the BMG node.""" super().__init__() @@ -99,6 +100,39 @@ def create_resources(self) -> None: resource_name=f"{self.node_definition.node_name}_plate_nest", ) + def state_handler(self) -> None: + """Periodically check state of BMG device""" + self.cached_temp1 = None + self.cached_temp2 = None + self.cached_temp3 = None + + if self.bmg_thread is None: + self.logger.log_error("BMG thread is not initialized") + return + if self.bmg_thread.is_busy: + self.node_state = { + "Temp1 (bottom heating plate)": self.cached_temp1, + "Temp2 (top heating plate)": self.cached_temp2, + "Temp3 (optic slide heating plate)": self.cached_temp3, + } + else: + # thread is not busy, query the device + try: + response = self.bmg_thread.send_command({"action": "read_temps"}) + temps = response["data"] + self.cached_temp1 = temps["Temp1"] + self.cached_temp2 = temps["Temp2"] + self.cached_temp3 = temps["Temp3"] + + self.node_state = { + "Temp1 (bottom heating plate)": temps["Temp1"], + "Temp2 (top heating plate)": temps["Temp2"], + "Temp3 (optic slide heating plate)": temps["Temp3"], + } + except Exception as e: + pass # Do nothing if this doesn't work + + def shutdown_handler(self) -> None: """Called to clean up resources before the node is shut down.""" try: @@ -112,6 +146,7 @@ def shutdown_handler(self) -> None: except Exception as err: self.logger.log_error(f"Error during BMGNode shutdown: {err}") + @action(name="open") def open(self) -> None: """Opens the BMG plate tray""" @@ -147,15 +182,15 @@ def set_temp(self, temp: float) -> None: temp = float(temp) if temp in {0.0, 0.1} or 25.0 <= temp <= 45.0: - # temp input is valid - self.bmg = BmgCom( - "CLARIOstar", - resource_client=self.resource_client, - plate_carrier=self.plate_carrier, - logger=self.logger, - ) - self.bmg.set_temp(temp=temp) - return None + # temp input is valid, send the command + response = self.bmg_thread.send_command({"action": "set_temp","temp": temp}) + + # interpret response + if not response["success"]: + self.logger.log_error(f"Error setting temperature: {response["error"]}") + return ActionFailed(errors=[f"Error setting temperature: {response["error"]}"]) + else: + return None # temp input is not valid (fail action, don't put node in error state) return ActionFailed(errors=["Invalid temperature input value"]) @@ -173,20 +208,37 @@ def run_assay( data_file_path = None # run the assay - self.bmg = BmgCom( - "CLARIOstar", - resource_client=self.resource_client, - plate_carrier=self.plate_carrier, - logger=self.logger, - ) - data_file_path = self.bmg.run_assay( - protocol_name=assay_name, - protocol_database_path=self.config.db_directory_path, - data_output_directory=self.config.output_path, - data_output_file_name=data_output_file_name, + # self.bmg = BmgCom( + # "CLARIOstar", + # resource_client=self.resource_client, + # plate_carrier=self.plate_carrier, + # logger=self.logger, + # ) + response = self.bmg_thread.send_commmand( + { + "action": "run_assay", + "protocol_name": assay_name, + "protocol_database_path": self.config.db_directory_path, + "data_output_directory": self.config.output_path, + "data_output_file_name": data_output_file_name, + } ) - return Path(data_file_path) + # interpret response + if not response["success"]: + self.logger.log_info(f"Error running assay: {response["error"]}") + return ActionFailed(errors=[f"Error running assay: {response["error"]}"]) + else: + return Path(response["data"]) + + # data_file_path = self.bmg.run_assay( + # protocol_name=assay_name, + # protocol_database_path=self.config.db_directory_path, + # data_output_directory=self.config.output_path, + # data_output_file_name=data_output_file_name, + # ) + + # return Path(data_file_path) if __name__ == "__main__": From dd951d22e1b9bb74138249dd864935f5bee08ef6 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Thu, 6 Nov 2025 11:13:55 -0600 Subject: [PATCH 08/28] progress on final cleanup --- src/bmg_interface.py | 33 ++++------- src/bmg_object_thread.py | 121 ++++++++++++++++++++++----------------- src/bmg_rest_node.py | 97 ++++++++++--------------------- 3 files changed, 111 insertions(+), 140 deletions(-) diff --git a/src/bmg_interface.py b/src/bmg_interface.py index 0a2e643..4daca7e 100644 --- a/src/bmg_interface.py +++ b/src/bmg_interface.py @@ -3,7 +3,6 @@ """ import ctypes -import threading import time from pathlib import Path from typing import Any, Optional @@ -11,8 +10,6 @@ import comtypes.client import pythoncom from madsci.client.event_client import EventClient -from madsci.client.resource_client import ResourceClient -from madsci.common.types.resource_types import Slot """TODO: - when to edit resource client and plate carrier? - there's not really a state change of the plate like in the peeler""" @@ -24,17 +21,12 @@ class BmgCom: def __init__( self, control_name: str, - resource_client: ResourceClient = None, - plate_carrier: Optional[Slot] = None, logger: EventClient = None, ) -> None: """Initializes and opens the connection the BMG plate reader""" self.control_name = control_name - self.resource_client = resource_client - self.plate_carrier = plate_carrier self.logger = logger or EventClient() - # self.lock = threading.Lock() pythoncom.CoInitialize() self.com = comtypes.client.CreateObject("BMG_ActiveX.BMGRemoteControl") @@ -105,8 +97,8 @@ def set_temp(self, temp: float) -> None: def read_temps(self) -> dict: """Reads the temperature at three locations in the BMG plate reader - - Returns: + + Returns: """ temps = {} @@ -117,23 +109,22 @@ def read_temps(self) -> dict: temp3_formatted = ctypes.c_char_p(b"Temp3") temp3 = self.com.GetInfo(temp3_formatted) - try: + try: # convert to floats in celsius - temp1 = float(temp1)/10 - temp2 = float(temp2)/10 - temp3 = float(temp3)/10 + temp1 = float(temp1) / 10 + temp2 = float(temp2) / 10 + temp3 = float(temp3) / 10 temps = { - "Temp1" : temp1, + "Temp1": temp1, "Temp2": temp2, "Temp3": temp3, } - except Exception as e: - # Error collecting temperatures, don't fail anything - pass + except Exception: + # Don't do anything except log if temperature collection fails + self.logger.log_error("Error collecting temperatures: {e}") return temps - def run_assay( self, protocol_name: str, @@ -170,12 +161,10 @@ def run_assay( def is_busy(self) -> bool: """Returns True if BMG is busy, False if not busy""" - # return self.status() == "Busy" return bool(self.lock.locked()) def exec(self, cmd: str, *args: Any) -> None: """Executed a command over the established connection with the BMG plate reader""" - # with self.lock: args = (cmd, *args) res = self.com.ExecuteAndWait(args) if res: @@ -185,5 +174,3 @@ def exec(self, cmd: str, *args: Any) -> None: if __name__ == "__main__": com = BmgCom("CLARIOstar") print(f"BMG LABTECH Remote Control Version: {com.version()}") # noqa: T201 - - diff --git a/src/bmg_object_thread.py b/src/bmg_object_thread.py index 6f2373f..961b465 100644 --- a/src/bmg_object_thread.py +++ b/src/bmg_object_thread.py @@ -1,56 +1,52 @@ -import threading +""" +Object for thread that connects and communicates to the BMG device +""" + import queue +import threading + +from madsci.client.event_client import EventClient from bmg_interface import BmgCom + class BMGThread(threading.Thread): """Dedicated thread for BMG communication.""" - def __init__(self, resource_client, plate_carrier, logger): + def __init__(self, logger: EventClient = None) -> None: + """Initializes the BMGThread object""" super().__init__(daemon=True) - self.resource_client = resource_client - self.plate_carrier = plate_carrier - self.logger = logger + self.logger = logger or EventClient() self.lock = threading.Lock() - # communication queue + # communication queue self.command_queue = queue.Queue() self.response_queue = queue.Queue() self.shutdown_event = threading.Event() self.bmg = None - def run(self): + def run(self) -> None: """Main thread loop for BMG communication.""" - try: + try: # initialize BMG communication self.bmg = BmgCom( "CLARIOstar", - resource_client=self.resource_client, - plate_carrier=self.plate_carrier, logger=self.logger, ) self.logger.log_info("BMG communication thread started.") while not self.shutdown_event.is_set(): - # self.logger.log_info("Waiting for command...") # TESTING try: - command = self.command_queue.get(timeout=1) # wait for command + # wait for command + command = self.command_queue.get(timeout=1) self.logger.log_info(f"Processing command: {command}") # process command action = command.get("action") - - # TESTING - self.logger.log_info(f"ACTION FOUND IN THREAD RUN FUNCTION: {action}") - result = { - "success": False, - "data": None, - "error": None - } - + result = {"success": False, "data": None, "error": None} with self.lock: - try: + try: if action == "plate_out": self.bmg.plate_out() result["success"] = True @@ -65,39 +61,47 @@ def run(self): temps = self.bmg.read_temps() result["success"] = True result["data"] = temps - elif action == "run_assay": + elif action == "run_assay": data_filename = self.bmg.run_assay( protocol_name=command.get("protocol_name"), - protocol_database_path=command.get("protocol_database_path"), - data_output_directory=command.get("data_output_directory"), - data_output_file_name=command.get("data_output_file_name"), + protocol_database_path=command.get( + "protocol_database_path" + ), + data_output_directory=command.get( + "data_output_directory" + ), + data_output_file_name=command.get( + "data_output_file_name" + ), ) if data_filename: result["data"] = data_filename - # TODO: do we want to fail the action if no filename is returned? - # presumably there's a backup on the windows machine if this happens - result["success"] = True - + # Don't fail the action if no data is returned + # presumably the data is still backed up on local machine. + result["success"] = True + else: result["error"] = f"Unknown action: {action}" except Exception as e: result["error"] = str(e) - self.logger.log_error(f"Error processing command {action}: {e}") + self.logger.log_error( + f"Error processing command {action}: {e}" + ) # Send response back self.response_queue.put(result) except queue.Empty: - continue # no command received, loop again - + # no command received, loop again + continue except Exception as e: self.logger.log_error(f"Error in BMG thread: {e}") - return # TODO: should I be returning here? - kills the thread if there's an error during init? - - finally: + return # Should I be returning here? - kills the thread if there's an error during init? + + finally: # clean up BMG communication if self.bmg: try: @@ -107,44 +111,59 @@ def run(self): except Exception as e: self.logger.log_error(f"Error closing BMG connection: {e}") + def _process_command(self, command: dict) -> dict: + # TODO: Extract processing from run function here. + # as is, pydantic says the run funtion is too complex. + pass + def send_command(self, command: dict, timeout: float = 300.0) -> dict: """Send a command to the BMG thread and wait for a response. - TODO: how long should the timeout be? - Args: command (dict): Command to send to the BMG thread. timeout (float): Time to wait for a response. Returns: dict: Response from the BMG thread. - """ - # TESTING - self.logger.log_info(f"COMMAND IN THREAD SEND COMMAND FUNCTION: {command}") self.command_queue.put(command) try: - response = self.response_queue.get(timeout=timeout) - return response + return self.response_queue.get(timeout=timeout) except queue.Empty: self.logger.log_error("Timeout waiting for BMG response.") - return {"success": False, "data": None, "error": "Timeout waiting for BMG response."} - - - def stop(self): + return { + "success": False, + "data": None, + "error": "Timeout waiting for BMG response.", + } + + def stop(self) -> None: """Signal thread to stop and wait for it to finish.""" + # close connection to the bmg device + try: + if self.bmg: + self.bmg.close_connection() + self.bmg = None + else: + self.logger.log_warning( + "No BMG device connection open, unable to close nonexistent connection." + ) + except Exception: + self.logger.log_warning("Unable to close connection to bmg device") + + # shut down the thread self.shutdown_event.set() self.join(timeout=5) if self.is_alive(): - self.logger.log_warning("BMG communication thread did not terminate in time.") - else: + self.logger.log_warning( + "BMG communication thread did not terminate in time." + ) + else: self.logger.log_info("BMG communication thread terminated.") @property def is_busy(self) -> bool: """Returns True if the BMG thread is handling a command""" return bool(self.lock.locked()) - - \ No newline at end of file diff --git a/src/bmg_rest_node.py b/src/bmg_rest_node.py index 07ad30c..2c1c34d 100644 --- a/src/bmg_rest_node.py +++ b/src/bmg_rest_node.py @@ -4,7 +4,6 @@ from pathlib import Path from typing import Annotated, Optional -import threading from madsci.common.types.action_types import ActionFailed from madsci.common.types.node_types import RestNodeConfig @@ -19,19 +18,8 @@ """ TODOs: -- figure out pdm dependencies with 32 bit python - Q: why does creating a .venv with the 32-bit python then pip installing not work? - only using ' -m pip install ... seems to work but it's not in the activated .venv .... -- always says ready even though workflow step shows it's still running for the correct amount of time - - TASK: open an issue for this. Node status remains ready when it should be busy - ISSUE OPENED - -- add temperature monitoring to custom state function and also interface? - -- MADSci second workflow step sent always fails after first one works - # NOTE: can't close connection after each step becuase closing the connection closes the device door -- MADSci: clicking show editable workflow step causes Squid dashboard page to freeze -- TASK: open an issue on MADsci repo -- MADSci: something is wrong with passing paths in through the command line args. Only works when default paths are set in BMGNodeConfig +- Something is wrong with passing paths in through the command line args. Only works when default paths are set in BMGNodeConfig TASK: make these into Path types, not string and test. Probably the double slash when passing in string is the issue """ @@ -45,8 +33,8 @@ class BMGNodeConfig(RestNodeConfig): db_directory_path: str = "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" """Path to directory where assay protocol files are stored""" state_update_interval: Optional[float] = 5.0 + """Interval for updating module state in seconds""" - class BMGNode(RestNode): """A node to control the BMG VANTAstar microplate reader""" @@ -56,13 +44,11 @@ class BMGNode(RestNode): config: BMGNodeConfig = BMGNodeConfig() module_version = "0.0.1" - def __init__(self) -> None: """Initializes the BMG node.""" super().__init__() self.bmg_thread = None - def startup_handler(self) -> None: """Called to (re)initialize the node. Should be used to open connections to devices or initialize any other resources.""" @@ -76,8 +62,6 @@ def startup_handler(self) -> None: logger=self.logger, ) self.bmg_thread.start() - - def init_resource_templates(self) -> None: """Initialize resource templates for the node module.""" @@ -100,7 +84,7 @@ def create_resources(self) -> None: resource_name=f"{self.node_definition.node_name}_plate_nest", ) - def state_handler(self) -> None: + def state_handler(self) -> None: """Periodically check state of BMG device""" self.cached_temp1 = None self.cached_temp2 = None @@ -114,10 +98,11 @@ def state_handler(self) -> None: "Temp1 (bottom heating plate)": self.cached_temp1, "Temp2 (top heating plate)": self.cached_temp2, "Temp3 (optic slide heating plate)": self.cached_temp3, + "bmg_thread_state": "BUSY", } - else: - # thread is not busy, query the device - try: + else: + # thread is not busy, query the device + try: response = self.bmg_thread.send_command({"action": "read_temps"}) temps = response["data"] self.cached_temp1 = temps["Temp1"] @@ -128,38 +113,34 @@ def state_handler(self) -> None: "Temp1 (bottom heating plate)": temps["Temp1"], "Temp2 (top heating plate)": temps["Temp2"], "Temp3 (optic slide heating plate)": temps["Temp3"], + "bmg_thead_state": "READY", } - except Exception as e: - pass # Do nothing if this doesn't work - + except Exception as e: + # Do nothing except log the error if state handler doesn't work + self.logger.log_error(f"Error in state handler: {e}") def shutdown_handler(self) -> None: """Called to clean up resources before the node is shut down.""" try: if self.bmg_thread: self.bmg_thread.stop() - # self.bmg_thread.join(timeout=5) - # self.logger.log_info("BMG communication thread stopped.") - # if self.bmg: - # self.bmg.close_connection() - # self.bmg = None - except Exception as err: - self.logger.log_error(f"Error during BMGNode shutdown: {err}") + except Exception as err: + self.logger.log_error(f"Error during BMG thread and node shutdown: {err}") @action(name="open") def open(self) -> None: """Opens the BMG plate tray""" + self.logger.log_info("Opening BMG plate tray") - + # send command to BMG thread response = self.bmg_thread.send_command({"action": "plate_out"}) # interpret response if not response["success"]: raise Exception(f"Failed to open BMG plate tray: {response['error']}") - else: - self.logger.log_info("BMG plate tray opened") + self.logger.log_info("BMG plate tray opened") @action(name="close") def close(self) -> None: @@ -169,12 +150,11 @@ def close(self) -> None: # send command to BMG thread response = self.bmg_thread.send_command({"action": "plate_in"}) - + # interpret response if not response["success"]: raise Exception(f"Failed to close BMG plate tray: {response['error']}") - else: - self.logger.log_info("BMG plate tray closed") + self.logger.log_info("BMG plate tray closed") @action(name="set_temp") def set_temp(self, temp: float) -> None: @@ -183,14 +163,17 @@ def set_temp(self, temp: float) -> None: temp = float(temp) if temp in {0.0, 0.1} or 25.0 <= temp <= 45.0: # temp input is valid, send the command - response = self.bmg_thread.send_command({"action": "set_temp","temp": temp}) + response = self.bmg_thread.send_command( + {"action": "set_temp", "temp": temp} + ) # interpret response - if not response["success"]: - self.logger.log_error(f"Error setting temperature: {response["error"]}") - return ActionFailed(errors=[f"Error setting temperature: {response["error"]}"]) - else: - return None + if not response["success"]: + self.logger.log_error(f"Error setting temperature: {response['error']}") + return ActionFailed( + errors=[f"Error setting temperature: {response['error']}"] + ) + return None # temp input is not valid (fail action, don't put node in error state) return ActionFailed(errors=["Invalid temperature input value"]) @@ -205,15 +188,7 @@ def run_assay( ) -> Annotated[Path, "Data .txt file returned by the BMG microplate reader"]: """Runs an assay on the BMG plate reader""" - data_file_path = None - - # run the assay - # self.bmg = BmgCom( - # "CLARIOstar", - # resource_client=self.resource_client, - # plate_carrier=self.plate_carrier, - # logger=self.logger, - # ) + # run the assay, collect response containting output data file name response = self.bmg_thread.send_commmand( { "action": "run_assay", @@ -226,19 +201,9 @@ def run_assay( # interpret response if not response["success"]: - self.logger.log_info(f"Error running assay: {response["error"]}") - return ActionFailed(errors=[f"Error running assay: {response["error"]}"]) - else: - return Path(response["data"]) - - # data_file_path = self.bmg.run_assay( - # protocol_name=assay_name, - # protocol_database_path=self.config.db_directory_path, - # data_output_directory=self.config.output_path, - # data_output_file_name=data_output_file_name, - # ) - - # return Path(data_file_path) + self.logger.log_info(f"Error running assay: {response['error']}") + return ActionFailed(errors=[f"Error running assay: {response['error']}"]) + return Path(response["data"]) if __name__ == "__main__": From 02dfbc7e972a3573f9cc49c2e8455843f85aa0a7 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Fri, 14 Nov 2025 12:53:16 -0600 Subject: [PATCH 09/28] resource handler and run_bmg script --- default.node.info.yaml | 2 +- definitions/inheco.node.info.yaml | 178 ++++++++++++++++++++++++++++++ definitions/inheco.node.yaml | 7 ++ pdm.lock | 86 +++++++-------- pyproject.toml | 12 +- run_bmg.bat | 4 + src/bmg_rest_node.py | 4 +- 7 files changed, 232 insertions(+), 61 deletions(-) create mode 100644 definitions/inheco.node.info.yaml create mode 100644 definitions/inheco.node.yaml create mode 100644 run_bmg.bat diff --git a/default.node.info.yaml b/default.node.info.yaml index a3b338e..f0da676 100644 --- a/default.node.info.yaml +++ b/default.node.info.yaml @@ -105,7 +105,7 @@ config: update_node_files: true status_update_interval: 2.0 state_update_interval: 5.0 - node_url: http://suestorm.cels.anl.gov:3003/ + node_url: http://127.0.0.1:2000/ uvicorn_kwargs: {} output_path: C:\Users\RPL\TEST db_directory_path: C:\Program Files (x86)\BMG\CLARIOstar\User\Definit diff --git a/definitions/inheco.node.info.yaml b/definitions/inheco.node.info.yaml new file mode 100644 index 0000000..89559ec --- /dev/null +++ b/definitions/inheco.node.info.yaml @@ -0,0 +1,178 @@ +node_name: inheco.node +node_id: 01K9DJ8X5PM87Y3YWCBPYJHMXS +node_description: null +node_type: device +module_name: bmg_node +module_version: 0.0.1 +capabilities: + get_info: true + get_state: true + get_status: true + send_action: true + get_action_status: true + get_action_result: true + get_action_history: true + action_files: true + send_admin_commands: true + set_config: true + get_resources: false + get_log: true + admin_commands: + - lock + - reset + - shutdown + - unlock +node_url: null +actions: + open: + name: open + description: Opens the BMG plate tray + args: {} + locations: {} + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + close: + name: close + description: Closes the BMG plate tray + args: {} + locations: {} + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + set_temp: + name: set_temp + description: Sets the temperature on the BMG microplate reader + args: + temp: + name: temp + description: '' + argument_type: float + required: true + default: null + locations: {} + files: {} + results: {} + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null + run_assay: + name: run_assay + description: Runs an assay on the BMG plate reader + args: + assay_name: + name: assay_name + description: '' + argument_type: str + required: true + default: null + data_output_file_name: + name: data_output_file_name + description: data output file name (ex. data.txt). Will default to .txt + (ex. 1731706249.txt) if no file name is entered. + argument_type: str + required: false + default: null + locations: {} + files: {} + results: + file: + result_label: file + description: null + result_type: file + blocking: false + asynchronous: true + accepts_var_args: false + accepts_var_kwargs: false + var_args_schema: null + var_kwargs_schema: null +config: + node_definition: definitions\inheco.node.yaml + node_info_path: null + update_node_files: true + status_update_interval: 2.0 + state_update_interval: 5.0 + node_url: http://suestorm.cels.anl.gov:3003/ + uvicorn_kwargs: {} + output_path: C:\Users\RPL\TEST + db_directory_path: C:\Program Files (x86)\BMG\CLARIOstar\User\Definit +config_schema: + description: Configuration for the BMG node. + properties: + node_definition: + anyOf: + - type: string + - format: path + type: string + - type: 'null' + default: default.node.yaml + description: Path to the node definition file to use. If set, the node will + load the definition from this file on startup. Otherwise, a default configuration + will be created. + title: Node Definition File + node_info_path: + anyOf: + - type: string + - format: path + type: string + - type: 'null' + default: null + description: Path to export the generated node info file. If not set, will use + the node name and the node_definition's path. + title: Node Info Path + update_node_files: + default: true + description: Whether to update the node definition and info files on startup. + If set to False, the node will not update the files even if they are out of + date. + title: Update Node Files + type: boolean + status_update_interval: + anyOf: + - type: number + - type: 'null' + default: 2.0 + description: The interval in seconds at which the node should update its status. + title: Status Update Interval + state_update_interval: + anyOf: + - type: number + - type: 'null' + default: 5.0 + title: State Update Interval + node_url: + default: http://127.0.0.1:2000/ + description: The URL used to communicate with the node. This is the base URL + for the REST API. + format: uri + minLength: 1 + title: Node URL + type: string + uvicorn_kwargs: + additionalProperties: true + description: Configuration for the Uvicorn server that runs the REST API. + title: Uvicorn Configuration + type: object + output_path: + default: C:\Users\RPL\TEST + title: Output Path + type: string + db_directory_path: + default: C:\Program Files (x86)\BMG\CLARIOstar\User\Definit + title: Db Directory Path + type: string + title: BMGNodeConfig + type: object diff --git a/definitions/inheco.node.yaml b/definitions/inheco.node.yaml new file mode 100644 index 0000000..b5d1358 --- /dev/null +++ b/definitions/inheco.node.yaml @@ -0,0 +1,7 @@ +node_name: inheco.node +node_id: 01K9DJ8X5PM87Y3YWCBPYJHMXS +node_description: null +node_type: device +module_name: bmg_node +module_version: 0.0.1 +capabilities: null diff --git a/pdm.lock b/pdm.lock index bc7628d..5cd7ca4 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:5d4e07bab5161a059640bc2b8f9298d342985676c3fb69315d25fd181d5d56d5" +content_hash = "sha256:f2c2ee26c4d212a0e29602f5b292c757875e10c20fdd6bee8a2adb533f152e8d" [[metadata.targets]] requires_python = ">=3.9.1" @@ -441,7 +441,7 @@ files = [ [[package]] name = "fastapi" -version = "0.121.0" +version = "0.121.2" requires_python = ">=3.8" summary = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" groups = ["default"] @@ -452,8 +452,8 @@ dependencies = [ "typing-extensions>=4.8.0", ] files = [ - {file = "fastapi-0.121.0-py3-none-any.whl", hash = "sha256:8bdf1b15a55f4e4b0d6201033da9109ea15632cb76cf156e7b8b4019f2172106"}, - {file = "fastapi-0.121.0.tar.gz", hash = "sha256:06663356a0b1ee93e875bbf05a31fb22314f5bed455afaaad2b2dad7f26e98fa"}, + {file = "fastapi-0.121.2-py3-none-any.whl", hash = "sha256:f2d80b49a86a846b70cc3a03eb5ea6ad2939298bf6a7fe377aa9cd3dd079d358"}, + {file = "fastapi-0.121.2.tar.gz", hash = "sha256:ca8e932b2b823ec1721c641e3669472c855ad9564a2854c9899d904c2848b8b9"}, ] [[package]] @@ -633,7 +633,7 @@ name = "iniconfig" version = "2.1.0" requires_python = ">=3.8" summary = "brain-dead simple config-ini parsing" -groups = ["default", "dev"] +groups = ["dev"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, @@ -655,9 +655,9 @@ files = [ [[package]] name = "madsci-client" -version = "0.5.0" +version = "0.5.3" requires_python = ">=3.9.1" -summary = "The Modular Autonomous Discovery for Science (MADSci) Python Client and CLI." +summary = "The Modular Autonomous Discovery for Science (MADSci) Python Clients." groups = ["default"] dependencies = [ "click>=8.1.7", @@ -666,13 +666,13 @@ dependencies = [ "trogon>=0.6.0", ] files = [ - {file = "madsci_client-0.5.0-py3-none-any.whl", hash = "sha256:350f63132a8a64a0c238e0b9d946b4a65cf03e744c2971d4fb3ff4e58d9f36fb"}, - {file = "madsci_client-0.5.0.tar.gz", hash = "sha256:1f7eb89ff83d20042f0d6b5d22a0a893cc535a3948c2e7bf487bbea259d3b946"}, + {file = "madsci_client-0.5.3-py3-none-any.whl", hash = "sha256:1b611f53c0adb1a0e4b14b820e1159978f91ecd7437fab10001f52ab0e663894"}, + {file = "madsci_client-0.5.3.tar.gz", hash = "sha256:d7335229f2d7ad7a32baffcd26bb8753098a00af59b1b0ebb38d5c3da2faf5dc"}, ] [[package]] name = "madsci-common" -version = "0.5.0" +version = "0.5.3" requires_python = ">=3.9.1" summary = "The Modular Autonomous Discovery for Science (MADSci) Common Definitions and Utilities." groups = ["default"] @@ -697,13 +697,13 @@ dependencies = [ "uvicorn[standard]>=0.32.0", ] files = [ - {file = "madsci_common-0.5.0-py3-none-any.whl", hash = "sha256:69f123576a414c046cff5b238ac9b2ab5f3ae895bde69688f1bb3b56bcada3d5"}, - {file = "madsci_common-0.5.0.tar.gz", hash = "sha256:68c7a2fbd27262ea0d228c1a20c18173d68e653908ebb672bbcdd9fe019f2835"}, + {file = "madsci_common-0.5.3-py3-none-any.whl", hash = "sha256:69cbcdad127e2c63ef8f086885844a1b1a0231833282fafa71bddb452e5681f7"}, + {file = "madsci_common-0.5.3.tar.gz", hash = "sha256:ad0c04d0f7363404278c6ec26b78743b22e34e0c2fe01e5bfd0a12be60d76ce5"}, ] [[package]] name = "madsci-node-module" -version = "0.5.0" +version = "0.5.3" requires_python = ">=3.9.1" summary = "The Modular Autonomous Discovery for Science (MADSci) Node Module Helper Classes." groups = ["default"] @@ -713,8 +713,8 @@ dependencies = [ "regex", ] files = [ - {file = "madsci_node_module-0.5.0-py3-none-any.whl", hash = "sha256:b7353d8633740bf62cfad05ad1a3bd2e9c190f997fac7fed93919361c503c36e"}, - {file = "madsci_node_module-0.5.0.tar.gz", hash = "sha256:77bdea14bf1ce01bc46fe99865cb3294f947e81e613d7b79c4097c6ed8462087"}, + {file = "madsci_node_module-0.5.3-py3-none-any.whl", hash = "sha256:32616ae0cfaaaecbba2641b40f1e8a7a59de4acd9fd96b5ae4c015cf00191ed2"}, + {file = "madsci_node_module-0.5.3.tar.gz", hash = "sha256:287f7e50cc2fe8ba87784b946f34e881d2a6e7f3b4fb10623b313a5aacffaa10"}, ] [[package]] @@ -833,7 +833,7 @@ name = "packaging" version = "25.0" requires_python = ">=3.8" summary = "Core utilities for Python packages" -groups = ["default", "dev"] +groups = ["dev"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, @@ -855,7 +855,7 @@ name = "pluggy" version = "1.6.0" requires_python = ">=3.9" summary = "plugin and hook calling mechanisms for python" -groups = ["default", "dev"] +groups = ["dev"] files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -1213,22 +1213,12 @@ files = [ {file = "pymongo-4.15.3.tar.gz", hash = "sha256:7a981271347623b5319932796690c2d301668ac3a1965974ac9f5c3b8a22cea5"}, ] -[[package]] -name = "pyserial" -version = "3.5" -summary = "Python Serial Port Extension" -groups = ["default"] -files = [ - {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, - {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, -] - [[package]] name = "pytest" version = "8.4.2" requires_python = ">=3.9" summary = "pytest: simple powerful testing with Python" -groups = ["default", "dev"] +groups = ["dev"] dependencies = [ "colorama>=0.4; sys_platform == \"win32\"", "exceptiongroup>=1; python_version < \"3.11\"", @@ -1551,30 +1541,30 @@ files = [ [[package]] name = "ruff" -version = "0.14.3" +version = "0.14.5" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." groups = ["dev"] files = [ - {file = "ruff-0.14.3-py3-none-linux_armv6l.whl", hash = "sha256:876b21e6c824f519446715c1342b8e60f97f93264012de9d8d10314f8a79c371"}, - {file = "ruff-0.14.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6fd8c79b457bedd2abf2702b9b472147cd860ed7855c73a5247fa55c9117654"}, - {file = "ruff-0.14.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:71ff6edca490c308f083156938c0c1a66907151263c4abdcb588602c6e696a14"}, - {file = "ruff-0.14.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:786ee3ce6139772ff9272aaf43296d975c0217ee1b97538a98171bf0d21f87ed"}, - {file = "ruff-0.14.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cd6291d0061811c52b8e392f946889916757610d45d004e41140d81fb6cd5ddc"}, - {file = "ruff-0.14.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a497ec0c3d2c88561b6d90f9c29f5ae68221ac00d471f306fa21fa4264ce5fcd"}, - {file = "ruff-0.14.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e231e1be58fc568950a04fbe6887c8e4b85310e7889727e2b81db205c45059eb"}, - {file = "ruff-0.14.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:469e35872a09c0e45fecf48dd960bfbce056b5db2d5e6b50eca329b4f853ae20"}, - {file = "ruff-0.14.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d6bc90307c469cb9d28b7cfad90aaa600b10d67c6e22026869f585e1e8a2db0"}, - {file = "ruff-0.14.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2f8a0bbcffcfd895df39c9a4ecd59bb80dca03dc43f7fb63e647ed176b741e"}, - {file = "ruff-0.14.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:678fdd7c7d2d94851597c23ee6336d25f9930b460b55f8598e011b57c74fd8c5"}, - {file = "ruff-0.14.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1ec1ac071e7e37e0221d2f2dbaf90897a988c531a8592a6a5959f0603a1ecf5e"}, - {file = "ruff-0.14.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afcdc4b5335ef440d19e7df9e8ae2ad9f749352190e96d481dc501b753f0733e"}, - {file = "ruff-0.14.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7bfc42f81862749a7136267a343990f865e71fe2f99cf8d2958f684d23ce3dfa"}, - {file = "ruff-0.14.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a65e448cfd7e9c59fae8cf37f9221585d3354febaad9a07f29158af1528e165f"}, - {file = "ruff-0.14.3-py3-none-win32.whl", hash = "sha256:f3d91857d023ba93e14ed2d462ab62c3428f9bbf2b4fbac50a03ca66d31991f7"}, - {file = "ruff-0.14.3-py3-none-win_amd64.whl", hash = "sha256:d7b7006ac0756306db212fd37116cce2bd307e1e109375e1c6c106002df0ae5f"}, - {file = "ruff-0.14.3-py3-none-win_arm64.whl", hash = "sha256:26eb477ede6d399d898791d01961e16b86f02bc2486d0d1a7a9bb2379d055dc1"}, - {file = "ruff-0.14.3.tar.gz", hash = "sha256:4ff876d2ab2b161b6de0aa1f5bd714e8e9b4033dc122ee006925fbacc4f62153"}, + {file = "ruff-0.14.5-py3-none-linux_armv6l.whl", hash = "sha256:f3b8248123b586de44a8018bcc9fefe31d23dda57a34e6f0e1e53bd51fd63594"}, + {file = "ruff-0.14.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f7a75236570318c7a30edd7f5491945f0169de738d945ca8784500b517163a72"}, + {file = "ruff-0.14.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d146132d1ee115f8802356a2dc9a634dbf58184c51bff21f313e8cd1c74899a"}, + {file = "ruff-0.14.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2380596653dcd20b057794d55681571a257a42327da8894b93bbd6111aa801f"}, + {file = "ruff-0.14.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d1fa985a42b1f075a098fa1ab9d472b712bdb17ad87a8ec86e45e7fa6273e68"}, + {file = "ruff-0.14.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88f0770d42b7fa02bbefddde15d235ca3aa24e2f0137388cc15b2dcbb1f7c7a7"}, + {file = "ruff-0.14.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3676cb02b9061fee7294661071c4709fa21419ea9176087cb77e64410926eb78"}, + {file = "ruff-0.14.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b595bedf6bc9cab647c4a173a61acf4f1ac5f2b545203ba82f30fcb10b0318fb"}, + {file = "ruff-0.14.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f55382725ad0bdb2e8ee2babcbbfb16f124f5a59496a2f6a46f1d9d99d93e6e2"}, + {file = "ruff-0.14.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7497d19dce23976bdaca24345ae131a1d38dcfe1b0850ad8e9e6e4fa321a6e19"}, + {file = "ruff-0.14.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:410e781f1122d6be4f446981dd479470af86537fb0b8857f27a6e872f65a38e4"}, + {file = "ruff-0.14.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01be527ef4c91a6d55e53b337bfe2c0f82af024cc1a33c44792d6844e2331e1"}, + {file = "ruff-0.14.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f66e9bb762e68d66e48550b59c74314168ebb46199886c5c5aa0b0fbcc81b151"}, + {file = "ruff-0.14.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d93be8f1fa01022337f1f8f3bcaa7ffee2d0b03f00922c45c2207954f351f465"}, + {file = "ruff-0.14.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c135d4b681f7401fe0e7312017e41aba9b3160861105726b76cfa14bc25aa367"}, + {file = "ruff-0.14.5-py3-none-win32.whl", hash = "sha256:c83642e6fccfb6dea8b785eb9f456800dcd6a63f362238af5fc0c83d027dd08b"}, + {file = "ruff-0.14.5-py3-none-win_amd64.whl", hash = "sha256:9d55d7af7166f143c94eae1db3312f9ea8f95a4defef1979ed516dbb38c27621"}, + {file = "ruff-0.14.5-py3-none-win_arm64.whl", hash = "sha256:4b700459d4649e2594b31f20a9de33bc7c19976d4746d8d0798ad959621d64a4"}, + {file = "ruff-0.14.5.tar.gz", hash = "sha256:8d3b48d7d8aad423d3137af7ab6c8b1e38e4de104800f0d596990f6ada1a9fc1"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 434e8ff..ded7925 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,24 +1,18 @@ [project] -name = "brooks_xpeel_module" +name = "bmg_module" version = "1.1.0" -description = "Software for automatting a A4S sealer device" +description = "Software for automatting a BMG VANTAstar" authors = [ - {name = "Doga Ozgulbas", email="dozgulbas@anl.gov"}, + {name = "Casey Stone", email="cstone@anl.gov"}, {name = "Ryan D. Lewis", email="ryan.lewis@anl.gov"}, - {name = "Tobias Ginsburg", email = "tginsburg@anl.gov"}, ] dependencies = [ - "fastapi>=0.103", - "uvicorn>=0.14.0", - "pyserial", "madsci.node_module~=0.5.0", "madsci.client~=0.5.0", "madsci.common~=0.5.0", - "pytest", "pywin32", "comtypes", - "starlette", ] requires-python = ">=3.9.1" diff --git a/run_bmg.bat b/run_bmg.bat new file mode 100644 index 0000000..86649d5 --- /dev/null +++ b/run_bmg.bat @@ -0,0 +1,4 @@ +cd /d "%~dp0" +call ".venv\Scripts\activate.bat" +python src\bmg_rest_node.py --port 3003 +pause diff --git a/src/bmg_rest_node.py b/src/bmg_rest_node.py index 2c1c34d..842ebc1 100644 --- a/src/bmg_rest_node.py +++ b/src/bmg_rest_node.py @@ -57,8 +57,6 @@ def startup_handler(self) -> None: # start the BMG thread after resources are initialized self.bmg_thread = BMGThread( - resource_client=self.resource_client, - plate_carrier=self.plate_carrier, logger=self.logger, ) self.bmg_thread.start() @@ -117,7 +115,7 @@ def state_handler(self) -> None: } except Exception as e: # Do nothing except log the error if state handler doesn't work - self.logger.log_error(f"Error in state handler: {e}") + self.logger.log_warning(f"Error in state handler: {e}") def shutdown_handler(self) -> None: """Called to clean up resources before the node is shut down.""" From 4f51d0c9c4a80a34e30af0db3d98167480e0cf9c Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Fri, 14 Nov 2025 12:56:02 -0600 Subject: [PATCH 10/28] small changes from Ryan --- Dockerfile | 25 ------------------------- default.node.info.yaml | 2 +- run_bmg.bat | 2 +- 3 files changed, 2 insertions(+), 27 deletions(-) delete mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 1f33ec4..0000000 --- a/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -FROM ghcr.io/ad-sdl/wei - -# TODO: update labels, if neccessary -LABEL org.opencontainers.image.source=https://github.com/AD-SDL/bmg_module -LABEL org.opencontainers.image.description="A template python module that demonstrates basic WEI module functionality." -LABEL org.opencontainers.image.licenses=MIT - -######################################### -# Module specific logic goes below here # -######################################### - -RUN mkdir -p bmg_module - -COPY ./src bmg_module/src -COPY ./README.md bmg_module/README.md -COPY ./pyproject.toml bmg_module/pyproject.toml - -RUN --mount=type=cache,target=/root/.cache \ - pip install ./bmg_module - -# TODO: Add any device-specific container configuration/setup here - -CMD ["python", "bmg_module.py"] - -######################################### diff --git a/default.node.info.yaml b/default.node.info.yaml index f0da676..a3b338e 100644 --- a/default.node.info.yaml +++ b/default.node.info.yaml @@ -105,7 +105,7 @@ config: update_node_files: true status_update_interval: 2.0 state_update_interval: 5.0 - node_url: http://127.0.0.1:2000/ + node_url: http://suestorm.cels.anl.gov:3003/ uvicorn_kwargs: {} output_path: C:\Users\RPL\TEST db_directory_path: C:\Program Files (x86)\BMG\CLARIOstar\User\Definit diff --git a/run_bmg.bat b/run_bmg.bat index 86649d5..bebbdb1 100644 --- a/run_bmg.bat +++ b/run_bmg.bat @@ -1,4 +1,4 @@ cd /d "%~dp0" call ".venv\Scripts\activate.bat" -python src\bmg_rest_node.py --port 3003 +python src\bmg_rest_node.py --node_url http://suestorm.cels.anl.gov:3003 pause From 79366517ea7466eda6faa0e288a97f42db32f010 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Tue, 18 Nov 2025 16:50:53 -0600 Subject: [PATCH 11/28] added todos and node definition --- definitions/bmg.info.yaml | 0 .../{inheco.node.info.yaml => bmg_billy.info.yaml} | 12 ++++++------ definitions/{bmg.node.yaml => bmg_billy.node.yaml} | 4 ++-- definitions/inheco.node.yaml | 7 ------- run_bmg.bat | 2 +- src/bmg_rest_node.py | 14 ++++++++------ 6 files changed, 17 insertions(+), 22 deletions(-) delete mode 100644 definitions/bmg.info.yaml rename definitions/{inheco.node.info.yaml => bmg_billy.info.yaml} (94%) rename definitions/{bmg.node.yaml => bmg_billy.node.yaml} (71%) delete mode 100644 definitions/inheco.node.yaml diff --git a/definitions/bmg.info.yaml b/definitions/bmg.info.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/definitions/inheco.node.info.yaml b/definitions/bmg_billy.info.yaml similarity index 94% rename from definitions/inheco.node.info.yaml rename to definitions/bmg_billy.info.yaml index 89559ec..5000133 100644 --- a/definitions/inheco.node.info.yaml +++ b/definitions/bmg_billy.info.yaml @@ -1,9 +1,9 @@ -node_name: inheco.node -node_id: 01K9DJ8X5PM87Y3YWCBPYJHMXS -node_description: null +node_name: bmg_billy +node_id: 01KAC1JS0HYKAQDD39PKYN36S4 +node_description: A node definition for BMG plate reader node_type: device -module_name: bmg_node -module_version: 0.0.1 +module_name: bmg_module +module_version: 1.1.0 capabilities: get_info: true get_state: true @@ -100,7 +100,7 @@ actions: var_args_schema: null var_kwargs_schema: null config: - node_definition: definitions\inheco.node.yaml + node_definition: C:\\Users\\RPL\\source\\repos\\bmg_module\\definitions\\bmg_billy.node.yaml node_info_path: null update_node_files: true status_update_interval: 2.0 diff --git a/definitions/bmg.node.yaml b/definitions/bmg_billy.node.yaml similarity index 71% rename from definitions/bmg.node.yaml rename to definitions/bmg_billy.node.yaml index 8f3be69..0127035 100644 --- a/definitions/bmg.node.yaml +++ b/definitions/bmg_billy.node.yaml @@ -1,5 +1,5 @@ -node_name: bmg -node_id: +node_name: bmg_billy +node_id: 01KAC1JS0HYKAQDD39PKYN36S4 node_description: A node definition for BMG plate reader node_type: device module_name: bmg_module diff --git a/definitions/inheco.node.yaml b/definitions/inheco.node.yaml deleted file mode 100644 index b5d1358..0000000 --- a/definitions/inheco.node.yaml +++ /dev/null @@ -1,7 +0,0 @@ -node_name: inheco.node -node_id: 01K9DJ8X5PM87Y3YWCBPYJHMXS -node_description: null -node_type: device -module_name: bmg_node -module_version: 0.0.1 -capabilities: null diff --git a/run_bmg.bat b/run_bmg.bat index bebbdb1..c911094 100644 --- a/run_bmg.bat +++ b/run_bmg.bat @@ -1,4 +1,4 @@ cd /d "%~dp0" call ".venv\Scripts\activate.bat" -python src\bmg_rest_node.py --node_url http://suestorm.cels.anl.gov:3003 +python src\bmg_rest_node.py --node_url http://suestorm.cels.anl.gov:3003 --node_definition "C:\\Users\\RPL\\source\\repos\\bmg_module\\definitions\\bmg_billy.node.yaml" pause diff --git a/src/bmg_rest_node.py b/src/bmg_rest_node.py index 842ebc1..cc08a7d 100644 --- a/src/bmg_rest_node.py +++ b/src/bmg_rest_node.py @@ -22,13 +22,15 @@ - Something is wrong with passing paths in through the command line args. Only works when default paths are set in BMGNodeConfig TASK: make these into Path types, not string and test. Probably the double slash when passing in string is the issue +- TODO: Take output path out of config and make it a required argument in run_assay that users have to specify in the workflow + """ class BMGNodeConfig(RestNodeConfig): """Configuration for the BMG node.""" - output_path: str = "C:\\Users\\RPL\\TEST" + output_path: str = "C:\\Users\\RPL\\TEST" # TODO: change this to the default BMG output file location """Data output directory path for bmg data""" db_directory_path: str = "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" """Path to directory where assay protocol files are stored""" @@ -66,10 +68,9 @@ def init_resource_templates(self) -> None: self.resource_client.create_template( resource=Slot( - resource_class="bmg_plate_nest", resource_description="The plate nest for a BMG microplate reader", ), - template_name="bmg_plate_nest", + template_name="bmg.nest", description="Template of a BMG microplate reader plate nest", tags=["PlateNest", "ANSI/SLAS"], ) @@ -78,8 +79,8 @@ def create_resources(self) -> None: """Create resources for the node module.""" self.plate_carrier = self.resource_client.create_resource_from_template( - "bmg_plate_nest", - resource_name=f"{self.node_definition.node_name}_plate_nest", + template_name="bmg.nest", + resource_name=f"{self.node_definition.node_name}.nest", ) def state_handler(self) -> None: @@ -187,7 +188,7 @@ def run_assay( """Runs an assay on the BMG plate reader""" # run the assay, collect response containting output data file name - response = self.bmg_thread.send_commmand( + response = self.bmg_thread.send_command( { "action": "run_assay", "protocol_name": assay_name, @@ -200,6 +201,7 @@ def run_assay( # interpret response if not response["success"]: self.logger.log_info(f"Error running assay: {response['error']}") + self.logger.log_info(f"response: {response}") return ActionFailed(errors=[f"Error running assay: {response['error']}"]) return Path(response["data"]) From e92682059f0f2e0ede1b0944cbf63cea67e7d6ab Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Thu, 20 Nov 2025 17:20:39 -0600 Subject: [PATCH 12/28] debugging lines added for testing tomorrow --- definitions/bmg_billy.info.yaml | 15 ++++++-- pdm.lock | 18 ++++----- src/bmg_interface.py | 33 ++++++++++++----- src/bmg_object_thread.py | 10 ++++- src/bmg_rest_node.py | 66 ++++++++++++++++++++++++++------- 5 files changed, 104 insertions(+), 38 deletions(-) diff --git a/definitions/bmg_billy.info.yaml b/definitions/bmg_billy.info.yaml index 5000133..334c916 100644 --- a/definitions/bmg_billy.info.yaml +++ b/definitions/bmg_billy.info.yaml @@ -79,6 +79,13 @@ actions: argument_type: str required: true default: null + data_output_directory_path: + name: data_output_directory_path + description: data output directory path. Path must point to an existing folder. + Defaults to 'C:\Program Files (x86)\BMG\CLARIOstar\User\Data' + argument_type: str + required: false + default: null data_output_file_name: name: data_output_file_name description: data output file name (ex. data.txt). Will default to .txt @@ -107,7 +114,7 @@ config: state_update_interval: 5.0 node_url: http://suestorm.cels.anl.gov:3003/ uvicorn_kwargs: {} - output_path: C:\Users\RPL\TEST + data_output_directory_path: C:\Program Files (x86)\BMG\CLARIOstar\User\Data db_directory_path: C:\Program Files (x86)\BMG\CLARIOstar\User\Definit config_schema: description: Configuration for the BMG node. @@ -166,9 +173,9 @@ config_schema: description: Configuration for the Uvicorn server that runs the REST API. title: Uvicorn Configuration type: object - output_path: - default: C:\Users\RPL\TEST - title: Output Path + data_output_directory_path: + default: C:\Program Files (x86)\BMG\CLARIOstar\User\Data + title: Data Output Directory Path type: string db_directory_path: default: C:\Program Files (x86)\BMG\CLARIOstar\User\Definit diff --git a/pdm.lock b/pdm.lock index 5cd7ca4..f447845 100644 --- a/pdm.lock +++ b/pdm.lock @@ -655,7 +655,7 @@ files = [ [[package]] name = "madsci-client" -version = "0.5.3" +version = "0.5.4" requires_python = ">=3.9.1" summary = "The Modular Autonomous Discovery for Science (MADSci) Python Clients." groups = ["default"] @@ -666,13 +666,13 @@ dependencies = [ "trogon>=0.6.0", ] files = [ - {file = "madsci_client-0.5.3-py3-none-any.whl", hash = "sha256:1b611f53c0adb1a0e4b14b820e1159978f91ecd7437fab10001f52ab0e663894"}, - {file = "madsci_client-0.5.3.tar.gz", hash = "sha256:d7335229f2d7ad7a32baffcd26bb8753098a00af59b1b0ebb38d5c3da2faf5dc"}, + {file = "madsci_client-0.5.4-py3-none-any.whl", hash = "sha256:da0811d75f8d8a402c1f1a05cae6bcffdba979d0f988447555c42cf90b1ce6b3"}, + {file = "madsci_client-0.5.4.tar.gz", hash = "sha256:64754d904ba2a3ee5e256d2c8fcb9c7eab253a0e9289c67c1325357779cc309f"}, ] [[package]] name = "madsci-common" -version = "0.5.3" +version = "0.5.4" requires_python = ">=3.9.1" summary = "The Modular Autonomous Discovery for Science (MADSci) Common Definitions and Utilities." groups = ["default"] @@ -697,13 +697,13 @@ dependencies = [ "uvicorn[standard]>=0.32.0", ] files = [ - {file = "madsci_common-0.5.3-py3-none-any.whl", hash = "sha256:69cbcdad127e2c63ef8f086885844a1b1a0231833282fafa71bddb452e5681f7"}, - {file = "madsci_common-0.5.3.tar.gz", hash = "sha256:ad0c04d0f7363404278c6ec26b78743b22e34e0c2fe01e5bfd0a12be60d76ce5"}, + {file = "madsci_common-0.5.4-py3-none-any.whl", hash = "sha256:98ea6e0cf407a7fbc88767149f5e4f46cb384c8b7960530b7e3629114a1ae126"}, + {file = "madsci_common-0.5.4.tar.gz", hash = "sha256:a92cef1d94cf6871ed638ac369e2f005935ab0df6af28544412acb30a1090aa8"}, ] [[package]] name = "madsci-node-module" -version = "0.5.3" +version = "0.5.4" requires_python = ">=3.9.1" summary = "The Modular Autonomous Discovery for Science (MADSci) Node Module Helper Classes." groups = ["default"] @@ -713,8 +713,8 @@ dependencies = [ "regex", ] files = [ - {file = "madsci_node_module-0.5.3-py3-none-any.whl", hash = "sha256:32616ae0cfaaaecbba2641b40f1e8a7a59de4acd9fd96b5ae4c015cf00191ed2"}, - {file = "madsci_node_module-0.5.3.tar.gz", hash = "sha256:287f7e50cc2fe8ba87784b946f34e881d2a6e7f3b4fb10623b313a5aacffaa10"}, + {file = "madsci_node_module-0.5.4-py3-none-any.whl", hash = "sha256:65bd8bb0184f11e5309e742ca4fa150b9a0f6a34d2e69f2b525478006870b180"}, + {file = "madsci_node_module-0.5.4.tar.gz", hash = "sha256:dfa68b2e55e4230d6259f228000c1f40fddf26527a2805069679d27f5d7f6ea7"}, ] [[package]] diff --git a/src/bmg_interface.py b/src/bmg_interface.py index 4daca7e..f897987 100644 --- a/src/bmg_interface.py +++ b/src/bmg_interface.py @@ -11,9 +11,6 @@ import pythoncom from madsci.client.event_client import EventClient -"""TODO: -- when to edit resource client and plate carrier? - there's not really a state change of the plate like in the peeler""" - class BmgCom: """Class to communicate with BMG microplate readers via ActiveX COM interface.""" @@ -129,7 +126,7 @@ def run_assay( self, protocol_name: str, protocol_database_path: str, - data_output_directory: str, + data_output_directory_path: str, data_output_file_name: Optional[str] = None, plate_id1: int = 1, # these plate IDs are optional plate_id2: int = 2, # but why? what do they do? @@ -142,18 +139,18 @@ def run_assay( data_output_file_name = str(int(time.time())) + ".txt" # format the data output file name and path - data_dir = Path(data_output_directory) + data_dir = Path(data_output_directory_path) data_file_path = data_dir / data_output_file_name self.exec( "Run", protocol_name, protocol_database_path, - data_output_directory, + data_output_directory_path, plate_id1, plate_id2, plate_id3, - data_output_directory, + data_output_directory_path, data_output_file_name, ) @@ -166,10 +163,26 @@ def is_busy(self) -> bool: def exec(self, cmd: str, *args: Any) -> None: """Executed a command over the established connection with the BMG plate reader""" args = (cmd, *args) - res = self.com.ExecuteAndWait(args) - if res: - raise Exception(f"command {cmd} failed: {res}") + # testing + print("status before: ", self.status()) + res = self.com.ExecuteAndWait(args) + + # TESTING + self.logger.log_info(f"Run assay response: {res=}") + + # TESTING + if int(res) == -10: + self.logger.log_error("Response of -10 found!!") + + + # testing + print("status after: ", self.status()) + # if res: + # raise Exception(f"command {cmd} failed: {res}") + + # Testing + return res if __name__ == "__main__": com = BmgCom("CLARIOstar") diff --git a/src/bmg_object_thread.py b/src/bmg_object_thread.py index 961b465..ba2fa69 100644 --- a/src/bmg_object_thread.py +++ b/src/bmg_object_thread.py @@ -61,14 +61,18 @@ def run(self) -> None: temps = self.bmg.read_temps() result["success"] = True result["data"] = temps + elif action == "device_state": + device_state = self.bmg.status() + result["success"] = True + result["data"] = device_state elif action == "run_assay": data_filename = self.bmg.run_assay( protocol_name=command.get("protocol_name"), protocol_database_path=command.get( "protocol_database_path" ), - data_output_directory=command.get( - "data_output_directory" + data_output_directory_path=command.get( + "data_output_directory_path" ), data_output_file_name=command.get( "data_output_file_name" @@ -85,6 +89,8 @@ def run(self) -> None: result["error"] = f"Unknown action: {action}" except Exception as e: + # TESTING + self.logger.log_error(f"PROCESSING COMMAND FAILED: {e}") result["error"] = str(e) self.logger.log_error( f"Error processing command {action}: {e}" diff --git a/src/bmg_rest_node.py b/src/bmg_rest_node.py index cc08a7d..99a39d2 100644 --- a/src/bmg_rest_node.py +++ b/src/bmg_rest_node.py @@ -1,7 +1,6 @@ """ REST-based node for BMG microplate readers that interfaces with WEI """ - from pathlib import Path from typing import Annotated, Optional @@ -16,21 +15,20 @@ from bmg_interface import BmgCom from bmg_object_thread import BMGThread + """ TODOs: - Something is wrong with passing paths in through the command line args. Only works when default paths are set in BMGNodeConfig TASK: make these into Path types, not string and test. Probably the double slash when passing in string is the issue -- TODO: Take output path out of config and make it a required argument in run_assay that users have to specify in the workflow - """ class BMGNodeConfig(RestNodeConfig): """Configuration for the BMG node.""" - output_path: str = "C:\\Users\\RPL\\TEST" # TODO: change this to the default BMG output file location + data_output_directory_path: str = "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" """Data output directory path for bmg data""" db_directory_path: str = "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" """Path to directory where assay protocol files are stored""" @@ -50,6 +48,10 @@ def __init__(self) -> None: """Initializes the BMG node.""" super().__init__() self.bmg_thread = None + self.cached_temp1 = None + self.cached_temp2 = None + self.cached_temp3 = None + self.cached_device_state = None def startup_handler(self) -> None: """Called to (re)initialize the node. Should be used to open connections to devices or initialize any other resources.""" @@ -85,23 +87,35 @@ def create_resources(self) -> None: def state_handler(self) -> None: """Periodically check state of BMG device""" - self.cached_temp1 = None - self.cached_temp2 = None - self.cached_temp3 = None if self.bmg_thread is None: self.logger.log_error("BMG thread is not initialized") return + if self.bmg_thread.is_busy: self.node_state = { "Temp1 (bottom heating plate)": self.cached_temp1, "Temp2 (top heating plate)": self.cached_temp2, "Temp3 (optic slide heating plate)": self.cached_temp3, "bmg_thread_state": "BUSY", + "bmg_device_state": "busy", } + else: # thread is not busy, query the device try: + # collect device state + device_state = self.bmg_thread.send_command({"action": "device_state"}) + if not response["success"]: + self.cached_device_state = "unknown" + else: + self.cached_device_state = device_state["data"] + + except Exception as e: + self.logger.log_error(f"Error collecting device state: {e}") + + try: + # collect temperature readings response = self.bmg_thread.send_command({"action": "read_temps"}) temps = response["data"] self.cached_temp1 = temps["Temp1"] @@ -109,10 +123,11 @@ def state_handler(self) -> None: self.cached_temp3 = temps["Temp3"] self.node_state = { - "Temp1 (bottom heating plate)": temps["Temp1"], - "Temp2 (top heating plate)": temps["Temp2"], - "Temp3 (optic slide heating plate)": temps["Temp3"], + "Temp1 (bottom heating plate)": self.cached_temp1, + "Temp2 (top heating plate)": self.cached_temp2, + "Temp3 (optic slide heating plate)": self.cached_temp3, "bmg_thead_state": "READY", + "bmg_device_state": self.cached_device_state } except Exception as e: # Do nothing except log the error if state handler doesn't work @@ -180,6 +195,10 @@ def set_temp(self, temp: float) -> None: def run_assay( self, assay_name: str, + data_output_directory_path: Annotated[ + Optional[str], + "data output directory path. Path must point to an existing folder. Defaults to 'C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data'" + ] = None, data_output_file_name: Annotated[ Optional[str], "data output file name (ex. data.txt). Will default to .txt (ex. 1731706249.txt) if no file name is entered.", @@ -187,21 +206,42 @@ def run_assay( ) -> Annotated[Path, "Data .txt file returned by the BMG microplate reader"]: """Runs an assay on the BMG plate reader""" + # collect and validate the data_directory_path + if data_output_directory_path is None: + data_output_directory_path = self.config.data_output_directory_path + else: + # check that the directory path exists + try: + if not Path(data_output_directory_path).is_dir(): + return ActionFailed(f"data_output_directory_path {data_output_directory_path} is not an existing folder") + except Exception as e: + self.logger.log_error(f"data_directory_output_path is invalid: {e}") + return ActionFailed(f"data_directory_output_path is invalid: {e}") + + # TESTING + self.logger.log_debug(f"{data_output_directory_path=}") + + # TESTING + self.logger.log_debug("calling bmg thread run command") # run the assay, collect response containting output data file name response = self.bmg_thread.send_command( { "action": "run_assay", "protocol_name": assay_name, "protocol_database_path": self.config.db_directory_path, - "data_output_directory": self.config.output_path, + "data_output_directory_path": data_output_directory_path, "data_output_file_name": data_output_file_name, } ) + self.logger.log_debug(f"{response=}") + + + # interpret response if not response["success"]: - self.logger.log_info(f"Error running assay: {response['error']}") - self.logger.log_info(f"response: {response}") + self.logger.log_error(f"Error running assay: {response['error']}") + self.logger.log_error(f"response: {response}") return ActionFailed(errors=[f"Error running assay: {response['error']}"]) return Path(response["data"]) From d11c80b05092da6867c7f2b931f057ae322d1217 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Tue, 25 Nov 2025 14:39:02 -0600 Subject: [PATCH 13/28] cleanup --- src/bmg_interface.py | 71 +++++++++++++++++++++------------------- src/bmg_object_thread.py | 35 ++++++++++---------- src/bmg_rest_node.py | 56 ++++++++++++++----------------- 3 files changed, 78 insertions(+), 84 deletions(-) diff --git a/src/bmg_interface.py b/src/bmg_interface.py index f897987..adea5ee 100644 --- a/src/bmg_interface.py +++ b/src/bmg_interface.py @@ -78,11 +78,13 @@ def plate_out(self) -> None: def set_temp(self, temp: float) -> None: """Sets the temperature on the BMG plate reader. - Allowed values: - 00.0 = The incubator unit will be switched off. - 00.1 = Temperature will not be controlled, but will be monitored. - 25.0 - 45.0 = Incubator will be switched on and new temp value will be set. Can be changed in increments of 0.1 deg C - 10.0 - 60.0 = This range is ONLY allowed on extended range models. WE DO NOT HAVE THIS + Args: + temp (float): Temperature in Celsius + Allowed values: + 00.0 = The incubator unit will be switched off. + 00.1 = Temperature will not be controlled, but will be monitored. + 25.0 - 45.0 = Incubator will be switched on and new temp value will be set. Can be changed in increments of 0.1 deg C + 10.0 - 60.0 = This range is ONLY allowed on extended range models. Notes: - Will throw error code -20 if temp input is not a valid value. @@ -95,7 +97,12 @@ def set_temp(self, temp: float) -> None: def read_temps(self) -> dict: """Reads the temperature at three locations in the BMG plate reader - Returns: + Returns: a dictionary of temperature readouts. + temps = { + "Temp1": (float temperature reading from bottom heating plate) + "Temp2": (float temperature reading from top heating plate) + "Temp3:" (float temperature reading from optic slide heating plate) + } """ temps = {} @@ -107,7 +114,7 @@ def read_temps(self) -> dict: temp3 = self.com.GetInfo(temp3_formatted) try: - # convert to floats in celsius + # Convert to floats in Celsius temp1 = float(temp1) / 10 temp2 = float(temp2) / 10 temp3 = float(temp3) / 10 @@ -117,34 +124,47 @@ def read_temps(self) -> dict: "Temp3": temp3, } except Exception: - # Don't do anything except log if temperature collection fails + # Don't raise exception if temperature collection fails. self.logger.log_error("Error collecting temperatures: {e}") return temps def run_assay( self, - protocol_name: str, + assay_name: str, protocol_database_path: str, data_output_directory_path: str, data_output_file_name: Optional[str] = None, - plate_id1: int = 1, # these plate IDs are optional - plate_id2: int = 2, # but why? what do they do? - plate_id3: int = 3, # and why are there three? curious. + plate_id1: int = 1, + plate_id2: int = 2, + plate_id3: int = 3, ) -> str: - """Runs an assay on the BMG plate reader""" + """Runs an assay on the BMG plate reader. + + Args: + assay_name (str): Name of the assay to run, name matches existing protocol name in SMART Control Software. + protocol_database_path (str): Path to directory where assay protocol files are stored. + data_output_directory (str): Path to data output directory for bmg data. Must be an existing directory. + data_output_file_name (str, optional): data output file name (ex. "data.txt"). + plate_id1 (int): Assay will not run without an integer passed in here. It's unclear what this plate_id does. + plate_id2 (int): Assay will not run without an integer passed in here. It's unclear what this plate_id does. + plate_id3 (int): Assay will not run without an integer passed in here. It's unclear what this plate_id does. + + Returns: + data_file_path (str): Path to resulting data file. + """ - # give the data file a unique name if no name is specified + # Give the data file a unique name if no name is specified if not data_output_file_name: data_output_file_name = str(int(time.time())) + ".txt" - # format the data output file name and path + # Format the data output file name and path data_dir = Path(data_output_directory_path) data_file_path = data_dir / data_output_file_name self.exec( "Run", - protocol_name, + assay_name, protocol_database_path, data_output_directory_path, plate_id1, @@ -163,25 +183,8 @@ def is_busy(self) -> bool: def exec(self, cmd: str, *args: Any) -> None: """Executed a command over the established connection with the BMG plate reader""" args = (cmd, *args) - - # testing - print("status before: ", self.status()) res = self.com.ExecuteAndWait(args) - - # TESTING - self.logger.log_info(f"Run assay response: {res=}") - - # TESTING - if int(res) == -10: - self.logger.log_error("Response of -10 found!!") - - - # testing - print("status after: ", self.status()) - # if res: - # raise Exception(f"command {cmd} failed: {res}") - - # Testing + self.logger.log_info(f"exec response: {res}") return res if __name__ == "__main__": diff --git a/src/bmg_object_thread.py b/src/bmg_object_thread.py index ba2fa69..caf5f23 100644 --- a/src/bmg_object_thread.py +++ b/src/bmg_object_thread.py @@ -16,10 +16,10 @@ class BMGThread(threading.Thread): def __init__(self, logger: EventClient = None) -> None: """Initializes the BMGThread object""" super().__init__(daemon=True) + self.logger = logger or EventClient() self.lock = threading.Lock() - # communication queue self.command_queue = queue.Queue() self.response_queue = queue.Queue() self.shutdown_event = threading.Event() @@ -29,7 +29,7 @@ def __init__(self, logger: EventClient = None) -> None: def run(self) -> None: """Main thread loop for BMG communication.""" try: - # initialize BMG communication + # Initialize BMG communication self.bmg = BmgCom( "CLARIOstar", logger=self.logger, @@ -38,11 +38,11 @@ def run(self) -> None: while not self.shutdown_event.is_set(): try: - # wait for command + # Wait for command command = self.command_queue.get(timeout=1) self.logger.log_info(f"Processing command: {command}") - # process command + # Process command action = command.get("action") result = {"success": False, "data": None, "error": None} with self.lock: @@ -67,7 +67,7 @@ def run(self) -> None: result["data"] = device_state elif action == "run_assay": data_filename = self.bmg.run_assay( - protocol_name=command.get("protocol_name"), + assay_name=command.get("protocol_name"), protocol_database_path=command.get( "protocol_database_path" ), @@ -81,16 +81,15 @@ def run(self) -> None: if data_filename: result["data"] = data_filename - # Don't fail the action if no data is returned - # presumably the data is still backed up on local machine. + """Don't fail the action if no data is returned. + Presumably the data is still backed up on local machine.""" + result["success"] = True else: result["error"] = f"Unknown action: {action}" except Exception as e: - # TESTING - self.logger.log_error(f"PROCESSING COMMAND FAILED: {e}") result["error"] = str(e) self.logger.log_error( f"Error processing command {action}: {e}" @@ -100,15 +99,15 @@ def run(self) -> None: self.response_queue.put(result) except queue.Empty: - # no command received, loop again + # No command received, loop again continue except Exception as e: self.logger.log_error(f"Error in BMG thread: {e}") - return # Should I be returning here? - kills the thread if there's an error during init? + return finally: - # clean up BMG communication + # Clean up BMG communication. if self.bmg: try: self.bmg.close_connection() @@ -119,7 +118,7 @@ def run(self) -> None: def _process_command(self, command: dict) -> dict: # TODO: Extract processing from run function here. - # as is, pydantic says the run funtion is too complex. + # as is, pydantic says the run function is too complex. pass def send_command(self, command: dict, timeout: float = 300.0) -> dict: @@ -127,10 +126,10 @@ def send_command(self, command: dict, timeout: float = 300.0) -> dict: Args: command (dict): Command to send to the BMG thread. - timeout (float): Time to wait for a response. + timeout (float, optional): Time to wait for a response. Returns: - dict: Response from the BMG thread. + response (dict): Response from the BMG thread. """ self.command_queue.put(command) @@ -146,7 +145,7 @@ def send_command(self, command: dict, timeout: float = 300.0) -> dict: def stop(self) -> None: """Signal thread to stop and wait for it to finish.""" - # close connection to the bmg device + # Close connection to the bmg device try: if self.bmg: self.bmg.close_connection() @@ -156,9 +155,9 @@ def stop(self) -> None: "No BMG device connection open, unable to close nonexistent connection." ) except Exception: - self.logger.log_warning("Unable to close connection to bmg device") + self.logger.log_warning("Unable to close connection to bmg device.") - # shut down the thread + # Shut down the thread self.shutdown_event.set() self.join(timeout=5) diff --git a/src/bmg_rest_node.py b/src/bmg_rest_node.py index 99a39d2..2df2cd8 100644 --- a/src/bmg_rest_node.py +++ b/src/bmg_rest_node.py @@ -59,7 +59,7 @@ def startup_handler(self) -> None: self.init_resource_templates() self.create_resources() - # start the BMG thread after resources are initialized + # Start the BMG thread after resources are initialized self.bmg_thread = BMGThread( logger=self.logger, ) @@ -102,9 +102,9 @@ def state_handler(self) -> None: } else: - # thread is not busy, query the device + # Thread is not busy, query the device try: - # collect device state + # Collect device state device_state = self.bmg_thread.send_command({"action": "device_state"}) if not response["success"]: self.cached_device_state = "unknown" @@ -115,7 +115,7 @@ def state_handler(self) -> None: self.logger.log_error(f"Error collecting device state: {e}") try: - # collect temperature readings + # Collect temperature readings response = self.bmg_thread.send_command({"action": "read_temps"}) temps = response["data"] self.cached_temp1 = temps["Temp1"] @@ -144,52 +144,52 @@ def shutdown_handler(self) -> None: @action(name="open") def open(self) -> None: - """Opens the BMG plate tray""" + """Opens the BMG plate tray.""" - self.logger.log_info("Opening BMG plate tray") + self.logger.log_info("Opening BMG plate tray.") - # send command to BMG thread + # Send command to BMG thread response = self.bmg_thread.send_command({"action": "plate_out"}) - # interpret response + # Interpret response if not response["success"]: raise Exception(f"Failed to open BMG plate tray: {response['error']}") - self.logger.log_info("BMG plate tray opened") + self.logger.log_info("BMG plate tray opened.") @action(name="close") def close(self) -> None: - """Closes the BMG plate tray""" + """Closes the BMG plate tray.""" - self.logger.log_info("Closing BMG plate tray") + self.logger.log_info("Closing BMG plate tray.") - # send command to BMG thread + # Send command to BMG thread response = self.bmg_thread.send_command({"action": "plate_in"}) - # interpret response + # Interpret response if not response["success"]: raise Exception(f"Failed to close BMG plate tray: {response['error']}") - self.logger.log_info("BMG plate tray closed") + self.logger.log_info("BMG plate tray closed.") @action(name="set_temp") def set_temp(self, temp: float) -> None: - """Sets the temperature on the BMG microplate reader""" + """Sets the temperature on the BMG microplate reader.""" temp = float(temp) if temp in {0.0, 0.1} or 25.0 <= temp <= 45.0: - # temp input is valid, send the command + # Temp input is valid, send the command response = self.bmg_thread.send_command( {"action": "set_temp", "temp": temp} ) - # interpret response + # Interpret response if not response["success"]: self.logger.log_error(f"Error setting temperature: {response['error']}") return ActionFailed( errors=[f"Error setting temperature: {response['error']}"] ) return None - # temp input is not valid (fail action, don't put node in error state) - return ActionFailed(errors=["Invalid temperature input value"]) + # Temp input is not valid (fail action, don't put node in error state) + return ActionFailed(errors=["Invalid temperature input value."]) @action(name="run_assay") def run_assay( @@ -206,24 +206,19 @@ def run_assay( ) -> Annotated[Path, "Data .txt file returned by the BMG microplate reader"]: """Runs an assay on the BMG plate reader""" - # collect and validate the data_directory_path + # Collect and validate the data_directory_path if data_output_directory_path is None: data_output_directory_path = self.config.data_output_directory_path else: - # check that the directory path exists + # Check that the directory path exists try: if not Path(data_output_directory_path).is_dir(): return ActionFailed(f"data_output_directory_path {data_output_directory_path} is not an existing folder") except Exception as e: self.logger.log_error(f"data_directory_output_path is invalid: {e}") return ActionFailed(f"data_directory_output_path is invalid: {e}") - - # TESTING - self.logger.log_debug(f"{data_output_directory_path=}") - # TESTING - self.logger.log_debug("calling bmg thread run command") - # run the assay, collect response containting output data file name + # Run the assay, collect response containing output data file name response = self.bmg_thread.send_command( { "action": "run_assay", @@ -233,12 +228,9 @@ def run_assay( "data_output_file_name": data_output_file_name, } ) - - self.logger.log_debug(f"{response=}") - - - # interpret response + # Interpret response + self.logger.log_debug(f"{response=}") if not response["success"]: self.logger.log_error(f"Error running assay: {response['error']}") self.logger.log_error(f"response: {response}") From ab11b49a56854426381234e57a42e572c6cba986 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Tue, 25 Nov 2025 16:34:57 -0600 Subject: [PATCH 14/28] updated README to MADSci --- README.md | 87 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 50 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index d21c8f8..165f8b9 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,51 @@ # BMG module -A WEI-powered module for controlling BMG Microplate Readers, currently tested with the VANTAstar model. +A MADSci-powered module for controlling BMG Microplate Readers, currently tested with the VANTAstar model. -Contains a BMG driver (bmg_driver.py) and BMG REST Node (bmg_rest_node.py). +Contains a BMG driver (bmg_driver.py), BMG Object Thread Class (bmg_object_thread.py), and BMG REST Node (bmg_rest_node.py). ### Assay Setup on BMG microplate reader -In order to run an assay on the BMG plate reader, you first need to create the assay using the BMG Smart Control software (not the Voyager software). Once the assay is created and saved, it needs to be exported and the .TCS file should be placed inside the BMG directory that contains the assay database .db file ("C:\Program Files (x86)\BMG\CLARIOstar\User\Definit" with default BMG Smart Control on Windows). Once the .TCS file for your assay has been saved into the database directory, it can be accessed by our BMG driver and REST Node by assay name. +To run an assay on the BMG plate reader, you first need to create the assay using the BMG SMART Control software (not the Voyager software). Once the assay is created and saved, it needs to be exported and the .TCS file should be placed inside the BMG directory that contains the assay database .db file ("C:\Program Files (x86)\BMG\CLARIOstar\User\Definit" with default BMG Smart Control on Windows). Once the .TCS file for your assay has been saved into the database directory, it can be accessed by our BMG driver and REST Node by assay name. ### Running instructions -The BMG driver and REST Node can only connect to the device if run with **32-bit python**. In the following commands, be sure that you're running the correct python version, replacing 'python' with the complete path to your 32-bit python .exe file if necessary. +The BMG driver and REST Node can only connect to the device if run with **32-bit python**. When creating your python virtual environment in the commands below, replace 'python.exe' with your path to a 32-bit python executable. #### Installation -When running the last line of these install instructions, be sure to pip install using your 32-bit python. An example of how to do this is below but make sure to replace python.exe with the path to your 32-bit python .exe. - - python.exe -m pip install -e . - -General install instructions: +Clone the repository: git clone https://github.com/AD-SDL/bmg_module.git cd bmg_module + +Create a virtual environment with 32-bit python, then activate it. Remember to use your 32-bit python path: + + python.exe -m venv .venv + .venv\Scripts\activate + +Install the dependencies using pdm or pip: + + pdm install + +or + pip install -e . -If you're having trouble installing the requirements or wei due to an issue installing httptools, use the Visual Studio Installer (download this if you don't have it already), and either modify or install Visual Studio Community 2022 to include "Desktop development with C++". +If you're having trouble installing the requirements or MADSci due to an issue installing httptools, use the Visual Studio Installer (download this if you don't have it already), and either modify or install Visual Studio Community 2022 to include "Desktop development with C++". -#### Running the driver +#### Running the interface + +Inside the bmg_module directory, run the following commands to test connection to the BMG device through the BMG interface. - cd bmg_module cd src python bmg_driver.py This will print out the current BMG LABTECH Remote Control Version Number if the driver is able to connect correctly to the BMG device. -You can also use the driver in other programs. See the below python program uses the bmg driver to open and close the plate tray, then sets the temperature and runs an assay named ASSAY_TEST. When connecting, the model must be CLARIOstar even when using a VANTAstar model. +You can also use the driver in other programs. The example python program below uses the bmg driver to open and close the plate tray, set the temperature, and run an assay named ASSAY_TEST. + +When instantiating the bmg_device, the model name must be entered as "CLARIOstar" even if you own a BMG VANTAstar device. Also, be sure to replace the protocol_database_path and data_output_directory values with your correct paths. import bmg_interface @@ -46,61 +57,63 @@ You can also use the driver in other programs. See the below python program uses protocol_name = "ASSAY_NAME", protocol_database_path = "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" , data_output_directory = "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data", - data_output_file_name = "assya_data.txt", + data_output_file_name = "assay_data.txt", ) -Be sure to replace the protocol_database_path and data_output_directory with your correct paths. - #### Running the REST Node The REST Node can be started with a command in the format below - python.exe bmg_rest_node.py --port --db_directory_path <(optional) path to bmg db directory containing assay .TCS files> --output_path <(optional) path to directory for saving data output files> + python bmg_rest_node.py --node_url <(str, optional) address for your LiCONiC MADSci REST Node> --db_directory_path <(str, optional) path to bmg db directory containing assay .TCS files> --output_path <(str, optional) path to directory for saving data output files> +--node_url will default to "http://127.0.0.1:2000" \ +--db_directory_path will default to "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" \ +and -- output_path will default to "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" ---db_directory_path will default to "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" unless specified \ -and -- output_path will default to "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" unless specified. +Example usage with no optional arguments: -Example usage with no optional arguments (remember to use 32-bit python): - - python.exe bmg_rest_node.py --port 3003 + python bmg_rest_node.py Example usage with all optional arguments: - python.exe bmg_rest_node.py --port 3003 --db_directory_path "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" --output_path "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" + python bmg_rest_node.py --node_url "http://127.0.0.1:3003" --db_directory_path "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" --output_path "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" + +### Example Usage in MADSci Workflow YAML file -### Example Usage in WEI Workflow YAML file +Below is an example of a MADSci YAML Workflow file that could interact with the BMG REST Node. Replace "ASSAY_NAME" and "ASSAY_DATA.txt" with the name of the assay you wish to run on the BMG and file name for the output data. -Below is an example of a YAML WEI Workflow file that could interact with the BMG REST Node. + name: Test Workflow - name: BMG Example - author: RPL - info: An example WEI workflow to show available BMG actions - version: '0.1' + metadata: + author: Casey Stone + info: Example MADSci workflow for BMG actions + version: 0.1 - flowdef: - - name: open bmg - module: bmg + steps: + - name: open BMG + node: bmg action: open - name: close bmg - module: bmg + node: bmg action: close - name: set temp - module: bmg + node: bmg action: set_temp args: temp: 30.0 - name: Run bmg - module: bmg + node: bmg action: run_assay args: - assay_name: Assay_name - data_output_file_name: assay_data.txt + assay_name: ASSAY_NAME + data_output_file_name: ASSAY_DATA.txt + + From b9dd53139926bd17084f7f3f72ccf26f4292a8e5 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Tue, 25 Nov 2025 16:48:09 -0600 Subject: [PATCH 15/28] bmg_object_thread reorganized, passing pre-commit, testing needed --- README.md | 14 ++--- src/bmg_interface.py | 15 ++--- src/bmg_object_thread.py | 123 ++++++++++++++++++++++----------------- src/bmg_rest_node.py | 36 +++++++----- 4 files changed, 102 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index 165f8b9..505cfae 100644 --- a/README.md +++ b/README.md @@ -14,21 +14,21 @@ The BMG driver and REST Node can only connect to the device if run with **32-bit #### Installation -Clone the repository: +Clone the repository: git clone https://github.com/AD-SDL/bmg_module.git cd bmg_module -Create a virtual environment with 32-bit python, then activate it. Remember to use your 32-bit python path: +Create a virtual environment with 32-bit python, then activate it. Remember to use your 32-bit python path: python.exe -m venv .venv .venv\Scripts\activate -Install the dependencies using pdm or pip: +Install the dependencies using pdm or pip: pdm install -or +or pip install -e . @@ -43,7 +43,7 @@ Inside the bmg_module directory, run the following commands to test connection t This will print out the current BMG LABTECH Remote Control Version Number if the driver is able to connect correctly to the BMG device. -You can also use the driver in other programs. The example python program below uses the bmg driver to open and close the plate tray, set the temperature, and run an assay named ASSAY_TEST. +You can also use the driver in other programs. The example python program below uses the bmg driver to open and close the plate tray, set the temperature, and run an assay named ASSAY_TEST. When instantiating the bmg_device, the model name must be entered as "CLARIOstar" even if you own a BMG VANTAstar device. Also, be sure to replace the protocol_database_path and data_output_directory values with your correct paths. @@ -89,7 +89,7 @@ Below is an example of a MADSci YAML Workflow file that could interact with the name: Test Workflow - metadata: + metadata: author: Casey Stone info: Example MADSci workflow for BMG actions version: 0.1 @@ -115,5 +115,3 @@ Below is an example of a MADSci YAML Workflow file that could interact with the args: assay_name: ASSAY_NAME data_output_file_name: ASSAY_DATA.txt - - diff --git a/src/bmg_interface.py b/src/bmg_interface.py index adea5ee..e0f2aaf 100644 --- a/src/bmg_interface.py +++ b/src/bmg_interface.py @@ -78,7 +78,7 @@ def plate_out(self) -> None: def set_temp(self, temp: float) -> None: """Sets the temperature on the BMG plate reader. - Args: + Args: temp (float): Temperature in Celsius Allowed values: 00.0 = The incubator unit will be switched off. @@ -140,18 +140,18 @@ def run_assay( plate_id3: int = 3, ) -> str: """Runs an assay on the BMG plate reader. - - Args: + + Args: assay_name (str): Name of the assay to run, name matches existing protocol name in SMART Control Software. protocol_database_path (str): Path to directory where assay protocol files are stored. - data_output_directory (str): Path to data output directory for bmg data. Must be an existing directory. + data_output_directory (str): Path to data output directory for bmg data. Must be an existing directory. data_output_file_name (str, optional): data output file name (ex. "data.txt"). plate_id1 (int): Assay will not run without an integer passed in here. It's unclear what this plate_id does. plate_id2 (int): Assay will not run without an integer passed in here. It's unclear what this plate_id does. plate_id3 (int): Assay will not run without an integer passed in here. It's unclear what this plate_id does. - - Returns: - data_file_path (str): Path to resulting data file. + + Returns: + data_file_path (str): Path to resulting data file. """ # Give the data file a unique name if no name is specified @@ -187,6 +187,7 @@ def exec(self, cmd: str, *args: Any) -> None: self.logger.log_info(f"exec response: {res}") return res + if __name__ == "__main__": com = BmgCom("CLARIOstar") print(f"BMG LABTECH Remote Control Version: {com.version()}") # noqa: T201 diff --git a/src/bmg_object_thread.py b/src/bmg_object_thread.py index caf5f23..7cc7466 100644 --- a/src/bmg_object_thread.py +++ b/src/bmg_object_thread.py @@ -43,57 +43,7 @@ def run(self) -> None: self.logger.log_info(f"Processing command: {command}") # Process command - action = command.get("action") - result = {"success": False, "data": None, "error": None} - with self.lock: - try: - if action == "plate_out": - self.bmg.plate_out() - result["success"] = True - elif action == "plate_in": - self.bmg.plate_in() - result["success"] = True - elif action == "set_temp": - temp = command.get("temp") - self.bmg.set_temp(temp=temp) - result["success"] = True - elif action == "read_temps": - temps = self.bmg.read_temps() - result["success"] = True - result["data"] = temps - elif action == "device_state": - device_state = self.bmg.status() - result["success"] = True - result["data"] = device_state - elif action == "run_assay": - data_filename = self.bmg.run_assay( - assay_name=command.get("protocol_name"), - protocol_database_path=command.get( - "protocol_database_path" - ), - data_output_directory_path=command.get( - "data_output_directory_path" - ), - data_output_file_name=command.get( - "data_output_file_name" - ), - ) - if data_filename: - result["data"] = data_filename - - """Don't fail the action if no data is returned. - Presumably the data is still backed up on local machine.""" - - result["success"] = True - - else: - result["error"] = f"Unknown action: {action}" - - except Exception as e: - result["error"] = str(e) - self.logger.log_error( - f"Error processing command {action}: {e}" - ) + result = self._process_command(command=command) # Send response back self.response_queue.put(result) @@ -104,7 +54,7 @@ def run(self) -> None: except Exception as e: self.logger.log_error(f"Error in BMG thread: {e}") - return + return finally: # Clean up BMG communication. @@ -117,9 +67,72 @@ def run(self) -> None: self.logger.log_error(f"Error closing BMG connection: {e}") def _process_command(self, command: dict) -> dict: - # TODO: Extract processing from run function here. - # as is, pydantic says the run function is too complex. - pass + """Process a single command and return the result.""" + action = command.get("action") + result = {"success": False, "data": None, "error": None} + + with self.lock: + try: + if action == "plate_out": + self.bmg.plate_out() + result["success"] = True + elif action == "plate_in": + self.bmg.plate_in() + result["success"] = True + elif action == "set_temp": + self._handle_set_temp(command, result) + elif action == "read_temps": + self._handle_read_temps(result) + elif action == "device_state": + self._handle_device_state(result) + elif action == "run_assay": + self._handle_run_assay(command, result) + else: + result["error"] = f"Unknown action: {action}" + + except Exception as e: + result["error"] = str(e) + self.logger.log_error(f"Error processing command {action}: {e}") + + return result + + def _handle_set_temp(self, command: dict, result: dict) -> None: + """Handle set_temp command.""" + temp = command.get("temp") + self.bmg.set_temp(temp=temp) + result["success"] = True + + def _handle_read_temps(self, result: dict) -> None: + """Handle read_temps command.""" + temps = self.bmg.read_temps() + result["success"] = True + result["data"] = temps + + def _handle_device_state(self, result: dict) -> None: + """Handle device_state command.""" + device_state = self.bmg.status() + result["success"] = True + result["data"] = device_state + + def _handle_run_assay(self, command: dict, result: dict) -> None: + """Handle run_assay command.""" + data_filename = self.bmg.run_assay( + assay_name=command.get("protocol_name"), + protocol_database_path=command.get("protocol_database_path"), + data_output_directory_path=command.get("data_output_directory_path"), + data_output_file_name=command.get("data_output_file_name"), + ) + if data_filename: + result["data"] = data_filename + else: + self.logger.log_error( + "No data_filename returned from _handle_run_assay in bmg_object thread" + ) + self.logger.log_error(f"{data_filename=}") + + # Don't fail the action if no data is returned. + # Presumably the data is still backed up on local machine. + result["success"] = True def send_command(self, command: dict, timeout: float = 300.0) -> dict: """Send a command to the BMG thread and wait for a response. diff --git a/src/bmg_rest_node.py b/src/bmg_rest_node.py index 2df2cd8..7ce94db 100644 --- a/src/bmg_rest_node.py +++ b/src/bmg_rest_node.py @@ -1,6 +1,7 @@ """ REST-based node for BMG microplate readers that interfaces with WEI """ + from pathlib import Path from typing import Annotated, Optional @@ -15,7 +16,6 @@ from bmg_interface import BmgCom from bmg_object_thread import BMGThread - """ TODOs: @@ -28,7 +28,9 @@ class BMGNodeConfig(RestNodeConfig): """Configuration for the BMG node.""" - data_output_directory_path: str = "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" + data_output_directory_path: str = ( + "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" + ) """Data output directory path for bmg data""" db_directory_path: str = "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" """Path to directory where assay protocol files are stored""" @@ -91,7 +93,7 @@ def state_handler(self) -> None: if self.bmg_thread is None: self.logger.log_error("BMG thread is not initialized") return - + if self.bmg_thread.is_busy: self.node_state = { "Temp1 (bottom heating plate)": self.cached_temp1, @@ -106,12 +108,12 @@ def state_handler(self) -> None: try: # Collect device state device_state = self.bmg_thread.send_command({"action": "device_state"}) - if not response["success"]: + if not device_state["success"]: self.cached_device_state = "unknown" - else: + else: self.cached_device_state = device_state["data"] - - except Exception as e: + + except Exception as e: self.logger.log_error(f"Error collecting device state: {e}") try: @@ -127,7 +129,7 @@ def state_handler(self) -> None: "Temp2 (top heating plate)": self.cached_temp2, "Temp3 (optic slide heating plate)": self.cached_temp3, "bmg_thead_state": "READY", - "bmg_device_state": self.cached_device_state + "bmg_device_state": self.cached_device_state, } except Exception as e: # Do nothing except log the error if state handler doesn't work @@ -196,9 +198,9 @@ def run_assay( self, assay_name: str, data_output_directory_path: Annotated[ - Optional[str], - "data output directory path. Path must point to an existing folder. Defaults to 'C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data'" - ] = None, + Optional[str], + "data output directory path. Path must point to an existing folder. Defaults to 'C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data'", + ] = None, data_output_file_name: Annotated[ Optional[str], "data output file name (ex. data.txt). Will default to .txt (ex. 1731706249.txt) if no file name is entered.", @@ -207,13 +209,15 @@ def run_assay( """Runs an assay on the BMG plate reader""" # Collect and validate the data_directory_path - if data_output_directory_path is None: + if data_output_directory_path is None: data_output_directory_path = self.config.data_output_directory_path - else: + else: # Check that the directory path exists - try: + try: if not Path(data_output_directory_path).is_dir(): - return ActionFailed(f"data_output_directory_path {data_output_directory_path} is not an existing folder") + return ActionFailed( + f"data_output_directory_path {data_output_directory_path} is not an existing folder" + ) except Exception as e: self.logger.log_error(f"data_directory_output_path is invalid: {e}") return ActionFailed(f"data_directory_output_path is invalid: {e}") @@ -228,7 +232,7 @@ def run_assay( "data_output_file_name": data_output_file_name, } ) - + # Interpret response self.logger.log_debug(f"{response=}") if not response["success"]: From 847079b7eedfd4055f11d4979a2a164c8af02a5b Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Mon, 8 Dec 2025 17:10:45 -0600 Subject: [PATCH 16/28] return plate resource id with assay data file path --- src/bmg_rest_node.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/bmg_rest_node.py b/src/bmg_rest_node.py index 7ce94db..f76525c 100644 --- a/src/bmg_rest_node.py +++ b/src/bmg_rest_node.py @@ -7,9 +7,7 @@ from madsci.common.types.action_types import ActionFailed from madsci.common.types.node_types import RestNodeConfig -from madsci.common.types.resource_types import ( - Slot, -) +from madsci.common.types.resource_types import Slot from madsci.node_module.helpers import action from madsci.node_module.rest_node_module import RestNode @@ -87,6 +85,20 @@ def create_resources(self) -> None: resource_name=f"{self.node_definition.node_name}.nest", ) + def collect_current_plate_resource(self) -> str: + """Collects the resource ID of the labware in the BMG Plate Nest according to the Resource Manager""" + assay_plate_resource_id = None + try: + child_resource = self.resource_client.get_resource(self.plate_carrier).child + assay_plate_resource_id = ( + child_resource.resource_id if child_resource else None + ) + except Exception as e: + # Don't fail the action if the child resource ID cannot be collected + self.logger.log_error(e) + + return assay_plate_resource_id + def state_handler(self) -> None: """Periodically check state of BMG device""" @@ -205,9 +217,12 @@ def run_assay( Optional[str], "data output file name (ex. data.txt). Will default to .txt (ex. 1731706249.txt) if no file name is entered.", ] = None, - ) -> Annotated[Path, "Data .txt file returned by the BMG microplate reader"]: + ) -> Annotated[tuple[Path, str], "Returns (data file path, assay plate ID)"]: """Runs an assay on the BMG plate reader""" + # Collect the resource ID of the assay plate in the BMG reader, if any. None if no assay plate present according to resource manager. + assay_plate_id = self.collect_current_plate_resource() + # Collect and validate the data_directory_path if data_output_directory_path is None: data_output_directory_path = self.config.data_output_directory_path @@ -239,7 +254,9 @@ def run_assay( self.logger.log_error(f"Error running assay: {response['error']}") self.logger.log_error(f"response: {response}") return ActionFailed(errors=[f"Error running assay: {response['error']}"]) - return Path(response["data"]) + + # return the path to the data file and the associated labware id (or None) + return (Path(response["data"]), assay_plate_id) if __name__ == "__main__": From 5cc8778b226719aad1d0210040657587833da58d Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Fri, 12 Dec 2025 15:39:04 -0600 Subject: [PATCH 17/28] argument validation for temperature in interface and conversion to Path from str clrs --- src/bmg_interface.py | 25 ++++++++++++++----------- src/bmg_rest_node.py | 14 ++++---------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/bmg_interface.py b/src/bmg_interface.py index e0f2aaf..8c83bb5 100644 --- a/src/bmg_interface.py +++ b/src/bmg_interface.py @@ -1,5 +1,5 @@ """ -Driver for the BMG microplate reader (our model is VANTAstar) +Python Driver for the BMG Microplate Reader (our model is BMG VANTAstar) """ import ctypes @@ -31,14 +31,14 @@ def __init__( self.open_connection() def open_connection(self) -> None: - """Open a connection to the BMG plate reader""" + """Opens a connection to the BMG plate reader""" ep = ctypes.c_char_p(self.control_name.encode("ascii")) res = self.com.OpenConnection(ep) if res: raise Exception(f"OpenConnection failed: {res}") def close_connection(self) -> None: - """Close the connection to the BMG plate reader""" + """Closes the connection to the BMG plate reader""" res = self.com.CloseConnection() if res: raise Exception(f"CloseConnection failed: {res}") @@ -84,13 +84,17 @@ def set_temp(self, temp: float) -> None: 00.0 = The incubator unit will be switched off. 00.1 = Temperature will not be controlled, but will be monitored. 25.0 - 45.0 = Incubator will be switched on and new temp value will be set. Can be changed in increments of 0.1 deg C - 10.0 - 60.0 = This range is ONLY allowed on extended range models. + 10.0 - 60.0 = This range is ONLY allowed on extended range models. Notes: - Will throw error code -20 if temp input is not a valid value. - Temp must be a float to be valid. - If more than one decimal point are included, will round to nearest valid temp input. """ + # Check that temperature input is valid (Outer range checked. Valid temp range varies by device model.). # TODO: TEST! + if not 10.0 <= temp <= 60.0 or temp == 0.0 or temp == 0.1: + raise ValueError("Temp argument must be a valid float between 10.0 and 60.0, or equal to 0.0 or 0.1") + nominal_temp = str(temp) self.exec("Temp", nominal_temp) @@ -103,7 +107,6 @@ def read_temps(self) -> dict: "Temp2": (float temperature reading from top heating plate) "Temp3:" (float temperature reading from optic slide heating plate) } - """ temps = {} temp1_formatted = ctypes.c_char_p(b"Temp1") @@ -153,7 +156,6 @@ def run_assay( Returns: data_file_path (str): Path to resulting data file. """ - # Give the data file a unique name if no name is specified if not data_output_file_name: data_output_file_name = str(int(time.time())) + ".txt" @@ -162,7 +164,7 @@ def run_assay( data_dir = Path(data_output_directory_path) data_file_path = data_dir / data_output_file_name - self.exec( + response = self.exec( "Run", assay_name, protocol_database_path, @@ -173,7 +175,8 @@ def run_assay( data_output_directory_path, data_output_file_name, ) - + self.logger.log_info(f"Run action response: {response}") + return data_file_path def is_busy(self) -> bool: @@ -183,9 +186,9 @@ def is_busy(self) -> bool: def exec(self, cmd: str, *args: Any) -> None: """Executed a command over the established connection with the BMG plate reader""" args = (cmd, *args) - res = self.com.ExecuteAndWait(args) - self.logger.log_info(f"exec response: {res}") - return res + response = self.com.ExecuteAndWait(args) + self.logger.log_info(f"exec response: {response}") + return response if __name__ == "__main__": diff --git a/src/bmg_rest_node.py b/src/bmg_rest_node.py index f76525c..ddc17ed 100644 --- a/src/bmg_rest_node.py +++ b/src/bmg_rest_node.py @@ -14,23 +14,16 @@ from bmg_interface import BmgCom from bmg_object_thread import BMGThread -""" -TODOs: - -- Something is wrong with passing paths in through the command line args. Only works when default paths are set in BMGNodeConfig - TASK: make these into Path types, not string and test. Probably the double slash when passing in string is the issue - -""" - class BMGNodeConfig(RestNodeConfig): """Configuration for the BMG node.""" - data_output_directory_path: str = ( + # TODO: TEST CONVERSION TO PATH TYPE HERE! + data_output_directory_path: Path = Path( "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" ) """Data output directory path for bmg data""" - db_directory_path: str = "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" + db_directory_path: Path = Path("C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit") """Path to directory where assay protocol files are stored""" state_update_interval: Optional[float] = 5.0 """Interval for updating module state in seconds""" @@ -191,6 +184,7 @@ def set_temp(self, temp: float) -> None: temp = float(temp) if temp in {0.0, 0.1} or 25.0 <= temp <= 45.0: # Temp input is valid, send the command + # NOTE: These temp values are specific to our model of BMG Plate Reader (VANTAStar) response = self.bmg_thread.send_command( {"action": "set_temp", "temp": temp} ) From 81f42bcaec9f576e22c40aac590faac3ee9b3244 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Fri, 12 Dec 2025 15:52:10 -0600 Subject: [PATCH 18/28] Paths tested, passing pre-commit checks --- src/bmg_interface.py | 12 +++++++----- src/bmg_rest_node.py | 5 +++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/bmg_interface.py b/src/bmg_interface.py index 8c83bb5..168f930 100644 --- a/src/bmg_interface.py +++ b/src/bmg_interface.py @@ -84,7 +84,7 @@ def set_temp(self, temp: float) -> None: 00.0 = The incubator unit will be switched off. 00.1 = Temperature will not be controlled, but will be monitored. 25.0 - 45.0 = Incubator will be switched on and new temp value will be set. Can be changed in increments of 0.1 deg C - 10.0 - 60.0 = This range is ONLY allowed on extended range models. + 10.0 - 60.0 = This range is ONLY allowed on extended range models. Notes: - Will throw error code -20 if temp input is not a valid value. @@ -92,9 +92,11 @@ def set_temp(self, temp: float) -> None: - If more than one decimal point are included, will round to nearest valid temp input. """ # Check that temperature input is valid (Outer range checked. Valid temp range varies by device model.). # TODO: TEST! - if not 10.0 <= temp <= 60.0 or temp == 0.0 or temp == 0.1: - raise ValueError("Temp argument must be a valid float between 10.0 and 60.0, or equal to 0.0 or 0.1") - + if not 10.0 <= temp <= 60.0 or temp in [0.0, 0.1]: + raise ValueError( + "Temp argument must be a valid float between 10.0 and 60.0, or equal to 0.0 or 0.1" + ) + nominal_temp = str(temp) self.exec("Temp", nominal_temp) @@ -176,7 +178,7 @@ def run_assay( data_output_file_name, ) self.logger.log_info(f"Run action response: {response}") - + return data_file_path def is_busy(self) -> bool: diff --git a/src/bmg_rest_node.py b/src/bmg_rest_node.py index ddc17ed..c28a310 100644 --- a/src/bmg_rest_node.py +++ b/src/bmg_rest_node.py @@ -18,12 +18,13 @@ class BMGNodeConfig(RestNodeConfig): """Configuration for the BMG node.""" - # TODO: TEST CONVERSION TO PATH TYPE HERE! data_output_directory_path: Path = Path( "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" ) """Data output directory path for bmg data""" - db_directory_path: Path = Path("C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit") + db_directory_path: Path = Path( + "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" + ) """Path to directory where assay protocol files are stored""" state_update_interval: Optional[float] = 5.0 """Interval for updating module state in seconds""" From 2a1aa62bc3e8b0b15d1f87bc3c7af0e8df17bb15 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Mon, 15 Dec 2025 12:36:41 -0600 Subject: [PATCH 19/28] cleanup --- README.md | 6 +++--- src/bmg_interface.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 505cfae..6be9b8a 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,11 @@ If you're having trouble installing the requirements or MADSci due to an issue i Inside the bmg_module directory, run the following commands to test connection to the BMG device through the BMG interface. cd src - python bmg_driver.py + python bmg_interface.py This will print out the current BMG LABTECH Remote Control Version Number if the driver is able to connect correctly to the BMG device. -You can also use the driver in other programs. The example python program below uses the bmg driver to open and close the plate tray, set the temperature, and run an assay named ASSAY_TEST. +You can also use the driver in other programs. The example python program below uses the bmg driver to open and close the plate tray, set the temperature, and run an assay named ASSAY_NAME. When instantiating the bmg_device, the model name must be entered as "CLARIOstar" even if you own a BMG VANTAstar device. Also, be sure to replace the protocol_database_path and data_output_directory values with your correct paths. @@ -63,7 +63,7 @@ When instantiating the bmg_device, the model name must be entered as "CLARIOstar #### Running the REST Node -The REST Node can be started with a command in the format below +The REST Node can be started with a command in the format below. python bmg_rest_node.py --node_url <(str, optional) address for your LiCONiC MADSci REST Node> --db_directory_path <(str, optional) path to bmg db directory containing assay .TCS files> --output_path <(str, optional) path to directory for saving data output files> diff --git a/src/bmg_interface.py b/src/bmg_interface.py index 168f930..5f58a7a 100644 --- a/src/bmg_interface.py +++ b/src/bmg_interface.py @@ -91,7 +91,7 @@ def set_temp(self, temp: float) -> None: - Temp must be a float to be valid. - If more than one decimal point are included, will round to nearest valid temp input. """ - # Check that temperature input is valid (Outer range checked. Valid temp range varies by device model.). # TODO: TEST! + # Check that temperature input is valid (Outer range checked. Valid temp range varies by device model.) if not 10.0 <= temp <= 60.0 or temp in [0.0, 0.1]: raise ValueError( "Temp argument must be a valid float between 10.0 and 60.0, or equal to 0.0 or 0.1" From aa001c0bb3a5ea62e763825dc18f4af1b9b272cd Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Mon, 15 Dec 2025 14:06:56 -0600 Subject: [PATCH 20/28] README cleanup --- README.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6be9b8a..ea3b2d1 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@ -# BMG module +# BMG Module -A MADSci-powered module for controlling BMG Microplate Readers, currently tested with the VANTAstar model. +A MADSci-powered module for controlling BMG microplate readers, currently tested with the VANTAstar model. Contains a BMG driver (bmg_driver.py), BMG Object Thread Class (bmg_object_thread.py), and BMG REST Node (bmg_rest_node.py). -### Assay Setup on BMG microplate reader +### Assay Setup on a BMG Microplate Reader -To run an assay on the BMG plate reader, you first need to create the assay using the BMG SMART Control software (not the Voyager software). Once the assay is created and saved, it needs to be exported and the .TCS file should be placed inside the BMG directory that contains the assay database .db file ("C:\Program Files (x86)\BMG\CLARIOstar\User\Definit" with default BMG Smart Control on Windows). Once the .TCS file for your assay has been saved into the database directory, it can be accessed by our BMG driver and REST Node by assay name. +To run an assay on the BMG plate reader, you must first create the assay using the BMG SMART Control software (not the Voyager software). Once the assay is created and saved, it must be exported, and the .TCS file should be placed inside the BMG directory that contains the assay database .db file ("C:\Program Files (x86)\BMG\CLARIOstar\User\Definit" with default BMG SMART Control on Windows). -### Running instructions +Once the .TCS file for your assay has been saved into the database directory, it can be accessed by the BMG driver and REST Node by assay name. -The BMG driver and REST Node can only connect to the device if run with **32-bit python**. When creating your python virtual environment in the commands below, replace 'python.exe' with your path to a 32-bit python executable. +### Running Instructions + +The BMG driver and REST Node can only connect to the device when run with **32-bit Python**. When creating your Python virtual environment in the commands below, replace 'python.exe' with the path to a 32-bit Python executable. #### Installation @@ -19,12 +21,12 @@ Clone the repository: git clone https://github.com/AD-SDL/bmg_module.git cd bmg_module -Create a virtual environment with 32-bit python, then activate it. Remember to use your 32-bit python path: +Create a virtual environment with 32-bit Python, then activate it. Be sure to use your 32-bit Python path: python.exe -m venv .venv .venv\Scripts\activate -Install the dependencies using pdm or pip: +Install the dependencies using PDM or pip: pdm install @@ -32,18 +34,18 @@ or pip install -e . -If you're having trouble installing the requirements or MADSci due to an issue installing httptools, use the Visual Studio Installer (download this if you don't have it already), and either modify or install Visual Studio Community 2022 to include "Desktop development with C++". +If you're having trouble installing the requirements or MADSci due to an issue installing httptools, use the Visual Studio Installer (download it if you do not already have it), and either modify or install Visual Studio Community 2022 to include Desktop development with C++. #### Running the interface -Inside the bmg_module directory, run the following commands to test connection to the BMG device through the BMG interface. +Inside the bmg_module directory, run the following commands to test the connection to the BMG device through the BMG interface. cd src python bmg_interface.py -This will print out the current BMG LABTECH Remote Control Version Number if the driver is able to connect correctly to the BMG device. +This will print the current BMG LABTECH Remote Control version number if the driver is able to connect successfully to the BMG device. -You can also use the driver in other programs. The example python program below uses the bmg driver to open and close the plate tray, set the temperature, and run an assay named ASSAY_NAME. +You can also use the driver in other programs. The example Python program below uses the BMG driver to open and close the plate tray, set the temperature, and run an assay named ASSAY_NAME. When instantiating the bmg_device, the model name must be entered as "CLARIOstar" even if you own a BMG VANTAstar device. Also, be sure to replace the protocol_database_path and data_output_directory values with your correct paths. @@ -85,7 +87,7 @@ Example usage with all optional arguments: ### Example Usage in MADSci Workflow YAML file -Below is an example of a MADSci YAML Workflow file that could interact with the BMG REST Node. Replace "ASSAY_NAME" and "ASSAY_DATA.txt" with the name of the assay you wish to run on the BMG and file name for the output data. +Below is an example of a MADSci YAML workflow file that interacts with the BMG REST Node. Replace "ASSAY_NAME" and "ASSAY_DATA.txt" with the name of the assay you wish to run on the BMG and the desired output data file name. name: Test Workflow From 544720487eb8ed9cbfebcc7e322fd490c21663e6 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Tue, 16 Dec 2025 11:58:16 -0600 Subject: [PATCH 21/28] passing pre-commit checks --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ea3b2d1..9af9d40 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Contains a BMG driver (bmg_driver.py), BMG Object Thread Class (bmg_object_threa ### Assay Setup on a BMG Microplate Reader -To run an assay on the BMG plate reader, you must first create the assay using the BMG SMART Control software (not the Voyager software). Once the assay is created and saved, it must be exported, and the .TCS file should be placed inside the BMG directory that contains the assay database .db file ("C:\Program Files (x86)\BMG\CLARIOstar\User\Definit" with default BMG SMART Control on Windows). +To run an assay on the BMG plate reader, you must first create the assay using the BMG SMART Control software (not the Voyager software). Once the assay is created and saved, it must be exported, and the .TCS file should be placed inside the BMG directory that contains the assay database .db file ("C:\Program Files (x86)\BMG\CLARIOstar\User\Definit" with default BMG SMART Control on Windows). Once the .TCS file for your assay has been saved into the database directory, it can be accessed by the BMG driver and REST Node by assay name. From 5b47fdc6b57381c272b5967f700dc68f4238fe12 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Tue, 16 Dec 2025 12:14:21 -0600 Subject: [PATCH 22/28] cleanup --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9af9d40..a1dfdc3 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Once the .TCS file for your assay has been saved into the database directory, it ### Running Instructions -The BMG driver and REST Node can only connect to the device when run with **32-bit Python**. When creating your Python virtual environment in the commands below, replace 'python.exe' with the path to a 32-bit Python executable. +The BMG driver and REST Node can only connect to the device when run with **32-bit Python** on a Windows machine. When creating your Python virtual environment in the commands below, replace 'python.exe' with the path to a 32-bit Python executable. #### Installation @@ -36,7 +36,7 @@ or If you're having trouble installing the requirements or MADSci due to an issue installing httptools, use the Visual Studio Installer (download it if you do not already have it), and either modify or install Visual Studio Community 2022 to include Desktop development with C++. -#### Running the interface +#### Running the Interface Inside the bmg_module directory, run the following commands to test the connection to the BMG device through the BMG interface. @@ -71,7 +71,7 @@ The REST Node can be started with a command in the format below. --node_url will default to "http://127.0.0.1:2000" \ --db_directory_path will default to "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" \ -and -- output_path will default to "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" +and --output_path will default to "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" Example usage with no optional arguments: From 34450b7ef587844580dddf839041a3d616f70859 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Tue, 16 Dec 2025 12:22:07 -0600 Subject: [PATCH 23/28] fixing typo --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ded7925..fb7d995 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "bmg_module" version = "1.1.0" -description = "Software for automatting a BMG VANTAstar" +description = "Software for automating a BMG VANTAstar with MADSci" authors = [ {name = "Casey Stone", email="cstone@anl.gov"}, {name = "Ryan D. Lewis", email="ryan.lewis@anl.gov"}, From 1765ef0bcbff160b4981ee262b1adfc19698316c Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Wed, 17 Dec 2025 16:23:50 -0600 Subject: [PATCH 24/28] edits after PR review, testing needed --- .vscode/settings.json | 33 +++++++++++++++++++++++++++++++++ compose.yaml | 39 --------------------------------------- src/bmg_interface.py | 33 +++++++++++++++------------------ src/bmg_object_thread.py | 2 +- 4 files changed, 49 insertions(+), 58 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 compose.yaml diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7a50092 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,33 @@ +{ + "workbench.colorCustomizations": { + "activityBar.activeBackground": "#65c89b", + "activityBar.background": "#65c89b", + "activityBar.foreground": "#15202b", + "activityBar.inactiveForeground": "#15202b99", + "activityBarBadge.background": "#945bc4", + "activityBarBadge.foreground": "#e7e7e7", + "commandCenter.border": "#15202b99", + "sash.hoverBorder": "#65c89b", + "statusBar.background": "#42b883", + "statusBar.foreground": "#15202b", + "statusBarItem.hoverBackground": "#359268", + "statusBarItem.remoteBackground": "#42b883", + "statusBarItem.remoteForeground": "#15202b", + "titleBar.activeBackground": "#42b883", + "titleBar.activeForeground": "#15202b", + "titleBar.inactiveBackground": "#42b88399", + "titleBar.inactiveForeground": "#15202b99" + }, + "peacock.color": "#42b883", + "cSpell.customDictionaries": { + "custom-dictionary-workspace": { + "name": "custom-dictionary-workspace", + "path": "${workspaceFolder:bmg_module}/.cspell/custom-dictionary-workspace.txt", + "addWords": true, + "scope": "workspace" + } + }, + "cSpell.ignoreWords": [ + "microplate" + ] +} \ No newline at end of file diff --git a/compose.yaml b/compose.yaml deleted file mode 100644 index 621ce28..0000000 --- a/compose.yaml +++ /dev/null @@ -1,39 +0,0 @@ -name: bmg_module -services: - bmg_module: - container_name: bmg_module - image: ghcr.io/ad-sdl/bmg_module - build: - context: . - tags: - - ghcr.io/ad-sdl/bmg_module:latest - - ghcr.io/ad-sdl/bmg_module:dev - volumes: - - ./src:/home/app/bmg_module/src - - ./tests:/home/app/bmg_module/tests - command: python -m bmg_module --port 2000 - ports: - - 2000:2000 -##################### -# WEI Core Services # -##################### - bmg_wei_server: - image: ghcr.io/ad-sdl/wei - container_name: bmg_wei_server - ports: - - 8000:8000 - environment: - - PYTHONUNBUFFERED=1 # Fix weird bug with empty logging - - USER_ID=${USER_ID:-1000} - - GROUP_ID=${GROUP_ID:-1000} - volumes: - - ./workcells:/workcells - command: python3 -m wei.server --workcell /workcells/test.workcell.yaml - depends_on: - - bmg_wei_redis - bmg_wei_redis: - image: redis - container_name: bmg_wei_redis - ports: - - 6379:6379 - command: redis-server --save 60 1 --loglevel warning diff --git a/src/bmg_interface.py b/src/bmg_interface.py index 5f58a7a..d3cbf6d 100644 --- a/src/bmg_interface.py +++ b/src/bmg_interface.py @@ -27,8 +27,7 @@ def __init__( pythoncom.CoInitialize() self.com = comtypes.client.CreateObject("BMG_ActiveX.BMGRemoteControl") - if control_name: - self.open_connection() + self.open_connection() def open_connection(self) -> None: """Opens a connection to the BMG plate reader""" @@ -43,21 +42,17 @@ def close_connection(self) -> None: if res: raise Exception(f"CloseConnection failed: {res}") - def version(self) -> str: + def get_version(self) -> str: """Returns the BMG instrument version""" return self.com.GetVersion() - def dummy(self) -> None: - """Use this to test if a connection to a BMG plate reader is active""" - self.exec("Dummy") - - def status(self) -> str: + def get_status(self) -> str: """Returns the current status of the BMG plate reader""" item = ctypes.c_char_p(b"Status") status = self.com.GetInfo(item) return status.strip() if isinstance(status, str) else "unknown" - def error(self) -> str: + def get_error(self) -> str: """Returns any errors on the BMG plate reader""" item = ctypes.c_char_p(b"Error") status = self.com.GetInfo(item) @@ -65,15 +60,15 @@ def error(self) -> str: def init(self) -> None: """Initializes the BMG plate reader""" - self.exec("Init") + self._exec("Init") def plate_in(self) -> None: """Closes the plate tray on the BMG plate reader""" - self.exec("PlateIn") + self._exec("PlateIn") def plate_out(self) -> None: """Opens the plate tray on the BMG plate reader""" - self.exec("PlateOut") + self._exec("PlateOut") def set_temp(self, temp: float) -> None: """Sets the temperature on the BMG plate reader. @@ -87,7 +82,7 @@ def set_temp(self, temp: float) -> None: 10.0 - 60.0 = This range is ONLY allowed on extended range models. Notes: - - Will throw error code -20 if temp input is not a valid value. + - Will throw get_ code -20 if temp input is not a valid value. - Temp must be a float to be valid. - If more than one decimal point are included, will round to nearest valid temp input. """ @@ -98,7 +93,7 @@ def set_temp(self, temp: float) -> None: ) nominal_temp = str(temp) - self.exec("Temp", nominal_temp) + self._exec("Temp", nominal_temp) def read_temps(self) -> dict: """Reads the temperature at three locations in the BMG plate reader @@ -129,7 +124,9 @@ def read_temps(self) -> dict: "Temp3": temp3, } except Exception: - # Don't raise exception if temperature collection fails. + """Do not raise an exception if temperature collection fails. + Any running action should continue, regardless of whether this + temperature collection is successful.""" self.logger.log_error("Error collecting temperatures: {e}") return temps @@ -166,7 +163,7 @@ def run_assay( data_dir = Path(data_output_directory_path) data_file_path = data_dir / data_output_file_name - response = self.exec( + response = self._exec( "Run", assay_name, protocol_database_path, @@ -185,7 +182,7 @@ def is_busy(self) -> bool: """Returns True if BMG is busy, False if not busy""" return bool(self.lock.locked()) - def exec(self, cmd: str, *args: Any) -> None: + def _exec(self, cmd: str, *args: Any) -> None: """Executed a command over the established connection with the BMG plate reader""" args = (cmd, *args) response = self.com.ExecuteAndWait(args) @@ -195,4 +192,4 @@ def exec(self, cmd: str, *args: Any) -> None: if __name__ == "__main__": com = BmgCom("CLARIOstar") - print(f"BMG LABTECH Remote Control Version: {com.version()}") # noqa: T201 + print(f"BMG LABTECH Remote Control Version: {com.get_version()}") # noqa: T201 diff --git a/src/bmg_object_thread.py b/src/bmg_object_thread.py index 7cc7466..c7723f4 100644 --- a/src/bmg_object_thread.py +++ b/src/bmg_object_thread.py @@ -110,7 +110,7 @@ def _handle_read_temps(self, result: dict) -> None: def _handle_device_state(self, result: dict) -> None: """Handle device_state command.""" - device_state = self.bmg.status() + device_state = self.bmg.get_status() result["success"] = True result["data"] = device_state From e6d5c2fdba8badb6f3534ddeb2cce7c6ad16b1e1 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Thu, 18 Dec 2025 15:25:31 -0600 Subject: [PATCH 25/28] Edits after PR review. Switched to windows paths and added argument for extended temperature range device models --- .gitignore | 3 ++ .vscode/settings.json | 33 ------------ definitions/bmg_billy.info.yaml | 39 +++++++++------ src/bmg_interface.py | 48 +++++++++--------- src/bmg_object_thread.py | 27 +++++++--- src/bmg_rest_node.py | 89 +++++++++++++++++++++++---------- 6 files changed, 132 insertions(+), 107 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 82f9275..0aa89b7 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,6 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# VS Code +.vscode/ diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 7a50092..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "workbench.colorCustomizations": { - "activityBar.activeBackground": "#65c89b", - "activityBar.background": "#65c89b", - "activityBar.foreground": "#15202b", - "activityBar.inactiveForeground": "#15202b99", - "activityBarBadge.background": "#945bc4", - "activityBarBadge.foreground": "#e7e7e7", - "commandCenter.border": "#15202b99", - "sash.hoverBorder": "#65c89b", - "statusBar.background": "#42b883", - "statusBar.foreground": "#15202b", - "statusBarItem.hoverBackground": "#359268", - "statusBarItem.remoteBackground": "#42b883", - "statusBarItem.remoteForeground": "#15202b", - "titleBar.activeBackground": "#42b883", - "titleBar.activeForeground": "#15202b", - "titleBar.inactiveBackground": "#42b88399", - "titleBar.inactiveForeground": "#15202b99" - }, - "peacock.color": "#42b883", - "cSpell.customDictionaries": { - "custom-dictionary-workspace": { - "name": "custom-dictionary-workspace", - "path": "${workspaceFolder:bmg_module}/.cspell/custom-dictionary-workspace.txt", - "addWords": true, - "scope": "workspace" - } - }, - "cSpell.ignoreWords": [ - "microplate" - ] -} \ No newline at end of file diff --git a/definitions/bmg_billy.info.yaml b/definitions/bmg_billy.info.yaml index 334c916..c1941b2 100644 --- a/definitions/bmg_billy.info.yaml +++ b/definitions/bmg_billy.info.yaml @@ -26,7 +26,7 @@ node_url: null actions: open: name: open - description: Opens the BMG plate tray + description: Opens the BMG plate tray. args: {} locations: {} files: {} @@ -39,7 +39,7 @@ actions: var_kwargs_schema: null close: name: close - description: Closes the BMG plate tray + description: Closes the BMG plate tray. args: {} locations: {} files: {} @@ -52,11 +52,12 @@ actions: var_kwargs_schema: null set_temp: name: set_temp - description: Sets the temperature on the BMG microplate reader + description: Sets the temperature on the BMG microplate reader. args: temp: name: temp - description: '' + description: Temperature in Celcius. Valid options are 0.0, 0.1, or 25.0 through + 45.0 (10.0 through 60.0 for extended temperature range models). argument_type: float required: true default: null @@ -81,14 +82,14 @@ actions: default: null data_output_directory_path: name: data_output_directory_path - description: data output directory path. Path must point to an existing folder. + description: Data output directory path. Path must point to an existing folder. Defaults to 'C:\Program Files (x86)\BMG\CLARIOstar\User\Data' argument_type: str required: false default: null data_output_file_name: name: data_output_file_name - description: data output file name (ex. data.txt). Will default to .txt + description: Data output file name (ex. data.txt). Will default to .txt (ex. 1731706249.txt) if no file name is entered. argument_type: str required: false @@ -100,6 +101,19 @@ actions: result_label: file description: null result_type: file + json_result: + result_label: json_result + description: null + result_type: json + json_schema: + properties: + data: + title: Data + type: string + required: + - data + title: strModel + type: object blocking: false asynchronous: true accepts_var_args: false @@ -116,6 +130,7 @@ config: uvicorn_kwargs: {} data_output_directory_path: C:\Program Files (x86)\BMG\CLARIOstar\User\Data db_directory_path: C:\Program Files (x86)\BMG\CLARIOstar\User\Definit + extended_temperature_range_model: false config_schema: description: Configuration for the BMG node. properties: @@ -173,13 +188,9 @@ config_schema: description: Configuration for the Uvicorn server that runs the REST API. title: Uvicorn Configuration type: object - data_output_directory_path: - default: C:\Program Files (x86)\BMG\CLARIOstar\User\Data - title: Data Output Directory Path - type: string - db_directory_path: - default: C:\Program Files (x86)\BMG\CLARIOstar\User\Definit - title: Db Directory Path - type: string + extended_temperature_range_model: + default: false + title: Extended Temperature Range Model + type: boolean title: BMGNodeConfig type: object diff --git a/src/bmg_interface.py b/src/bmg_interface.py index d3cbf6d..7989497 100644 --- a/src/bmg_interface.py +++ b/src/bmg_interface.py @@ -18,11 +18,13 @@ class BmgCom: def __init__( self, control_name: str, + extended_temperature_range_model: bool, logger: EventClient = None, ) -> None: """Initializes and opens the connection the BMG plate reader""" self.control_name = control_name + self.extended_temperature_range_model = extended_temperature_range_model self.logger = logger or EventClient() pythoncom.CoInitialize() @@ -86,12 +88,16 @@ def set_temp(self, temp: float) -> None: - Temp must be a float to be valid. - If more than one decimal point are included, will round to nearest valid temp input. """ - # Check that temperature input is valid (Outer range checked. Valid temp range varies by device model.) - if not 10.0 <= temp <= 60.0 or temp in [0.0, 0.1]: + # Check that temperature input is valid. Valid temp range varies by device model. + min_temp, max_temp = ( + (10.0, 60.0) if self.extended_temperature_range_model else (25.0, 45.0) + ) + if not min_temp <= temp <= max_temp and temp not in [0.0, 0.1]: raise ValueError( - "Temp argument must be a valid float between 10.0 and 60.0, or equal to 0.0 or 0.1" + f"Temp argument must be a valid float between {min_temp} and {max_temp}, or equal to 0.0 or 0.1" ) + # Format and execute action nominal_temp = str(temp) self._exec("Temp", nominal_temp) @@ -99,13 +105,12 @@ def read_temps(self) -> dict: """Reads the temperature at three locations in the BMG plate reader Returns: a dictionary of temperature readouts. - temps = { + { "Temp1": (float temperature reading from bottom heating plate) "Temp2": (float temperature reading from top heating plate) "Temp3:" (float temperature reading from optic slide heating plate) } """ - temps = {} temp1_formatted = ctypes.c_char_p(b"Temp1") temp1 = self.com.GetInfo(temp1_formatted) temp2_formatted = ctypes.c_char_p(b"Temp2") @@ -113,23 +118,15 @@ def read_temps(self) -> dict: temp3_formatted = ctypes.c_char_p(b"Temp3") temp3 = self.com.GetInfo(temp3_formatted) - try: - # Convert to floats in Celsius - temp1 = float(temp1) / 10 - temp2 = float(temp2) / 10 - temp3 = float(temp3) / 10 - temps = { - "Temp1": temp1, - "Temp2": temp2, - "Temp3": temp3, - } - except Exception: - """Do not raise an exception if temperature collection fails. - Any running action should continue, regardless of whether this - temperature collection is successful.""" - self.logger.log_error("Error collecting temperatures: {e}") - - return temps + # Convert to floats in Celsius + temp1 = float(temp1) / 10 + temp2 = float(temp2) / 10 + temp3 = float(temp3) / 10 + return { + "Temp1": temp1, + "Temp2": temp2, + "Temp3": temp3, + } def run_assay( self, @@ -163,15 +160,16 @@ def run_assay( data_dir = Path(data_output_directory_path) data_file_path = data_dir / data_output_file_name + # Execute run assay command response = self._exec( "Run", assay_name, - protocol_database_path, - data_output_directory_path, + str(protocol_database_path), + str(data_output_directory_path), plate_id1, plate_id2, plate_id3, - data_output_directory_path, + str(data_output_directory_path), data_output_file_name, ) self.logger.log_info(f"Run action response: {response}") diff --git a/src/bmg_object_thread.py b/src/bmg_object_thread.py index c7723f4..a2aa564 100644 --- a/src/bmg_object_thread.py +++ b/src/bmg_object_thread.py @@ -13,11 +13,16 @@ class BMGThread(threading.Thread): """Dedicated thread for BMG communication.""" - def __init__(self, logger: EventClient = None) -> None: + def __init__( + self, + extended_temperature_range_model: bool, + logger: EventClient = None, + ) -> None: """Initializes the BMGThread object""" super().__init__(daemon=True) self.logger = logger or EventClient() + self.extended_temperature_range_model = extended_temperature_range_model self.lock = threading.Lock() self.command_queue = queue.Queue() @@ -32,6 +37,7 @@ def run(self) -> None: # Initialize BMG communication self.bmg = BmgCom( "CLARIOstar", + extended_temperature_range_model=self.extended_temperature_range_model, logger=self.logger, ) self.logger.log_info("BMG communication thread started.") @@ -105,8 +111,12 @@ def _handle_set_temp(self, command: dict, result: dict) -> None: def _handle_read_temps(self, result: dict) -> None: """Handle read_temps command.""" temps = self.bmg.read_temps() - result["success"] = True - result["data"] = temps + if temps: + result["success"] = True + result["data"] = temps + else: + result["success"] = False + result["error"] = "Unable to read temperatures from BMG device." def _handle_device_state(self, result: dict) -> None: """Handle device_state command.""" @@ -116,6 +126,7 @@ def _handle_device_state(self, result: dict) -> None: def _handle_run_assay(self, command: dict, result: dict) -> None: """Handle run_assay command.""" + data_filename = None data_filename = self.bmg.run_assay( assay_name=command.get("protocol_name"), protocol_database_path=command.get("protocol_database_path"), @@ -123,16 +134,16 @@ def _handle_run_assay(self, command: dict, result: dict) -> None: data_output_file_name=command.get("data_output_file_name"), ) if data_filename: + result["success"] = True result["data"] = data_filename else: + result["success"] = False + result["error"] = ( + f"No data_filename returned from _handle_run_assay in bmg_object thread. {data_filename=}" + ) self.logger.log_error( "No data_filename returned from _handle_run_assay in bmg_object thread" ) - self.logger.log_error(f"{data_filename=}") - - # Don't fail the action if no data is returned. - # Presumably the data is still backed up on local machine. - result["success"] = True def send_command(self, command: dict, timeout: float = 300.0) -> dict: """Send a command to the BMG thread and wait for a response. diff --git a/src/bmg_rest_node.py b/src/bmg_rest_node.py index c28a310..235aab5 100644 --- a/src/bmg_rest_node.py +++ b/src/bmg_rest_node.py @@ -2,7 +2,7 @@ REST-based node for BMG microplate readers that interfaces with WEI """ -from pathlib import Path +from pathlib import Path, WindowsPath from typing import Annotated, Optional from madsci.common.types.action_types import ActionFailed @@ -10,6 +10,7 @@ from madsci.common.types.resource_types import Slot from madsci.node_module.helpers import action from madsci.node_module.rest_node_module import RestNode +from pydantic.json_schema import SkipJsonSchema from bmg_interface import BmgCom from bmg_object_thread import BMGThread @@ -18,16 +19,17 @@ class BMGNodeConfig(RestNodeConfig): """Configuration for the BMG node.""" - data_output_directory_path: Path = Path( - "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" + data_output_directory_path: SkipJsonSchema[WindowsPath] = WindowsPath( + "C:/Program Files (x86)/BMG/CLARIOstar/User/Data" ) """Data output directory path for bmg data""" - db_directory_path: Path = Path( - "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" + db_directory_path: SkipJsonSchema[WindowsPath] = WindowsPath( + "C:/Program Files (x86)/BMG/CLARIOstar/User/Definit" ) - """Path to directory where assay protocol files are stored""" state_update_interval: Optional[float] = 5.0 """Interval for updating module state in seconds""" + extended_temperature_range_model: bool = False + """True if your device allows an extended temperature range (10 deg C to 60 deg C)""" class BMGNode(RestNode): @@ -55,6 +57,7 @@ def startup_handler(self) -> None: # Start the BMG thread after resources are initialized self.bmg_thread = BMGThread( + extended_temperature_range_model=self.config.extended_temperature_range_model, logger=self.logger, ) self.bmg_thread.start() @@ -120,8 +123,17 @@ def state_handler(self) -> None: self.cached_device_state = device_state["data"] except Exception as e: + """Do nothing except log the error if collecting device state in the + state handler doesn't work. We want any running actions to continue or + for the device to remain ready to receive the next action, regardless + of our ability to collect this device state data.""" self.logger.log_error(f"Error collecting device state: {e}") + """Log if the device returns that it is in an Error state. + This doesn't stop """ + if self.cached_device_state == "Error": + self.logger.log_warning("Device is in an Error state.") + try: # Collect temperature readings response = self.bmg_thread.send_command({"action": "read_temps"}) @@ -138,8 +150,11 @@ def state_handler(self) -> None: "bmg_device_state": self.cached_device_state, } except Exception as e: - # Do nothing except log the error if state handler doesn't work - self.logger.log_warning(f"Error in state handler: {e}") + """Do nothing except log the error if collecting temperatures in the + state handler doesn't work. We want any running actions to continue or + for the device to remain ready to receive the next action, regardless + of our ability to collect this temperature data.""" + self.logger.log_error(f"Error collecting device temperatures: {e}") def shutdown_handler(self) -> None: """Called to clean up resources before the node is shut down.""" @@ -179,13 +194,24 @@ def close(self) -> None: self.logger.log_info("BMG plate tray closed.") @action(name="set_temp") - def set_temp(self, temp: float) -> None: + def set_temp( + self, + temp: Annotated[ + float, + "Temperature in Celcius. Valid options are 0.0, 0.1, or 25.0 through 45.0 (10.0 through 60.0 for extended temperature range models).", + ], + ) -> None: """Sets the temperature on the BMG microplate reader.""" temp = float(temp) - if temp in {0.0, 0.1} or 25.0 <= temp <= 45.0: + min_temp, max_temp = ( + (10.0, 60.0) + if self.config.extended_temperature_range_model + else (25.0, 45.0) + ) + + if temp in {0.0, 0.1} or min_temp <= temp <= max_temp: # Temp input is valid, send the command - # NOTE: These temp values are specific to our model of BMG Plate Reader (VANTAStar) response = self.bmg_thread.send_command( {"action": "set_temp", "temp": temp} ) @@ -206,11 +232,11 @@ def run_assay( assay_name: str, data_output_directory_path: Annotated[ Optional[str], - "data output directory path. Path must point to an existing folder. Defaults to 'C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data'", + "Data output directory path. Path must point to an existing folder. Defaults to 'C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data'", ] = None, data_output_file_name: Annotated[ Optional[str], - "data output file name (ex. data.txt). Will default to .txt (ex. 1731706249.txt) if no file name is entered.", + "Data output file name (ex. data.txt). Will default to .txt (ex. 1731706249.txt) if no file name is entered.", ] = None, ) -> Annotated[tuple[Path, str], "Returns (data file path, assay plate ID)"]: """Runs an assay on the BMG plate reader""" @@ -224,7 +250,7 @@ def run_assay( else: # Check that the directory path exists try: - if not Path(data_output_directory_path).is_dir(): + if not WindowsPath(data_output_directory_path).is_dir(): return ActionFailed( f"data_output_directory_path {data_output_directory_path} is not an existing folder" ) @@ -233,24 +259,33 @@ def run_assay( return ActionFailed(f"data_directory_output_path is invalid: {e}") # Run the assay, collect response containing output data file name - response = self.bmg_thread.send_command( - { - "action": "run_assay", - "protocol_name": assay_name, - "protocol_database_path": self.config.db_directory_path, - "data_output_directory_path": data_output_directory_path, - "data_output_file_name": data_output_file_name, - } - ) + try: + response = self.bmg_thread.send_command( + { + "action": "run_assay", + "protocol_name": assay_name, + "protocol_database_path": str(self.config.db_directory_path), + "data_output_directory_path": str(data_output_directory_path), + "data_output_file_name": data_output_file_name, + } + ) + except Exception as e: + self.logger.log_error(f"Error running assay in REST Node. {e}") + return ActionFailed(errors=[f"Error running assay in REST Node. {e}"]) # Interpret response self.logger.log_debug(f"{response=}") if not response["success"]: - self.logger.log_error(f"Error running assay: {response['error']}") - self.logger.log_error(f"response: {response}") - return ActionFailed(errors=[f"Error running assay: {response['error']}"]) + self.logger.log_error( + f"Error running assay. response['success'] is False. response = {response}" + ) + return ActionFailed( + errors=[ + f"Error running assay. response['success'] is False. response = {response}" + ] + ) - # return the path to the data file and the associated labware id (or None) + # Return the path to the data file and the associated labware ID (or None) return (Path(response["data"]), assay_plate_id) From e01368b58d7fe10cdab54e024c6455f3641be524 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Thu, 18 Dec 2025 17:33:45 -0600 Subject: [PATCH 26/28] improved device error state reporting and updated README --- src/bmg_interface.py | 11 ++++-- src/bmg_object_thread.py | 21 +++++++++- src/bmg_rest_node.py | 84 +++++++++++++++++++++------------------- 3 files changed, 72 insertions(+), 44 deletions(-) diff --git a/src/bmg_interface.py b/src/bmg_interface.py index 7989497..38cb15d 100644 --- a/src/bmg_interface.py +++ b/src/bmg_interface.py @@ -18,7 +18,7 @@ class BmgCom: def __init__( self, control_name: str, - extended_temperature_range_model: bool, + extended_temperature_range_model: bool = False, logger: EventClient = None, ) -> None: """Initializes and opens the connection the BMG plate reader""" @@ -99,7 +99,10 @@ def set_temp(self, temp: float) -> None: # Format and execute action nominal_temp = str(temp) - self._exec("Temp", nominal_temp) + response = self._exec("Temp", nominal_temp) + if response != 0: + self.logger.log_error(f"Failed to set temperature. response = {response}") + raise Exception(f"Failed to set temperature. response = {response}") def read_temps(self) -> dict: """Reads the temperature at three locations in the BMG plate reader @@ -172,7 +175,9 @@ def run_assay( str(data_output_directory_path), data_output_file_name, ) - self.logger.log_info(f"Run action response: {response}") + if response != 0: + self.logger.log_error(f"Failed to run assay. response = {response}") + raise Exception(f"Failed to run assay. response = {response}") return data_file_path diff --git a/src/bmg_object_thread.py b/src/bmg_object_thread.py index a2aa564..d3d0246 100644 --- a/src/bmg_object_thread.py +++ b/src/bmg_object_thread.py @@ -89,6 +89,8 @@ def _process_command(self, command: dict) -> dict: self._handle_set_temp(command, result) elif action == "read_temps": self._handle_read_temps(result) + elif action == "read_error": + self._handle_read_error(result) elif action == "device_state": self._handle_device_state(result) elif action == "run_assay": @@ -105,8 +107,13 @@ def _process_command(self, command: dict) -> dict: def _handle_set_temp(self, command: dict, result: dict) -> None: """Handle set_temp command.""" temp = command.get("temp") - self.bmg.set_temp(temp=temp) - result["success"] = True + try: + self.bmg.set_temp(temp=temp) + result["success"] = True + except Exception as e: + result["success"] = False + result["error"] = e + raise e def _handle_read_temps(self, result: dict) -> None: """Handle read_temps command.""" @@ -118,6 +125,16 @@ def _handle_read_temps(self, result: dict) -> None: result["success"] = False result["error"] = "Unable to read temperatures from BMG device." + def _handle_read_error(self, result: dict) -> None: + """Handle read_temps command.""" + error = self.bmg.get_error() + if error: + result["success"] = True + result["data"] = error + else: + result["success"] = False + result["error"] = "Unable to read error message from BMG device." + def _handle_device_state(self, result: dict) -> None: """Handle device_state command.""" device_state = self.bmg.get_status() diff --git a/src/bmg_rest_node.py b/src/bmg_rest_node.py index 235aab5..c642f77 100644 --- a/src/bmg_rest_node.py +++ b/src/bmg_rest_node.py @@ -2,7 +2,7 @@ REST-based node for BMG microplate readers that interfaces with WEI """ -from pathlib import Path, WindowsPath +from pathlib import Path from typing import Annotated, Optional from madsci.common.types.action_types import ActionFailed @@ -10,7 +10,6 @@ from madsci.common.types.resource_types import Slot from madsci.node_module.helpers import action from madsci.node_module.rest_node_module import RestNode -from pydantic.json_schema import SkipJsonSchema from bmg_interface import BmgCom from bmg_object_thread import BMGThread @@ -19,13 +18,11 @@ class BMGNodeConfig(RestNodeConfig): """Configuration for the BMG node.""" - data_output_directory_path: SkipJsonSchema[WindowsPath] = WindowsPath( + data_output_directory_path: Path = Path( "C:/Program Files (x86)/BMG/CLARIOstar/User/Data" ) """Data output directory path for bmg data""" - db_directory_path: SkipJsonSchema[WindowsPath] = WindowsPath( - "C:/Program Files (x86)/BMG/CLARIOstar/User/Definit" - ) + db_directory_path: Path = Path("C:/Program Files (x86)/BMG/CLARIOstar/User/Definit") state_update_interval: Optional[float] = 5.0 """Interval for updating module state in seconds""" extended_temperature_range_model: bool = False @@ -48,6 +45,7 @@ def __init__(self) -> None: self.cached_temp2 = None self.cached_temp3 = None self.cached_device_state = None + self.cached_current_errors = None def startup_handler(self) -> None: """Called to (re)initialize the node. Should be used to open connections to devices or initialize any other resources.""" @@ -103,6 +101,7 @@ def state_handler(self) -> None: self.logger.log_error("BMG thread is not initialized") return + # If the BMG device is busy, report cached values. if self.bmg_thread.is_busy: self.node_state = { "Temp1 (bottom heating plate)": self.cached_temp1, @@ -110,18 +109,18 @@ def state_handler(self) -> None: "Temp3 (optic slide heating plate)": self.cached_temp3, "bmg_thread_state": "BUSY", "bmg_device_state": "busy", + "errors": self.cached_current_errors, } + # If the BMG device is not busy, query for new state values. else: - # Thread is not busy, query the device + # Collect device state try: - # Collect device state device_state = self.bmg_thread.send_command({"action": "device_state"}) if not device_state["success"]: self.cached_device_state = "unknown" else: self.cached_device_state = device_state["data"] - except Exception as e: """Do nothing except log the error if collecting device state in the state handler doesn't work. We want any running actions to continue or @@ -129,13 +128,25 @@ def state_handler(self) -> None: of our ability to collect this device state data.""" self.logger.log_error(f"Error collecting device state: {e}") - """Log if the device returns that it is in an Error state. - This doesn't stop """ + # Collect any error messages if device is in an error state if self.cached_device_state == "Error": self.logger.log_warning("Device is in an Error state.") + try: + self.cached_current_errors = self.bmg_thread.send_command( + {"action": "read_error"} + )["data"] + except Exception as e: + """Do nothing except log the error if the the device error messages cannot + be collected. Error messages on the device often do not prevent the device + from running the next action.""" + self.logger.log_warning( + f"Error collecting current device errors: {e}" + ) + else: + self.cached_current_errors = None + # Collect temperature readings try: - # Collect temperature readings response = self.bmg_thread.send_command({"action": "read_temps"}) temps = response["data"] self.cached_temp1 = temps["Temp1"] @@ -148,6 +159,7 @@ def state_handler(self) -> None: "Temp3 (optic slide heating plate)": self.cached_temp3, "bmg_thead_state": "READY", "bmg_device_state": self.cached_device_state, + "errors": self.cached_current_errors, } except Exception as e: """Do nothing except log the error if collecting temperatures in the @@ -212,19 +224,22 @@ def set_temp( if temp in {0.0, 0.1} or min_temp <= temp <= max_temp: # Temp input is valid, send the command - response = self.bmg_thread.send_command( - {"action": "set_temp", "temp": temp} - ) - - # Interpret response - if not response["success"]: - self.logger.log_error(f"Error setting temperature: {response['error']}") + try: + response = self.bmg_thread.send_command( + {"action": "set_temp", "temp": temp} + ) + # Interpret response + if response["success"]: + return None + return ActionFailed(errors=response["error"]) + except Exception as e: + self.logger.log_error(f"Exception raised from set_temp action: {e}") return ActionFailed( - errors=[f"Error setting temperature: {response['error']}"] + errors=f"Exception raised from set_temp action: {e}" ) - return None - # Temp input is not valid (fail action, don't put node in error state) - return ActionFailed(errors=["Invalid temperature input value."]) + else: + # Temp input is not valid (fail action, don't put node in error state) + return ActionFailed(errors=["Invalid temperature input value."]) @action(name="run_assay") def run_assay( @@ -250,7 +265,7 @@ def run_assay( else: # Check that the directory path exists try: - if not WindowsPath(data_output_directory_path).is_dir(): + if not Path(data_output_directory_path).is_dir(): return ActionFailed( f"data_output_directory_path {data_output_directory_path} is not an existing folder" ) @@ -269,25 +284,16 @@ def run_assay( "data_output_file_name": data_output_file_name, } ) + # Interpret response + if response["success"]: + return (Path(response["data"]), assay_plate_id) + self.logger.log_error(f"Error running assay. {response=}") + return ActionFailed(errors=[f"Error running assay. {response=}"]) + except Exception as e: self.logger.log_error(f"Error running assay in REST Node. {e}") return ActionFailed(errors=[f"Error running assay in REST Node. {e}"]) - # Interpret response - self.logger.log_debug(f"{response=}") - if not response["success"]: - self.logger.log_error( - f"Error running assay. response['success'] is False. response = {response}" - ) - return ActionFailed( - errors=[ - f"Error running assay. response['success'] is False. response = {response}" - ] - ) - - # Return the path to the data file and the associated labware ID (or None) - return (Path(response["data"]), assay_plate_id) - if __name__ == "__main__": bmg_node = BMGNode() From f6490f6372a86bbd5de88d0dc397bc3f7eb119ef Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Fri, 19 Dec 2025 13:55:16 -0600 Subject: [PATCH 27/28] edits to README and a doc string --- README.md | 22 +++++++++++++++------- src/bmg_object_thread.py | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a1dfdc3..bd003fc 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,16 @@ You can also use the driver in other programs. The example Python program below When instantiating the bmg_device, the model name must be entered as "CLARIOstar" even if you own a BMG VANTAstar device. Also, be sure to replace the protocol_database_path and data_output_directory values with your correct paths. +If you own a BMG device with extended temperature range (10.0 deg C to 60.0 deg C), you will need to specify this when instantiating your BMG device in the code below. + import bmg_interface - bmg_device = bmg_interface.BmgCom("CLARIOstar") + bmg_device = bmg_interface.BmgCom(control_name = "CLARIOstar") + # for extended temperature range models, use the instantiation below instead. + # bmg_device = bmg_interface.BmgCom( + # control_name = "CLARIOstar", + # extended_temperature_range_model = True, + # ) bmg_device.plate_out() bmg_device.plate_in() bmg_device.set_temp(30.0) @@ -67,11 +74,13 @@ When instantiating the bmg_device, the model name must be entered as "CLARIOstar The REST Node can be started with a command in the format below. - python bmg_rest_node.py --node_url <(str, optional) address for your LiCONiC MADSci REST Node> --db_directory_path <(str, optional) path to bmg db directory containing assay .TCS files> --output_path <(str, optional) path to directory for saving data output files> + python bmg_rest_node.py --node_url <(str, optional) address for your LiCONiC MADSci REST Node> --db_directory_path <(str, optional) path to bmg db directory containing assay .TCS files> --data_output_directory_path <(str, optional) path to directory for saving data output files> --extended_temperature_range_model <(bool, optional) True if your BMG device has an extended temperature range of 10.0 deg C to 60.0 deg C, False if your BMG device has a temperature range of 25.0 deg C to 45.0 deg C> --node_url will default to "http://127.0.0.1:2000" \ --db_directory_path will default to "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" \ -and --output_path will default to "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" +--data_output_directory_path will default to "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" \ +and --extended_temperature_range_model will default to False. + Example usage with no optional arguments: @@ -81,13 +90,11 @@ Example usage with no optional arguments: Example usage with all optional arguments: - - python bmg_rest_node.py --node_url "http://127.0.0.1:3003" --db_directory_path "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" --output_path "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" - + python bmg_rest_node.py --node_url "http://127.0.0.1:3003" --db_directory_path "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Definit" --data_output_directory_path "C:\\Program Files (x86)\\BMG\\CLARIOstar\\User\\Data" --extended_temperature_range_model False ### Example Usage in MADSci Workflow YAML file -Below is an example of a MADSci YAML workflow file that interacts with the BMG REST Node. Replace "ASSAY_NAME" and "ASSAY_DATA.txt" with the name of the assay you wish to run on the BMG and the desired output data file name. +Below is an example of a MADSci YAML workflow file that interacts with the BMG REST Node. Replace "ASSAY_NAME","ASSAY_DATA.txt", and "YOUR/DATA/OUTPUT/PATH/" with the name of the assay you wish to run on the BMG, the desired output data file name, and the path to the directory where the output data will be stored. name: Test Workflow @@ -117,3 +124,4 @@ Below is an example of a MADSci YAML workflow file that interacts with the BMG R args: assay_name: ASSAY_NAME data_output_file_name: ASSAY_DATA.txt + data_output_directory_path: YOUR/DATA/OUTPUT/PATH/ diff --git a/src/bmg_object_thread.py b/src/bmg_object_thread.py index d3d0246..64c1d63 100644 --- a/src/bmg_object_thread.py +++ b/src/bmg_object_thread.py @@ -126,7 +126,7 @@ def _handle_read_temps(self, result: dict) -> None: result["error"] = "Unable to read temperatures from BMG device." def _handle_read_error(self, result: dict) -> None: - """Handle read_temps command.""" + """Handle read_error command.""" error = self.bmg.get_error() if error: result["success"] = True From 49653f34e4ee623e0da5ada3809b2c4303edfc59 Mon Sep 17 00:00:00 2001 From: Casey Stone Date: Fri, 19 Dec 2025 16:54:55 -0600 Subject: [PATCH 28/28] upgrade to MADSci 0.6.0 --- definitions/bmg_billy.info.yaml | 76 ++++++++++++++++-- pdm.lock | 131 +++++++++++++++++++++++++++++--- pyproject.toml | 6 +- 3 files changed, 194 insertions(+), 19 deletions(-) diff --git a/definitions/bmg_billy.info.yaml b/definitions/bmg_billy.info.yaml index c1941b2..fca1667 100644 --- a/definitions/bmg_billy.info.yaml +++ b/definitions/bmg_billy.info.yaml @@ -31,7 +31,7 @@ actions: locations: {} files: {} results: {} - blocking: false + blocking: true asynchronous: true accepts_var_args: false accepts_var_kwargs: false @@ -44,7 +44,7 @@ actions: locations: {} files: {} results: {} - blocking: false + blocking: true asynchronous: true accepts_var_args: false accepts_var_kwargs: false @@ -64,7 +64,7 @@ actions: locations: {} files: {} results: {} - blocking: false + blocking: true asynchronous: true accepts_var_args: false accepts_var_kwargs: false @@ -114,7 +114,7 @@ actions: - data title: strModel type: object - blocking: false + blocking: true asynchronous: true accepts_var_args: false accepts_var_kwargs: false @@ -127,7 +127,14 @@ config: status_update_interval: 2.0 state_update_interval: 5.0 node_url: http://suestorm.cels.anl.gov:3003/ - uvicorn_kwargs: {} + uvicorn_kwargs: + limit_concurrency: 10 + enable_rate_limiting: true + rate_limit_requests: 100 + rate_limit_window: 60 + rate_limit_short_requests: 50 + rate_limit_short_window: 1 + rate_limit_cleanup_interval: 300 data_output_directory_path: C:\Program Files (x86)\BMG\CLARIOstar\User\Data db_directory_path: C:\Program Files (x86)\BMG\CLARIOstar\User\Definit extended_temperature_range_model: false @@ -185,9 +192,66 @@ config_schema: type: string uvicorn_kwargs: additionalProperties: true - description: Configuration for the Uvicorn server that runs the REST API. + description: Configuration for the Uvicorn server that runs the REST API. By + default, sets limit_concurrency=10 to protect against connection exhaustion + attacks. title: Uvicorn Configuration type: object + enable_rate_limiting: + default: true + description: Enable rate limiting middleware for the REST API. + title: Enable Rate Limiting + type: boolean + rate_limit_requests: + default: 100 + description: Maximum number of requests allowed per long time window (only used + if enable_rate_limiting is True). + minimum: 1 + title: Rate Limit Requests + type: integer + rate_limit_window: + default: 60 + description: Long time window in seconds for rate limiting (only used if enable_rate_limiting + is True). + minimum: 1 + title: Rate Limit Window + type: integer + rate_limit_short_requests: + anyOf: + - minimum: 1 + type: integer + - type: 'null' + default: 50 + description: Maximum number of requests allowed per short time window for burst + protection (only used if enable_rate_limiting is True). If None, short window + limiting is disabled. + title: Rate Limit Short Requests + rate_limit_short_window: + anyOf: + - minimum: 1 + type: integer + - type: 'null' + default: 1 + description: Short time window for burst protection in seconds (only used if + enable_rate_limiting is True). If None, short window limiting is disabled. + title: Rate Limit Short Window + rate_limit_cleanup_interval: + default: 300 + description: Interval in seconds between cleanup operations to prevent memory + leaks (only used if enable_rate_limiting is True). + minimum: 1 + title: Rate Limit Cleanup Interval + type: integer + data_output_directory_path: + default: C:\Program Files (x86)\BMG\CLARIOstar\User\Data + format: path + title: Data Output Directory Path + type: string + db_directory_path: + default: C:\Program Files (x86)\BMG\CLARIOstar\User\Definit + format: path + title: Db Directory Path + type: string extended_temperature_range_model: default: false title: Extended Temperature Range Model diff --git a/pdm.lock b/pdm.lock index f447845..5072fe3 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:f2c2ee26c4d212a0e29602f5b292c757875e10c20fdd6bee8a2adb533f152e8d" +content_hash = "sha256:e0b1db4689604acf4eeb2b8b583f36aa93158579c8349077a241bed7f549cd65" [[metadata.targets]] requires_python = ">=3.9.1" @@ -554,6 +554,21 @@ files = [ {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] +[[package]] +name = "httpcore" +version = "1.0.9" +requires_python = ">=3.8" +summary = "A minimal low-level HTTP client." +groups = ["default"] +dependencies = [ + "certifi", + "h11>=0.16", +] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + [[package]] name = "httptools" version = "0.7.1" @@ -606,6 +621,23 @@ files = [ {file = "httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9"}, ] +[[package]] +name = "httpx" +version = "0.28.1" +requires_python = ">=3.8" +summary = "The next generation HTTP client." +groups = ["default"] +dependencies = [ + "anyio", + "certifi", + "httpcore==1.*", + "idna", +] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + [[package]] name = "identify" version = "2.6.15" @@ -655,7 +687,7 @@ files = [ [[package]] name = "madsci-client" -version = "0.5.4" +version = "0.6.0" requires_python = ">=3.9.1" summary = "The Modular Autonomous Discovery for Science (MADSci) Python Clients." groups = ["default"] @@ -666,13 +698,13 @@ dependencies = [ "trogon>=0.6.0", ] files = [ - {file = "madsci_client-0.5.4-py3-none-any.whl", hash = "sha256:da0811d75f8d8a402c1f1a05cae6bcffdba979d0f988447555c42cf90b1ce6b3"}, - {file = "madsci_client-0.5.4.tar.gz", hash = "sha256:64754d904ba2a3ee5e256d2c8fcb9c7eab253a0e9289c67c1325357779cc309f"}, + {file = "madsci_client-0.6.0-py3-none-any.whl", hash = "sha256:2ad18496f624eac047549d93d030aa37cf5c6f1527d1c2738599c3d0f22d7972"}, + {file = "madsci_client-0.6.0.tar.gz", hash = "sha256:a0e232b60a5daf3b90b0adc722a035b759b5739006a533c8ca6d7a6e9f230f1b"}, ] [[package]] name = "madsci-common" -version = "0.5.4" +version = "0.6.0" requires_python = ">=3.9.1" summary = "The Modular Autonomous Discovery for Science (MADSci) Common Definitions and Utilities." groups = ["default"] @@ -680,8 +712,11 @@ dependencies = [ "PyYAML>=6.0.2", "aenum>=3.1.15", "classy-fastapi>=0.7.0", + "click>=8.1.0", "fastapi>=0.115.4", + "httpx>=0.28.1", "multiprocess>=0.70.17", + "psycopg2-binary>=2.9.0", "pydantic-extra-types>=2.10.2", "pydantic-settings-export>=1.0.2", "pydantic-settings>=2.9.1", @@ -697,13 +732,13 @@ dependencies = [ "uvicorn[standard]>=0.32.0", ] files = [ - {file = "madsci_common-0.5.4-py3-none-any.whl", hash = "sha256:98ea6e0cf407a7fbc88767149f5e4f46cb384c8b7960530b7e3629114a1ae126"}, - {file = "madsci_common-0.5.4.tar.gz", hash = "sha256:a92cef1d94cf6871ed638ac369e2f005935ab0df6af28544412acb30a1090aa8"}, + {file = "madsci_common-0.6.0-py3-none-any.whl", hash = "sha256:8838e08aaa4fa8d863aa33b171189f97ebdfb72ebbd927e718112f518d542cf1"}, + {file = "madsci_common-0.6.0.tar.gz", hash = "sha256:48a7d6a11a45b085cc4057cf37cc0053baa95f5ea3a8875c5d104dbca3e4307b"}, ] [[package]] name = "madsci-node-module" -version = "0.5.4" +version = "0.6.0" requires_python = ">=3.9.1" summary = "The Modular Autonomous Discovery for Science (MADSci) Node Module Helper Classes." groups = ["default"] @@ -713,8 +748,8 @@ dependencies = [ "regex", ] files = [ - {file = "madsci_node_module-0.5.4-py3-none-any.whl", hash = "sha256:65bd8bb0184f11e5309e742ca4fa150b9a0f6a34d2e69f2b525478006870b180"}, - {file = "madsci_node_module-0.5.4.tar.gz", hash = "sha256:dfa68b2e55e4230d6259f228000c1f40fddf26527a2805069679d27f5d7f6ea7"}, + {file = "madsci_node_module-0.6.0-py3-none-any.whl", hash = "sha256:33901d72dfd347b3636dcc4804ef02ce3aa2059a16bbc9c68b6757592ae0833b"}, + {file = "madsci_node_module-0.6.0.tar.gz", hash = "sha256:b081d0454bfb713420d99b2d945dbe4b4b269b241849a0b04d1e42c377e2b33e"}, ] [[package]] @@ -879,6 +914,82 @@ files = [ {file = "pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16"}, ] +[[package]] +name = "psycopg2-binary" +version = "2.9.11" +requires_python = ">=3.9" +summary = "psycopg2 - Python-PostgreSQL Database Adapter" +groups = ["default"] +files = [ + {file = "psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e"}, + {file = "psycopg2_binary-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908"}, + {file = "psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d"}, + {file = "psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1"}, + {file = "psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d"}, + {file = "psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20e7fb94e20b03dcc783f76c0865f9da39559dcc0c28dd1a3fce0d01902a6b9c"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bdab48575b6f870f465b397c38f1b415520e9879fdf10a53ee4f49dcbdf8a21"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9d3a9edcfbe77a3ed4bc72836d466dfce4174beb79eda79ea155cc77237ed9e8"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:44fc5c2b8fa871ce7f0023f619f1349a0aa03a0857f2c96fbc01c657dcbbdb49"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c55460033867b4622cda1b6872edf445809535144152e5d14941ef591980edf"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2d11098a83cca92deaeaed3d58cfd150d49b3b06ee0d0852be466bf87596899e"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:691c807d94aecfbc76a14e1408847d59ff5b5906a04a23e12a89007672b9e819"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b81627b691f29c4c30a8f322546ad039c40c328373b11dff7490a3e1b517855"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:b637d6d941209e8d96a072d7977238eea128046effbf37d1d8b2c0764750017d"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:41360b01c140c2a03d346cec3280cf8a71aa07d94f3b1509fa0161c366af66b4"}, + {file = "psycopg2_binary-2.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:875039274f8a2361e5207857899706da840768e2a775bf8c65e82f60b197df02"}, +] + [[package]] name = "pycparser" version = "2.23" diff --git a/pyproject.toml b/pyproject.toml index fb7d995..01b05cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,9 +8,9 @@ authors = [ ] dependencies = [ - "madsci.node_module~=0.5.0", - "madsci.client~=0.5.0", - "madsci.common~=0.5.0", + "madsci.node_module~=0.6.0", + "madsci.client~=0.6.0", + "madsci.common~=0.6.0", "pywin32", "comtypes", ]