From 2a8705ef11205a06bbadd70971951ad5be6af219 Mon Sep 17 00:00:00 2001 From: Mike Arpaia Date: Wed, 23 Sep 2026 21:22:35 -0600 Subject: [PATCH 1/5] Initial introduction of protocols --- .github/workflows/ci.yml | 29 + README.md | 6 +- docs/development.rst | 8 + docs/index.rst | 1 + docs/installation.rst | 17 +- docs/protocols.rst | 148 + pyproject.toml | 7 +- setup.py | 7 +- src/buildcompiler/__init__.py | 5 + .../adapters/native_protocols.py | 55 + .../adapters/opentrons/simulation.py | 39 +- .../adapters/protocol_requests.py | 42 + src/buildcompiler/api/__init__.py | 8 + src/buildcompiler/api/protocols.py | 94 + src/buildcompiler/api/serialization.py | 1 + src/buildcompiler/buildcompiler.py | 74 +- src/buildcompiler/domain/build_result.py | 3 + src/buildcompiler/domain/protocol_requests.py | 90 + src/buildcompiler/protocols/__init__.py | 51 + .../protocols/allocation/__init__.py | 1 + .../protocols/allocation/models.py | 123 + .../protocols/allocation/plating.py | 30 + .../protocols/allocation/wells.py | 39 + src/buildcompiler/protocols/artifacts.py | 43 + src/buildcompiler/protocols/attribution.py | 26 + .../protocols/backends/__init__.py | 1 + .../protocols/backends/markdown.py | 104 + .../protocols/backends/opentrons/__init__.py | 1 + .../backends/opentrons/_simulation_worker.py | 133 + .../backends/opentrons/allocation.py | 55 + .../protocols/backends/opentrons/lowering.py | 110 + .../backends/opentrons/operations.py | 231 ++ .../protocols/backends/opentrons/palette.py | 29 + .../protocols/backends/opentrons/plating.py | 191 ++ .../protocols/backends/opentrons/profile.py | 131 + .../protocols/backends/opentrons/program.py | 40 + .../protocols/backends/opentrons/rendering.py | 120 + .../protocols/backends/opentrons/setup.py | 71 + .../backends/opentrons/simulation.py | 80 + .../protocols/backends/opentrons/tips.py | 76 + .../backends/opentrons/transformation.py | 220 ++ src/buildcompiler/protocols/compiler.py | 190 ++ src/buildcompiler/protocols/config.py | 7 + src/buildcompiler/protocols/handoffs.py | 82 + src/buildcompiler/protocols/inputs.py | 87 + src/buildcompiler/protocols/materials.py | 40 + .../protocols/methods/__init__.py | 1 + .../protocols/methods/assembly.py | 171 + .../protocols/methods/assembly_config.py | 92 + .../protocols/methods/plating.py | 169 + .../protocols/methods/plating_config.py | 62 + .../protocols/methods/transformation.py | 254 ++ .../methods/transformation_config.py | 56 + src/buildcompiler/protocols/plans.py | 43 + src/buildcompiler/protocols/steps.py | 129 + src/buildcompiler/protocols/validation.py | 27 + src/buildcompiler/robotutils.py | 22 +- src/buildcompiler/stages/assembly_lvl1.py | 12 + src/buildcompiler/stages/assembly_lvl2.py | 12 + src/buildcompiler/stages/transformation.py | 8 + tests/automation/README.md | 19 +- tests/automation/conftest.py | 31 + tests/automation/pudu_reference.json | 11 + tests/automation/test_assembly_equivalence.py | 144 + .../test_opentrons_simulation_optional.py | 7 - tests/automation/test_plating_equivalence.py | 93 + .../test_transformation_equivalence.py | 167 + tests/automation/test_workflow_equivalence.py | 130 + tests/test_plating.py | 3 +- .../opentrons/test_simulation_boundary.py | 43 +- tests/unit/adapters/test_native_protocols.py | 30 + tests/unit/protocols/test_compilation.py | 221 ++ .../protocols/test_transformation_plating.py | 225 ++ tests/unit/stages/test_assembly_lvl1.py | 7 + tests/unit/stages/test_assembly_lvl2.py | 7 + tests/unit/stages/test_transformation.py | 12 +- uv.lock | 3030 ++++++++--------- 77 files changed, 6518 insertions(+), 1666 deletions(-) create mode 100644 docs/protocols.rst create mode 100644 src/buildcompiler/adapters/native_protocols.py create mode 100644 src/buildcompiler/adapters/protocol_requests.py create mode 100644 src/buildcompiler/api/protocols.py create mode 100644 src/buildcompiler/domain/protocol_requests.py create mode 100644 src/buildcompiler/protocols/__init__.py create mode 100644 src/buildcompiler/protocols/allocation/__init__.py create mode 100644 src/buildcompiler/protocols/allocation/models.py create mode 100644 src/buildcompiler/protocols/allocation/plating.py create mode 100644 src/buildcompiler/protocols/allocation/wells.py create mode 100644 src/buildcompiler/protocols/artifacts.py create mode 100644 src/buildcompiler/protocols/attribution.py create mode 100644 src/buildcompiler/protocols/backends/__init__.py create mode 100644 src/buildcompiler/protocols/backends/markdown.py create mode 100644 src/buildcompiler/protocols/backends/opentrons/__init__.py create mode 100644 src/buildcompiler/protocols/backends/opentrons/_simulation_worker.py create mode 100644 src/buildcompiler/protocols/backends/opentrons/allocation.py create mode 100644 src/buildcompiler/protocols/backends/opentrons/lowering.py create mode 100644 src/buildcompiler/protocols/backends/opentrons/operations.py create mode 100644 src/buildcompiler/protocols/backends/opentrons/palette.py create mode 100644 src/buildcompiler/protocols/backends/opentrons/plating.py create mode 100644 src/buildcompiler/protocols/backends/opentrons/profile.py create mode 100644 src/buildcompiler/protocols/backends/opentrons/program.py create mode 100644 src/buildcompiler/protocols/backends/opentrons/rendering.py create mode 100644 src/buildcompiler/protocols/backends/opentrons/setup.py create mode 100644 src/buildcompiler/protocols/backends/opentrons/simulation.py create mode 100644 src/buildcompiler/protocols/backends/opentrons/tips.py create mode 100644 src/buildcompiler/protocols/backends/opentrons/transformation.py create mode 100644 src/buildcompiler/protocols/compiler.py create mode 100644 src/buildcompiler/protocols/config.py create mode 100644 src/buildcompiler/protocols/handoffs.py create mode 100644 src/buildcompiler/protocols/inputs.py create mode 100644 src/buildcompiler/protocols/materials.py create mode 100644 src/buildcompiler/protocols/methods/__init__.py create mode 100644 src/buildcompiler/protocols/methods/assembly.py create mode 100644 src/buildcompiler/protocols/methods/assembly_config.py create mode 100644 src/buildcompiler/protocols/methods/plating.py create mode 100644 src/buildcompiler/protocols/methods/plating_config.py create mode 100644 src/buildcompiler/protocols/methods/transformation.py create mode 100644 src/buildcompiler/protocols/methods/transformation_config.py create mode 100644 src/buildcompiler/protocols/plans.py create mode 100644 src/buildcompiler/protocols/steps.py create mode 100644 src/buildcompiler/protocols/validation.py create mode 100644 tests/automation/conftest.py create mode 100644 tests/automation/pudu_reference.json create mode 100644 tests/automation/test_assembly_equivalence.py delete mode 100644 tests/automation/test_opentrons_simulation_optional.py create mode 100644 tests/automation/test_plating_equivalence.py create mode 100644 tests/automation/test_transformation_equivalence.py create mode 100644 tests/automation/test_workflow_equivalence.py create mode 100644 tests/unit/adapters/test_native_protocols.py create mode 100644 tests/unit/protocols/test_compilation.py create mode 100644 tests/unit/protocols/test_transformation_plating.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4754fa7..74ef409 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,10 +35,12 @@ jobs: src/buildcompiler/adapters src/buildcompiler/reporting src/buildcompiler/domain + src/buildcompiler/protocols tests/conftest.py tests/unit tests/stages tests/integration + tests/automation - name: Ruff format check run: >- ruff format --check @@ -51,9 +53,36 @@ jobs: src/buildcompiler/adapters src/buildcompiler/reporting src/buildcompiler/domain + src/buildcompiler/protocols tests/conftest.py tests/unit tests/stages tests/integration + tests/automation - name: Core tests run: pytest tests/unit tests/stages tests/integration -q + + protocol-equivalence: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + repository: MyersResearchGroup/PUDU + ref: cdf7bde2128871c889fb25a4a0c168d5d8f0c1d3 + path: .reference/PUDU + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install simulator and test dependencies + run: python -m pip install -e '.[test,simulation]' + - name: Compare against pinned PUDU in the Opentrons simulator + env: + PUDU_REPOSITORY: ${{ github.workspace }}/.reference/PUDU + BUILDCOMPILER_REQUIRE_EQUIVALENCE: "1" + run: pytest tests/automation -q --basetemp=.equivalence-traces + - uses: actions/upload-artifact@v4 + if: always() + with: + name: protocol-equivalence-traces + path: .equivalence-traces diff --git a/README.md b/README.md index b6a1698..55fd9ee 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ The compiler should answer: - Produce in-memory PUDU-compatible JSON intermediates in compiler-only mode. - Chain successful assembly/domestication products to transformation and plating, deduplicated by product identity. - Return structured statuses, missing inputs, required approvals, warnings, summaries, and optional detailed reports. -- Keep PUDU protocol generation and Opentrons simulation optional. +- Compile assembly, transformation and plating protocols natively; keep Opentrons simulation optional. See the [protocol compiler guide](docs/protocols.rst). ## Non-goals for v1 @@ -255,7 +255,7 @@ docker compose run --rm app ruff format --check . docker compose run --rm app pytest ``` -Core CI should not require PUDU or Opentrons. Those dependencies are optional and should live behind optional test jobs or manual workflows. +Core CI does not require PUDU or Opentrons. A separate acceptance job compares native protocols against a pinned PUDU checkout using Opentrons 8.8.2. ## Testing and quality checks @@ -278,7 +278,7 @@ Testing priorities: 7. SBOL assembly service port using existing fixtures. 8. Transformation and plating deduplication. 9. Summary/report/graph generation. -10. Optional PUDU/Opentrons adapter smoke tests. +10. PUDU equivalence in the real Opentrons simulator, including the connected assembly/transformation/plating workflow. ## How ChatGPT and Codex should use these docs diff --git a/docs/development.rst b/docs/development.rst index 91c5211..2c8f139 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -19,6 +19,14 @@ Run the focused tests for the documented PUDU path: tests/unit/adapters/pudu/test_plating_json.py \ tests/test_buildcompiler_transformation.py +Run native protocol acceptance separately in the Python 3.10 simulation +environment described in ``tests/automation/README.md``: + +.. code-block:: bash + + PUDU_REPOSITORY=../PUDU BUILDCOMPILER_REQUIRE_EQUIVALENCE=1 \ + python -m pytest tests/automation -q + Build docs locally: .. code-block:: bash diff --git a/docs/index.rst b/docs/index.rst index aada7b7..d0d5e8d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -43,6 +43,7 @@ Representative use cases installation quickstart + protocols examples/offline_lvl1 examples/transformation_pudu examples/full_build diff --git a/docs/installation.rst b/docs/installation.rst index bb4c440..dafdef2 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -24,20 +24,17 @@ Install test dependencies: Optional automation dependencies -------------------------------- -PUDU and Opentrons support are optional. BuildCompiler can emit PUDU-compatible -JSON without importing PUDU. To generate or simulate OT-2 protocols, install the -automation dependencies or use a local PUDU checkout: +Native protocol compilation and PUDU-compatible JSON generation are included in +the core package. To run the pinned simulator or equivalence tests, use Python +3.10 and install: .. code-block:: bash - python -m pip install -e ".[automation,test]" + python -m pip install -e ".[simulation,test]" -In the development environment used for the repository examples, PUDU was -available as a sibling checkout: - -.. code-block:: text - - /Users/gonzalovidal/Documents/GitHub/PUDU +The equivalence suite additionally needs a PUDU reference checkout, as described +in ``tests/automation/README.md``. Generated native scripts do not need PUDU. +See :doc:`protocols` for compilation, artifact writing and handoffs. Read the Docs ------------- diff --git a/docs/protocols.rst b/docs/protocols.rst new file mode 100644 index 0000000..d17800f --- /dev/null +++ b/docs/protocols.rst @@ -0,0 +1,148 @@ +Native protocol compilation +=========================== + +``buildcompiler.protocols`` owns the assembly, transformation and plating methods +previously supplied by PUDU's ``SBOLLoopAssembly``, ``HeatShockTransformation`` +and ``Plating`` classes. The planners, allocation records and artifact renderer +require neither PUDU nor the Opentrons SDK. Generated robot scripts import only +Opentrons. The old automated script writers also delegate to this compiler. + +Structure and ownership +----------------------- + +* ``domain.protocol_requests`` defines frozen ``MaterialRef``, + ``AssemblyReaction``, ``TransformationReaction`` and method-specific request + records. Requests retain full material identities and ordered components. +* ``protocols.methods`` contains pure planners and their immutable configurations. + ``TransformationConfig`` owns transformation parameters; its planner owns + chassis grouping, tube consumption and the two replicate axes. +* ``protocols.materials`` and ``protocols.steps`` define samples and shared + operations: transfer, distribute, mix, tip lifecycle and module operations. + ``ProtocolPlan`` records explicit inputs, intermediates, outputs and lineage. +* ``protocols.allocation`` defines container-qualified placements and output + manifests. Sample identity does not depend on a display label or well name. +* ``protocols.backends.opentrons`` owns supported hardware profiles, allocation, + tip scheduling, shared operation lowering and standalone source rendering. + It emits immutable ``OpentronsProgram`` records before rendering Python. +* ``protocols.backends.markdown`` describes the same logical plan and allocation. + ``ArtifactBundle.write`` is the explicit filesystem boundary. Compilation does + not simulate, write files, or mutate SBOL documents. + +Compiling stage results +----------------------- + +Successful assembly and transformation stages include typed +``StageResult.protocol_requests`` alongside their existing JSON intermediates. +Given stage results for corresponding material identities: + +.. code-block:: python + + from buildcompiler.api import ( + compile_assembly, + compile_transformation, + compile_plating, + ) + from buildcompiler.protocols import OpentronsPlatingProfile + + assembled = compile_assembly(assembly_stage_result) + transformed = compile_transformation( + transformation_stage_result, + inputs=assembled.manifest, + ) + plated = compile_plating( + transformed.manifest, + profile=OpentronsPlatingProfile( + thermocycler_labware="nest_96_wellplate_100ul_pcr_full_skirt", + ), + ) + + assembled.artifacts.write("results/assembly") + transformed.artifacts.write("results/transformation") + plated.artifacts.write("results/plating") + +``OutputManifest`` represents planned outputs, not evidence that a run happened. +The transformation compiler preserves each assembly sample's ID as an upstream +source reference and rebinds its well to the source DNA plate. Every plasmid in +the request must exist in that manifest, with equal source-location replicate +counts. These multiply the configured transformation replicates; the two axes +are not interchangeable. Without an input manifest, DNA is allocated sequentially +on the temperature module. + +The plating profile is explicit here because PUDU's standalone plating default +uses a different source plate definition from its transformation default. The +compiler does not infer a physical plate transfer from material identity. + +Compiling existing JSON +----------------------- + +.. code-block:: python + + from buildcompiler.protocols import ( + ProtocolCompiler, + TransformationConfig, + plasmid_manifest_from_json, + transformation_request_from_json, + ) + + request = transformation_request_from_json( + transformation_data, + request_id="transformation", + ) + config = TransformationConfig().with_overrides(explicit_overrides) + compiled = ProtocolCompiler(transformation=config).compile( + request, + inputs=plasmid_manifest_from_json(plasmid_locations), + ) + +``assembly_request_from_json`` accepts the existing assembly format. +``bacterium_manifest_from_json`` accepts the existing plating handoff; because +that format contains labels rather than material identities, the adapter assigns +local import identities instead of claiming those labels are SBOL URIs. Native +manifests retain identity and lineage throughout a workflow. + +Each compilation returns ``protocol.py``, ``protocol.md``, ``manifest.json``, +``compilation.json`` and a method-specific JSON handoff. ``compilation.json`` +records the resolved configuration, target profile, typed operation kinds, +containers, placements and PUDU reference revision. Writing refuses existing +files unless ``overwrite=True`` is supplied explicitly. + +Configuration and supported targets +------------------------------------ + +``AssemblyConfig``, ``TransformationConfig`` and ``PlatingConfig`` are separate +from their ``Opentrons*Profile`` hardware records. ``with_overrides`` applies +explicit keys, including ``False`` and default values, and rejects unknown keys. +Profiles validate deck collisions, well offsets and tip offsets. Compilation +rejects missing source bindings, duplicate placements, exhausted tips, unsupported +profiles and capacity violations detected by the supported target. + +The initial targets preserve PUDU's API 2.21 hardware choices, pipettes and labware. +They expose deck and starting-position configuration; they do not promise support +for arbitrary replacement instruments or labware definitions. Transformation and +plating each use one rack per pipette and reject batches that exhaust those racks. +Assembly retains its explicit rack replacement schedule. + +PUDU's default transformation simulation skips temperature operations. The native +plan retains those operations with an explicit simulation condition, so both the +review document and script show the distinction. ``water_testing=True`` omits +those operations altogether. + +Validation and migration boundary +---------------------------------- + +Install ``.[test,simulation]`` in Python 3.10 to run the pinned Opentrons 8.8.2 +acceptance suite described in ``tests/automation/README.md``. The suite compares +the independent PUDU implementation with native output and tests the connected +three-stage handoff. It separately exercises transformation's temperature path +inside the simulator. Hardware and experimental outcomes are not tested. + +The native compiler does not reproduce PUDU's Excel presentation or simulation-time +file writes. It returns structured artifacts explicitly. Other PUDU protocol +families are outside this migration. Legacy orchestration, notebook APIs and the +legacy manual plating summary remain compatibility surfaces; their automated +assembly, transformation and plating script writers use the native compiler. +Legacy multi-batch plating produces one standalone script per batch instead of +passing an unsupported ``batches`` object into a single protocol. + +PUDU's MIT attribution is retained in ``protocols.attribution`` and generated +scripts. diff --git a/pyproject.toml b/pyproject.toml index 2719f5b..a869d77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,10 @@ dependencies = [ ] [project.optional-dependencies] +simulation = [ + "opentrons==8.8.2 ; python_version == '3.10'", + "xlsxwriter>=3.2.5", +] test = [ "pytest>=7,<9", "pytest-cov[all]" @@ -47,8 +51,7 @@ dev = [ "twine>=5.0", ] automation = [ - "pudupy", - "opentrons", + "opentrons==8.8.2 ; python_version == '3.10'", "SBOLInventory @ git+https://github.com/DRAGGON-Lab/SBOLInventory.git ; python_version >= '3.10'", ] notebook = [ diff --git a/setup.py b/setup.py index 0bcf9e5..e62853e 100644 --- a/setup.py +++ b/setup.py @@ -37,6 +37,10 @@ "pydna", ], extras_require={ + "simulation": [ + "opentrons==8.8.2 ; python_version == '3.10'", + "xlsxwriter>=3.2.5", + ], "test": [ "pytest>=7,<9", "pytest-cov[all]", @@ -49,8 +53,7 @@ "twine>=5.0", ], "automation": [ - "pudupy", - "opentrons", + "opentrons==8.8.2 ; python_version == '3.10'", "SBOLInventory @ " "git+https://github.com/DRAGGON-Lab/SBOLInventory.git ; " "python_version >= '3.10'", diff --git a/src/buildcompiler/__init__.py b/src/buildcompiler/__init__.py index acc0cd7..1a76eaa 100644 --- a/src/buildcompiler/__init__.py +++ b/src/buildcompiler/__init__.py @@ -10,3 +10,8 @@ full_build as full_build, transformation as transformation, ) +from buildcompiler.api.protocols import ( + compile_assembly, + compile_transformation, + compile_plating, +) diff --git a/src/buildcompiler/adapters/native_protocols.py b/src/buildcompiler/adapters/native_protocols.py new file mode 100644 index 0000000..9b32fe6 --- /dev/null +++ b/src/buildcompiler/adapters/native_protocols.py @@ -0,0 +1,55 @@ +"""Legacy JSON entry points backed by BuildCompiler's native protocol compiler.""" + +from dataclasses import fields + +from buildcompiler.protocols import ( + OpentronsPlatingProfile, + PlatingConfig, + PlatingRequest, + ProtocolCompiler, + assembly_request_from_json, + bacterium_manifest_from_json, + plasmid_manifest_from_json, + transformation_request_from_json, +) + + +def compile_assembly_payload(payload, *, name): + return ProtocolCompiler().compile( + assembly_request_from_json(payload, request_id=name) + ) + + +def compile_transformation_payload(payload, *, plasmid_locations): + request = transformation_request_from_json( + payload, request_id="BuildCompiler Transformation" + ) + inputs = ( + plasmid_manifest_from_json(plasmid_locations) if plasmid_locations else None + ) + return ProtocolCompiler().compile(request, inputs=inputs) + + +def compile_plating_payload(payload, *, advanced_params=None): + parameters = dict(payload) + parameters.update(advanced_params or {}) + inputs = bacterium_manifest_from_json(parameters) + parameters.pop("bacterium_locations") + name = parameters.pop("protocol_name", "BuildCompiler Plating") + config_names = {f.name for f in fields(PlatingConfig)} + profile_names = {f.name for f in fields(OpentronsPlatingProfile)} + config = PlatingConfig( + **{key: value for key, value in parameters.items() if key in config_names} + ) + profile = OpentronsPlatingProfile( + **{key: value for key, value in parameters.items() if key in profile_names} + ) + unknown = set(parameters) - config_names - profile_names + if unknown: + raise ValueError( + f"Unsupported native plating parameters: {sorted(unknown)}. Use a supported target profile." + ) + request = PlatingRequest(id=name, sample_ids=tuple(s.id for s in inputs.samples)) + return ProtocolCompiler(plating=config).compile( + request, inputs=inputs, profile=profile + ) diff --git a/src/buildcompiler/adapters/opentrons/simulation.py b/src/buildcompiler/adapters/opentrons/simulation.py index 1fe66db..340daa7 100644 --- a/src/buildcompiler/adapters/opentrons/simulation.py +++ b/src/buildcompiler/adapters/opentrons/simulation.py @@ -2,12 +2,12 @@ from __future__ import annotations +import importlib.util from dataclasses import dataclass, field from pathlib import Path -import shutil -import subprocess -from buildcompiler.api import ProtocolOptions +from buildcompiler.api.options import ProtocolOptions +from buildcompiler.protocols.backends.opentrons.simulation import simulate_source class OptionalAutomationDependencyError(ImportError): @@ -36,36 +36,27 @@ def simulate( metadata={"protocol_source": str(protocol_source)}, ) - executable = shutil.which("opentrons_simulate") - if executable is None: + if importlib.util.find_spec("opentrons") is None: raise OptionalAutomationDependencyError( - "Install synbio-buildcompiler[automation] and ensure " - "opentrons_simulate is on PATH." + "Install synbio-buildcompiler[simulation] in a Python 3.10 environment to use Opentrons simulation." ) source = Path(protocol_source) if not source.is_file(): raise FileNotFoundError(f"Protocol source does not exist: {source}") - completed = subprocess.run( - [executable, source.name], - check=False, - capture_output=True, - cwd=source.parent, - text=True, - ) - logs = [line for line in (completed.stdout, completed.stderr) if line] - if completed.returncode: - raise ProtocolSimulationError( - f"Opentrons simulation failed with exit code {completed.returncode}: " - + "\n".join(logs) - ) + try: + trace = simulate_source(source.read_text(encoding="utf-8")) + except RuntimeError as exc: + raise ProtocolSimulationError(str(exc)) from exc return SimulationResult( ran=True, - logs=logs, + logs=["Opentrons simulation completed.", *trace.stdout.splitlines()], metadata={ - "protocol_source": str(source), - "command": [executable, source.name], - "returncode": completed.returncode, + "protocol_source": str(protocol_source), + "simulator_version": trace.simulator_version, + "returncode": 0, + "command_count": len(trace.actions), + "stderr": trace.stderr, }, ) diff --git a/src/buildcompiler/adapters/protocol_requests.py b/src/buildcompiler/adapters/protocol_requests.py new file mode 100644 index 0000000..86ae83b --- /dev/null +++ b/src/buildcompiler/adapters/protocol_requests.py @@ -0,0 +1,42 @@ +"""Translate selected build routes into typed protocol requests.""" + +from collections.abc import Sequence + +from buildcompiler.domain import IndexedBackbone, IndexedPlasmid, IndexedReagent +from buildcompiler.domain.protocol_requests import ( + AssemblyReaction, + AssemblyRequest, + MaterialRef, +) + + +def assembly_request_from_route( + *, + stage_id: str, + products: Sequence[IndexedPlasmid], + parts: Sequence[IndexedPlasmid], + backbone: IndexedBackbone, + restriction_enzyme: IndexedReagent, +) -> AssemblyRequest: + """Use actual produced identities and preserve the selected component order.""" + + def material(record) -> MaterialRef: + return MaterialRef( + identity=record.identity, + label=record.name or record.display_id or record.identity, + ) + + return AssemblyRequest( + id=stage_id, + source_stage_id=stage_id, + reactions=tuple( + AssemblyReaction( + id=f"{stage_id}/product/{index}", + product=material(product), + backbone=material(backbone), + parts=tuple(material(part) for part in parts), + restriction_enzyme=material(restriction_enzyme), + ) + for index, product in enumerate(products) + ), + ) diff --git a/src/buildcompiler/api/__init__.py b/src/buildcompiler/api/__init__.py index ae81ed8..909d7a1 100644 --- a/src/buildcompiler/api/__init__.py +++ b/src/buildcompiler/api/__init__.py @@ -10,6 +10,11 @@ SynBioHubResponseError, ) from buildcompiler.inventory import index_collections +from buildcompiler.api.protocols import ( + compile_assembly, + compile_transformation, + compile_plating, +) from .compiler import ( BuildCompiler, @@ -86,6 +91,9 @@ "full_build", "index_collections", "transformation", + "compile_assembly", + "compile_transformation", + "compile_plating", "deserialize_build_plan", "dumps_json_dto", "serialize_approval", diff --git a/src/buildcompiler/api/protocols.py b/src/buildcompiler/api/protocols.py new file mode 100644 index 0000000..f516660 --- /dev/null +++ b/src/buildcompiler/api/protocols.py @@ -0,0 +1,94 @@ +"""Compile stage requests and physical handoffs without rerunning SBOL operations.""" + +from buildcompiler.domain import StageResult, StageStatus +from buildcompiler.domain.protocol_requests import ( + AssemblyRequest, + TransformationRequest, +) +from buildcompiler.protocols.allocation.models import OutputManifest +from buildcompiler.protocols import ( + AssemblyConfig, + CompiledProtocol, + OpentronsAssemblyProfile, + ProtocolCompiler, + TransformationConfig, + OpentronsTransformationProfile, + PlatingConfig, + PlatingRequest, + OpentronsPlatingProfile, +) + + +def compile_assembly( + result: StageResult, + *, + config: AssemblyConfig = AssemblyConfig(), + profile: OpentronsAssemblyProfile = OpentronsAssemblyProfile(), +) -> CompiledProtocol: + """Allocate the stage's complete batch once and return in-memory artifacts.""" + if result.status not in (StageStatus.SUCCESS, StageStatus.PARTIAL_SUCCESS): + raise ValueError("Only successful stage outputs can be compiled.") + if not result.protocol_requests: + raise ValueError("The stage contains no typed assembly requests.") + if any( + not isinstance(request, AssemblyRequest) for request in result.protocol_requests + ): + raise TypeError("The stage contains requests for another method.") + reactions = tuple( + reaction + for request in result.protocol_requests + for reaction in request.reactions + ) + request = AssemblyRequest( + id=result.id, reactions=reactions, source_stage_id=result.id + ) + return ProtocolCompiler(assembly=config).compile(request, profile=profile) + + +def compile_transformation( + result: StageResult, + *, + config: TransformationConfig = TransformationConfig(), + profile: OpentronsTransformationProfile = OpentronsTransformationProfile(), + inputs: OutputManifest | None = None, +) -> CompiledProtocol: + """Compile the complete transformation batch against an optional assembly manifest.""" + if result.status not in (StageStatus.SUCCESS, StageStatus.PARTIAL_SUCCESS): + raise ValueError("Only successful stage outputs can be compiled.") + if not result.protocol_requests: + raise ValueError("The stage contains no typed transformation requests.") + if any( + not isinstance(request, TransformationRequest) + for request in result.protocol_requests + ): + raise TypeError("The stage contains requests for another method.") + request = TransformationRequest( + id=result.id, + source_stage_id=result.id, + reactions=tuple( + reaction + for request in result.protocol_requests + for reaction in request.reactions + ), + ) + return ProtocolCompiler(transformation=config).compile( + request, profile=profile, inputs=inputs + ) + + +def compile_plating( + inputs: OutputManifest, + *, + request_id: str = "plating", + config: PlatingConfig = PlatingConfig(), + profile: OpentronsPlatingProfile = OpentronsPlatingProfile(), +) -> CompiledProtocol: + """Compile plating for the exact physical samples in a transformation manifest.""" + request = PlatingRequest( + id=request_id, + source_stage_id=inputs.protocol_id, + sample_ids=tuple(sample.id for sample in inputs.samples), + ) + return ProtocolCompiler(plating=config).compile( + request, profile=profile, inputs=inputs + ) diff --git a/src/buildcompiler/api/serialization.py b/src/buildcompiler/api/serialization.py index 1ad9a99..7e76f46 100644 --- a/src/buildcompiler/api/serialization.py +++ b/src/buildcompiler/api/serialization.py @@ -245,6 +245,7 @@ def serialize_stage_result(result: StageResult) -> dict[str, Any]: ], "warnings": [serialize_warning(item) for item in result.warnings], "json_intermediate": _json_safe(result.json_intermediate), + "protocol_requests": _json_safe(result.protocol_requests), "protocol_artifacts": _json_safe(result.protocol_artifacts), "logs": list(result.logs), } diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index db1d4f8..da86aa4 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -32,6 +32,11 @@ write_plate_map_json, write_plating_protocol_script, ) +from buildcompiler.adapters.native_protocols import ( + compile_assembly_payload, + compile_transformation_payload, + compile_plating_payload, +) from .adapters.pudu import ( legacy_assembly_routes_to_pudu_json, plasmid_locations_to_pudu_json, @@ -1768,11 +1773,16 @@ def _write_full_build_artifacts( plating_path = results_path / "plating_pudu_input.json" self._write_json(plating_path, plating_payload) written.append(plating_path) - written.append( - self._write_pudu_plating_protocol_script( - results_path / "pudu_plating_protocol.py", plating_payload + for batch_index, batch_payload in enumerate(plating_payloads): + filename = ( + "pudu_plating_protocol.py" if batch_index == 0 + else f"pudu_plating_protocol_batch_{batch_index + 1}.py" + ) + written.append( + self._write_pudu_plating_protocol_script( + results_path / filename, batch_payload + ) ) - ) return written @@ -1787,65 +1797,25 @@ def _plasmid_sets_for_transformed_strains( def _write_pudu_assembly_protocol_script( self, path: Path, payload: list[dict[str, object]], protocol_name: str ) -> Path: - script = ( - "from pudu.assembly import SBOLLoopAssembly\n" - "from opentrons import protocol_api\n\n" - f"assembly_data = {json.dumps(payload, indent=4)}\n\n" - "metadata = {\n" - f" 'protocolName': {protocol_name!r},\n" - " 'author': 'BuildCompiler',\n" - " 'apiLevel': '2.21',\n" - "}\n\n" - "def run(protocol: protocol_api.ProtocolContext):\n" - " protocol_instance = SBOLLoopAssembly(assembly_data=assembly_data)\n" - " protocol_instance.run(protocol)\n" - ) - path.write_text(script, encoding="utf-8") + compiled = compile_assembly_payload(payload, name=protocol_name) + path.write_text(compiled.script, encoding="utf-8") return path def _write_pudu_transformation_protocol_script( - self, - path: Path, - transformation_payload: list[dict[str, object]], + self, path: Path, transformation_payload: list[dict[str, object]], plasmid_locations: dict[str, list[str]], ) -> Path: - script = ( - "from pudu.transformation import HeatShockTransformation\n" - "from opentrons import protocol_api\n\n" - f"transformation_data = {json.dumps(transformation_payload, indent=4)}\n\n" - f"plasmid_locations = {json.dumps(plasmid_locations, indent=4)}\n\n" - "metadata = {\n" - " 'protocolName': 'BuildCompiler Transformation',\n" - " 'author': 'BuildCompiler',\n" - " 'apiLevel': '2.21',\n" - "}\n\n" - "def run(protocol: protocol_api.ProtocolContext):\n" - " protocol_instance = HeatShockTransformation(\n" - " transformation_data=transformation_data,\n" - " plasmid_locations=plasmid_locations,\n" - " )\n" - " protocol_instance.run(protocol)\n" + compiled = compile_transformation_payload( + transformation_payload, plasmid_locations=plasmid_locations ) - path.write_text(script, encoding="utf-8") + path.write_text(compiled.script, encoding="utf-8") return path def _write_pudu_plating_protocol_script( self, path: Path, plating_payload: dict[str, object] ) -> Path: - script = ( - "from pudu.plating import Plating\n" - "from opentrons import protocol_api\n\n" - f"plating_data = {json.dumps(plating_payload, indent=4)}\n\n" - "metadata = {\n" - " 'protocolName': 'BuildCompiler Plating',\n" - " 'author': 'BuildCompiler',\n" - " 'apiLevel': '2.21',\n" - "}\n\n" - "def run(protocol: protocol_api.ProtocolContext):\n" - " protocol_instance = Plating(plating_data=plating_data)\n" - " protocol_instance.run(protocol)\n" - ) - path.write_text(script, encoding="utf-8") + compiled = compile_plating_payload(plating_payload) + path.write_text(compiled.script, encoding="utf-8") return path def _resolve_full_build_zip_path( diff --git a/src/buildcompiler/domain/build_result.py b/src/buildcompiler/domain/build_result.py index 3a6ff84..873623d 100644 --- a/src/buildcompiler/domain/build_result.py +++ b/src/buildcompiler/domain/build_result.py @@ -3,6 +3,8 @@ from dataclasses import dataclass, field from typing import Any +from buildcompiler.domain.protocol_requests import ProtocolRequest + from .approvals import RequiredApproval from .build_stage import BuildStage from .missing_input import MissingBuildInput @@ -27,6 +29,7 @@ class StageResult: json_intermediate: dict[str, Any] | list[Any] | None = None protocol_artifacts: dict[str, Any] = field(default_factory=dict) logs: list[str] = field(default_factory=list) + protocol_requests: tuple[ProtocolRequest, ...] = () @dataclass diff --git a/src/buildcompiler/domain/protocol_requests.py b/src/buildcompiler/domain/protocol_requests.py new file mode 100644 index 0000000..54f0bf3 --- /dev/null +++ b/src/buildcompiler/domain/protocol_requests.py @@ -0,0 +1,90 @@ +"""Immutable protocol inputs shared by build stages and protocol compilers.""" + +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True, kw_only=True) +class MaterialRef: + identity: str + label: str + + def __post_init__(self) -> None: + if not self.identity or not self.label: + raise ValueError("Material identity and label must be nonempty.") + + +@dataclass(frozen=True, slots=True, kw_only=True) +class AssemblyReaction: + id: str + product: MaterialRef + backbone: MaterialRef + parts: tuple[MaterialRef, ...] + restriction_enzyme: MaterialRef + + def __post_init__(self) -> None: + if not self.id or not self.parts: + raise ValueError("An assembly reaction requires an id and ordered parts.") + if not isinstance(self.parts, tuple): + raise TypeError("AssemblyReaction.parts must be a tuple.") + + +@dataclass(frozen=True, slots=True, kw_only=True) +class AssemblyRequest: + id: str + reactions: tuple[AssemblyReaction, ...] + source_stage_id: str | None = None + + def __post_init__(self) -> None: + if not self.id or not self.reactions: + raise ValueError("An assembly request requires an id and reactions.") + if not isinstance(self.reactions, tuple): + raise TypeError("AssemblyRequest.reactions must be a tuple.") + if len({reaction.id for reaction in self.reactions}) != len(self.reactions): + raise ValueError("Reaction ids must be unique within a request.") + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TransformationReaction: + id: str + strain: MaterialRef + chassis: MaterialRef + plasmids: tuple[MaterialRef, ...] + + def __post_init__(self) -> None: + if not self.id or not self.plasmids: + raise ValueError("A transformation requires an id and plasmids.") + if not isinstance(self.plasmids, tuple): + raise TypeError("Transformation plasmids must be a tuple.") + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TransformationRequest: + id: str + reactions: tuple[TransformationReaction, ...] + source_stage_id: str | None = None + + def __post_init__(self) -> None: + if not self.id or not isinstance(self.reactions, tuple) or not self.reactions: + raise ValueError( + "A transformation request requires an id and a nonempty reaction tuple." + ) + if len({r.id for r in self.reactions}) != len(self.reactions): + raise ValueError("Reaction ids must be unique within a request.") + + +@dataclass(frozen=True, slots=True, kw_only=True) +class PlatingRequest: + id: str + sample_ids: tuple[str, ...] + source_stage_id: str | None = None + + def __post_init__(self) -> None: + if not self.id or not isinstance(self.sample_ids, tuple) or not self.sample_ids: + raise ValueError( + "A plating request requires an id and a nonempty sample tuple." + ) + if len(set(self.sample_ids)) != len(self.sample_ids): + raise ValueError("Plating source sample ids must be unique.") + + +ProtocolRequest = AssemblyRequest | TransformationRequest | PlatingRequest diff --git a/src/buildcompiler/protocols/__init__.py b/src/buildcompiler/protocols/__init__.py new file mode 100644 index 0000000..a258393 --- /dev/null +++ b/src/buildcompiler/protocols/__init__.py @@ -0,0 +1,51 @@ +"""Typed protocol planning and artifact compilation, without optional SDK imports.""" + +from buildcompiler.domain.protocol_requests import ( + AssemblyReaction, + AssemblyRequest, + MaterialRef, + TransformationRequest, + TransformationReaction, + PlatingRequest, +) + +from buildcompiler.protocols.backends.opentrons.profile import ( + OpentronsAssemblyProfile, + OpentronsTransformationProfile, + OpentronsPlatingProfile, +) +from buildcompiler.protocols.compiler import CompiledProtocol, ProtocolCompiler +from buildcompiler.protocols.config import AssemblyConfig +from buildcompiler.protocols.inputs import ( + assembly_request_from_json, + transformation_request_from_json, +) +from buildcompiler.protocols.handoffs import ( + plasmid_manifest_from_json, + bacterium_manifest_from_json, +) +from buildcompiler.protocols.methods.transformation_config import TransformationConfig +from buildcompiler.protocols.methods.plating_config import PlatingConfig +from buildcompiler.protocols.plans import ProtocolPlan + +__all__ = [ + "AssemblyConfig", + "AssemblyReaction", + "AssemblyRequest", + "CompiledProtocol", + "MaterialRef", + "OpentronsAssemblyProfile", + "ProtocolCompiler", + "ProtocolPlan", + "assembly_request_from_json", + "TransformationConfig", + "TransformationReaction", + "TransformationRequest", + "OpentronsTransformationProfile", + "transformation_request_from_json", + "plasmid_manifest_from_json", + "PlatingRequest", + "PlatingConfig", + "OpentronsPlatingProfile", + "bacterium_manifest_from_json", +] diff --git a/src/buildcompiler/protocols/allocation/__init__.py b/src/buildcompiler/protocols/allocation/__init__.py new file mode 100644 index 0000000..78f2e70 --- /dev/null +++ b/src/buildcompiler/protocols/allocation/__init__.py @@ -0,0 +1 @@ +"""Container geometry and explicit sample placement.""" diff --git a/src/buildcompiler/protocols/allocation/models.py b/src/buildcompiler/protocols/allocation/models.py new file mode 100644 index 0000000..4c1b8e0 --- /dev/null +++ b/src/buildcompiler/protocols/allocation/models.py @@ -0,0 +1,123 @@ +"""Allocation records are data, independent of any robot SDK.""" + +from dataclasses import dataclass + +from buildcompiler.protocols.materials import Sample +from buildcompiler.protocols.plans import ProtocolPlan + + +@dataclass(frozen=True, slots=True, kw_only=True) +class WellRef: + container_id: str + well_name: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class SamplePlacement: + sample_id: str + location: WellRef + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ContainerSpec: + id: str + definition_id: str + parent: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class OutputManifest: + protocol_id: str + samples: tuple[Sample, ...] + placements: tuple[SamplePlacement, ...] + + def __post_init__(self) -> None: + ids = {s.id for s in self.samples} + if ( + len(ids) != len(self.samples) + or len(self.placements) != len(ids) + or {p.sample_id for p in self.placements} != ids + ): + raise ValueError( + "Manifest samples need unique ids and exactly one placement each." + ) + if len({p.location for p in self.placements}) != len(self.placements): + raise ValueError("Manifest sample locations must be unique.") + + def to_dict(self) -> dict: + locations = {p.sample_id: p.location for p in self.placements} + return { + "schema_version": "1.0", + "protocol_id": self.protocol_id, + "state": "planned", + "outputs": [ + { + "sample_id": sample.id, + "material_identity": sample.material.identity, + "label": sample.material.label, + "parent_sample_ids": list(sample.parent_ids), + "replicate": sample.replicate, + "source_sample_id": sample.source_sample_id, + "contents": list(sample.contents), + "dilution": sample.dilution, + "container_id": locations[sample.id].container_id, + "well_name": locations[sample.id].well_name, + } + for sample in self.samples + ], + } + + def plasmid_locations(self) -> dict[str, list[str]]: + """Export the PUDU handoff only when one container is unambiguous.""" + locations = {p.sample_id: p.location for p in self.placements} + if len({p.location.container_id for p in self.placements}) > 1: + raise ValueError( + "PUDU's location format cannot represent multiple containers." + ) + result: dict[str, list[str]] = {} + for sample in self.samples: + result.setdefault(sample.material.identity, []).append( + locations[sample.id].well_name + ) + return result + + def bacterium_locations(self) -> dict[str, list[str]]: + if len({p.location.container_id for p in self.placements}) > 1: + raise ValueError( + "PUDU's location format cannot represent multiple containers." + ) + locations = {p.sample_id: p.location for p in self.placements} + return { + locations[s.id].well_name: list(s.contents or (s.material.label,)) + for s in self.samples + } + + +@dataclass(frozen=True, slots=True, kw_only=True) +class AllocatedProtocolPlan: + protocol: ProtocolPlan + containers: tuple[ContainerSpec, ...] + placements: tuple[SamplePlacement, ...] + + def __post_init__(self) -> None: + sample_ids = {sample.id for sample in self.protocol.samples} + if ( + len(self.placements) != len(sample_ids) + or {p.sample_id for p in self.placements} != sample_ids + ): + raise ValueError("Every sample must have exactly one placement.") + if len({p.location for p in self.placements}) != len(self.placements): + raise ValueError("Two samples cannot occupy the same location.") + container_ids = {container.id for container in self.containers} + if len(container_ids) != len(self.containers): + raise ValueError("Container ids must be unique.") + if any(p.location.container_id not in container_ids for p in self.placements): + raise ValueError("Placements must reference declared containers.") + + def output_manifest(self) -> OutputManifest: + ids = set(self.protocol.output_sample_ids) + return OutputManifest( + protocol_id=self.protocol.id, + samples=tuple(s for s in self.protocol.samples if s.id in ids), + placements=tuple(p for p in self.placements if p.sample_id in ids), + ) diff --git a/src/buildcompiler/protocols/allocation/plating.py b/src/buildcompiler/protocols/allocation/plating.py new file mode 100644 index 0000000..d672443 --- /dev/null +++ b/src/buildcompiler/protocols/allocation/plating.py @@ -0,0 +1,30 @@ +"""Export the plated sample graph without recalculating a second plate layout.""" + +from buildcompiler.protocols.allocation.models import AllocatedProtocolPlan + + +def plating_layout( + allocation: AllocatedProtocolPlan, *, dilution_factor: float +) -> dict: + samples = {s.id: s for s in allocation.protocol.samples} + locations = {p.sample_id: p.location for p in allocation.placements} + plates = {} + outputs = [samples[sid] for sid in allocation.protocol.output_sample_ids] + # PUDU's export groups dilution first, then construct and replicate. + for sample in sorted(outputs, key=lambda s: s.dilution): + source = samples[sample.parent_ids[0]] + while source.role == "dilution": + source = samples[source.parent_ids[0]] + location = locations[sample.id] + plate = location.container_id.replace("agar_", "plate_") + factor = dilution_factor**sample.dilution + factor = int(factor) if float(factor).is_integer() else factor + entry = plates.setdefault(plate, {}).setdefault( + f"dilution_{sample.dilution}", {"ratio": f"1/{factor}", "wells": {}} + ) + entry["wells"][location.well_name] = { + "construct": ", ".join(source.contents), + "source_well": locations[source.id].well_name, + "replicate": sample.replicate, + } + return {"agar_plates": plates} diff --git a/src/buildcompiler/protocols/allocation/wells.py b/src/buildcompiler/protocols/allocation/wells.py new file mode 100644 index 0000000..934bd69 --- /dev/null +++ b/src/buildcompiler/protocols/allocation/wells.py @@ -0,0 +1,39 @@ +"""Well geometry with explicit, deterministic column-major traversal.""" + +import re +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True, kw_only=True) +class WellGrid: + rows: int + columns: int + + def __post_init__(self) -> None: + if type(self.rows) is not int or not 1 <= self.rows <= 26: + raise ValueError("rows must be an integer between 1 and 26.") + if type(self.columns) is not int or self.columns < 1: + raise ValueError("columns must be a positive integer.") + + @property + def capacity(self) -> int: + return self.rows * self.columns + + def name(self, index: int) -> str: + if type(index) is not int or not 0 <= index < self.capacity: + raise ValueError(f"Well index {index} is outside this grid.") + return f"{chr(ord('A') + index % self.rows)}{index // self.rows + 1}" + + def index(self, name: str) -> int: + match = re.fullmatch(r"([A-Z])([1-9][0-9]*)", name) + if not match: + raise ValueError(f"Invalid well name: {name!r}") + row = ord(match[1]) - ord("A") + column = int(match[2]) - 1 + if row >= self.rows or column >= self.columns: + raise ValueError(f"Well {name} is outside this grid.") + return column * self.rows + row + + +PLATE_96 = WellGrid(rows=8, columns=12) +BLOCK_24 = WellGrid(rows=4, columns=6) diff --git a/src/buildcompiler/protocols/artifacts.py b/src/buildcompiler/protocols/artifacts.py new file mode 100644 index 0000000..1f371dd --- /dev/null +++ b/src/buildcompiler/protocols/artifacts.py @@ -0,0 +1,43 @@ +"""In-memory artifacts and an explicit, opt-in filesystem boundary.""" + +from dataclasses import dataclass +from pathlib import Path + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ProtocolArtifact: + name: str + media_type: str + content: str + + def __post_init__(self) -> None: + if ( + not self.name + or self.name in (".", "..") + or Path(self.name).name != self.name + or "\\" in self.name + ): + raise ValueError("Artifact names must be plain filenames.") + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ArtifactBundle: + artifacts: tuple[ProtocolArtifact, ...] + + def __post_init__(self) -> None: + if len({artifact.name for artifact in self.artifacts}) != len(self.artifacts): + raise ValueError("Artifact filenames must be unique.") + + def write( + self, directory: str | Path, *, overwrite: bool = False + ) -> tuple[Path, ...]: + """Write only when called, refusing existing files by default.""" + output = Path(directory) + paths = tuple(output / artifact.name for artifact in self.artifacts) + if not overwrite and any(path.exists() for path in paths): + raise FileExistsError("One or more output artifacts already exist.") + output.mkdir(parents=True, exist_ok=True) + for path, artifact in zip(paths, self.artifacts, strict=True): + with path.open("w" if overwrite else "x", encoding="utf-8") as handle: + handle.write(artifact.content) + return paths diff --git a/src/buildcompiler/protocols/attribution.py b/src/buildcompiler/protocols/attribution.py new file mode 100644 index 0000000..83bf9d2 --- /dev/null +++ b/src/buildcompiler/protocols/attribution.py @@ -0,0 +1,26 @@ +"""Attribution retained in source distributions and standalone generated scripts.""" + +PUDU_REVISION = "cdf7bde2128871c889fb25a4a0c168d5d8f0c1d3" + +PUDU_LICENSE = """MIT License + +Copyright (c) 2022 Rudge Lab + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" diff --git a/src/buildcompiler/protocols/backends/__init__.py b/src/buildcompiler/protocols/backends/__init__.py new file mode 100644 index 0000000..a16601a --- /dev/null +++ b/src/buildcompiler/protocols/backends/__init__.py @@ -0,0 +1 @@ +"""Artifact renderers and target-specific compilation.""" diff --git a/src/buildcompiler/protocols/backends/markdown.py b/src/buildcompiler/protocols/backends/markdown.py new file mode 100644 index 0000000..1580f46 --- /dev/null +++ b/src/buildcompiler/protocols/backends/markdown.py @@ -0,0 +1,104 @@ +"""Render a review document directly from the same plan used by automation.""" + +from buildcompiler.protocols.allocation.models import AllocatedProtocolPlan +from buildcompiler.protocols.plans import ProtocolPlan +from buildcompiler.protocols.steps import ( + DeactivateSourceModule, + DropTip, + LidAction, + OperatorAction, + RunTemperatureProgram, + SetTemperature, + Transfer, + Distribute, + PickUpTip, + Mix, +) + + +def _escape(text: str) -> str: + return text.replace("\\", "\\\\").replace("|", "\\|").replace("\n", " ") + + +def render_markdown( + plan: ProtocolPlan, *, allocation: AllocatedProtocolPlan | None = None +) -> str: + """Render planned operations, never claim that an experiment has run.""" + if allocation is not None and allocation.protocol != plan: + raise ValueError("Allocation belongs to a different protocol plan.") + samples = {sample.id: sample for sample in plan.samples} + locations = ( + {p.sample_id: p.location for p in allocation.placements} if allocation else {} + ) + + def describe(sample_id: str) -> str: + sample = samples[sample_id] + result = _escape(sample.material.label) + if sample.replicate is not None: + result += f" (replicate {sample.replicate})" + if sample_id in locations: + location = locations[sample_id] + result += f" [{location.container_id}/{location.well_name}]" + return result + + lines = [ + f"# Protocol {_escape(plan.id)}", + "", + "Status: planned. This document describes the compiled protocol; it is not an execution record.", + "", + "## Operations", + "", + ] + for index, step in enumerate(plan.steps, 1): + if isinstance(step, Transfer): + action = f"Transfer {step.volume_ul:g} µL from {describe(step.source.sample_id)} to {describe(step.destination.sample_id)}." + if step.limit_to_pipette_capacity: + action += " Limit each movement to the selected pipette capacity." + if step.mix_before_ul: + action += f" Mix source {step.mix_repetitions} times at {step.mix_before_ul:g} µL first." + action += " Use a new tip." if step.new_tip else " Retain the attached tip." + if step.drop_tip: + action += " Discard the tip afterward." + elif isinstance(step, Distribute): + action = ( + f"Distribute {step.volume_ul:g} µL from {describe(step.source.sample_id)} to each of: " + + "; ".join(describe(d.sample_id) for d in step.destinations) + + "." + ) + if step.mix_before: + action += f" Mix source {step.mix_before[0]} times at {step.mix_before[1]:g} µL first." + action += f" Tip policy: {step.new_tip}; disposal volume: {step.disposal_volume_ul:g} µL." + if step.air_gap_ul: + action += f" Air gap: {step.air_gap_ul:g} µL." + elif isinstance(step, Mix): + action = f"Mix {describe(step.location.sample_id)} {step.repetitions} times at {step.volume_ul:g} µL." + elif isinstance(step, PickUpTip): + action = f"Pick up a tip on {step.instrument}." + elif isinstance(step, DropTip): + action = "Discard the attached tip." + elif isinstance(step, SetTemperature): + action = f"Set {step.module} temperature to {step.celsius:g} °C." + elif isinstance(step, RunTemperatureProgram): + program = "; ".join( + f"{s.celsius:g} °C for {s.minutes:g} minutes" + for s in step.program.steps + ) + action = f"Run {program}; repetitions: {step.program.repetitions}." + elif isinstance(step, LidAction): + action = f"{step.action.capitalize()} the thermocycler lid." + elif isinstance(step, DeactivateSourceModule): + action = "Deactivate the source temperature module." + elif isinstance(step, OperatorAction): + action = step.instruction + else: + raise TypeError(f"Unsupported operation: {type(step).__name__}") + if getattr(step, "skip_during_simulation", False): + action += " Omitted during Opentrons simulation (PUDU behavior)." + lines.append(f"{index}. {action}") + lines.extend(("", "## Planned outputs", "")) + for sample_id in plan.output_sample_ids: + sample = samples[sample_id] + lines.append( + f"- {describe(sample_id)} — `{sample.material.identity}`; sample `{sample.id}`" + ) + return "\n".join(lines) + "\n" diff --git a/src/buildcompiler/protocols/backends/opentrons/__init__.py b/src/buildcompiler/protocols/backends/opentrons/__init__.py new file mode 100644 index 0000000..010ad0c --- /dev/null +++ b/src/buildcompiler/protocols/backends/opentrons/__init__.py @@ -0,0 +1 @@ +"""Opentrons support; the SDK is needed only in the simulation worker.""" diff --git a/src/buildcompiler/protocols/backends/opentrons/_simulation_worker.py b/src/buildcompiler/protocols/backends/opentrons/_simulation_worker.py new file mode 100644 index 0000000..5c4bb37 --- /dev/null +++ b/src/buildcompiler/protocols/backends/opentrons/_simulation_worker.py @@ -0,0 +1,133 @@ +"""Private worker, launched as a file so core imports never load the SDK. + +The trace adapter is qualified against Opentrons 8.8.2. SDK calls are +observed, not substituted; the official simulate() entry point runs both sides. +""" + +import functools +import inspect +import json +from importlib.metadata import version +from pathlib import Path + + +def main() -> None: + from opentrons import protocol_api, types + from opentrons.legacy_broker import LegacyBroker + from opentrons.protocol_api import module_contexts + from opentrons.protocol_api.disposal_locations import TrashBin + from opentrons.protocol_api.instrument_context import _Unset + from opentrons.protocol_api.protocol_context import _Unset as _ContextUnset + from opentrons.simulate import simulate + + if version("opentrons") != "8.8.2": + raise RuntimeError("The structured trace adapter requires opentrons==8.8.2.") + + def normalize(value): + if value is None or type(value) in (str, int, float, bool): + return value + if value is protocol_api.OFF_DECK: + return {"off_deck": True} + if isinstance(value, (_Unset, _ContextUnset)): + return {"unset": True} + if isinstance(value, dict): + return {key: normalize(item) for key, item in value.items()} + if isinstance(value, types.Location): + return {"point": list(value.point), "labware": str(value.labware)} + if isinstance(value, protocol_api.InstrumentContext): + return { + "instrument": value.name, + "model": value.model, + "mount": value.mount, + } + if isinstance(value, protocol_api.labware.Well): + return {"well": value.well_name, "labware": str(value.parent)} + if isinstance(value, protocol_api.labware.Labware): + return {"labware": str(value)} + if isinstance(value, TrashBin): + return {"trash": True} # Its slot is also retained in the command text. + if isinstance(value, (list, tuple)): + return [normalize(item) for item in value] + if isinstance(value, protocol_api.Liquid): + return { + "name": value.name, + "description": value.description, + "display_color": value.display_color, + } + raise TypeError(f"Unhandled simulator trace value: {type(value).__name__}") + + actions = [] + calls = [] + depth = 0 + publish = LegacyBroker.publish + + def capture(self, topic, message): + nonlocal depth + if topic == "command": + if message["$"] == "before": + if message["name"] != "command.COMMENT": + actions.append( + { + "name": message["name"], + "level": depth, + "payload": normalize(message["payload"]), + } + ) + depth += 1 + else: + depth -= 1 + return publish(self, topic, message) + + LegacyBroker.publish = capture + + def observe(cls, method_name): + original = getattr(cls, method_name) + signature = inspect.signature(original) + + @functools.wraps(original) + def wrapper(*args, **kwargs): + bound = signature.bind(*args, **kwargs) + bound.apply_defaults() + parameters = { + key: value for key, value in bound.arguments.items() if key != "self" + } + calls.append( + { + "method": f"{cls.__name__}.{method_name}", + "parameters": normalize(parameters), + } + ) + return original(*args, **kwargs) + + setattr(cls, method_name, wrapper) + + for cls, method in ( + (protocol_api.ProtocolContext, "load_module"), + (protocol_api.ProtocolContext, "load_labware"), + (protocol_api.ProtocolContext, "load_instrument"), + (protocol_api.ProtocolContext, "define_liquid"), + (module_contexts.TemperatureModuleContext, "load_labware"), + (module_contexts.ThermocyclerContext, "load_labware"), + (module_contexts.ThermocyclerContext, "execute_profile"), + (protocol_api.InstrumentContext, "touch_tip"), + (protocol_api.labware.Well, "load_liquid"), + ): + observe(cls, method) + with Path("protocol.py").open(encoding="utf-8") as source: + simulate(source) + Path("trace.json").write_text( + json.dumps( + { + "simulator_version": version("opentrons"), + "actions": actions, + "sdk_calls": calls, + }, + indent=2, + sort_keys=True, + ), + encoding="utf-8", + ) + + +if __name__ == "__main__": + main() diff --git a/src/buildcompiler/protocols/backends/opentrons/allocation.py b/src/buildcompiler/protocols/backends/opentrons/allocation.py new file mode 100644 index 0000000..4a12946 --- /dev/null +++ b/src/buildcompiler/protocols/backends/opentrons/allocation.py @@ -0,0 +1,55 @@ +"""Deterministic allocation for the supported assembly hardware profile.""" + +from buildcompiler.protocols.allocation.models import ( + AllocatedProtocolPlan, + ContainerSpec, + SamplePlacement, + WellRef, +) +from buildcompiler.protocols.allocation.wells import BLOCK_24, PLATE_96 +from buildcompiler.protocols.plans import ProtocolPlan +from buildcompiler.protocols.backends.opentrons.profile import OpentronsAssemblyProfile + + +def allocate( + plan: ProtocolPlan, *, profile: OpentronsAssemblyProfile +) -> AllocatedProtocolPlan: + if len(plan.input_sample_ids) > BLOCK_24.capacity: + raise ValueError("Assembly source block capacity exceeded.") + if ( + len(plan.output_sample_ids) + profile.thermocycler_starting_well + > PLATE_96.capacity + ): + raise ValueError("Assembly reaction plate capacity exceeded.") + placements = tuple( + SamplePlacement( + sample_id=sample_id, + location=WellRef(container_id="sources", well_name=BLOCK_24.name(index)), + ) + for index, sample_id in enumerate(plan.input_sample_ids) + ) + tuple( + SamplePlacement( + sample_id=sample_id, + location=WellRef( + container_id="reactions", + well_name=PLATE_96.name(index + profile.thermocycler_starting_well), + ), + ) + for index, sample_id in enumerate(plan.output_sample_ids) + ) + return AllocatedProtocolPlan( + protocol=plan, + containers=( + ContainerSpec( + id="sources", + definition_id="opentrons_24_aluminumblock_nest_1.5ml_snapcap", + parent="source_module", + ), + ContainerSpec( + id="reactions", + definition_id="nest_96_wellplate_100ul_pcr_full_skirt", + parent="reaction_module", + ), + ), + placements=placements, + ) diff --git a/src/buildcompiler/protocols/backends/opentrons/lowering.py b/src/buildcompiler/protocols/backends/opentrons/lowering.py new file mode 100644 index 0000000..ea9462b --- /dev/null +++ b/src/buildcompiler/protocols/backends/opentrons/lowering.py @@ -0,0 +1,110 @@ +"""Lower shared operations into explicit SDK calls, preserving PUDU ordering.""" + +from buildcompiler.protocols.backends.opentrons.operations import lower_operations + +from buildcompiler.protocols.allocation.models import AllocatedProtocolPlan +from buildcompiler.protocols.steps import ( + Transfer, +) +from buildcompiler.protocols.backends.opentrons.profile import OpentronsAssemblyProfile +from buildcompiler.protocols.backends.opentrons.program import ( + Call, + OpentronsProgram, + Reference, + SetStartingTip, +) +from buildcompiler.protocols.backends.opentrons.tips import schedule_tips + +from buildcompiler.protocols.backends.opentrons.palette import COLORS + + +def lower( + plan: AllocatedProtocolPlan, *, profile: OpentronsAssemblyProfile +) -> OpentronsProgram: + logical = plan.protocol + tips = schedule_tips( + sum(isinstance(s, Transfer) and s.new_tip for s in logical.steps), + profile=profile, + ) + instructions: list[Call | SetStartingTip] = [ + Call( + target="protocol", + method="load_module", + kwargs=( + ("module_name", "temperature module"), + ("location", profile.temperature_module_position), + ), + result="source_module", + ), + Call( + target="source_module", + method="load_labware", + args=(plan.containers[0].definition_id,), + result="sources", + ), + Call( + target="protocol", + method="load_module", + args=("thermocycler module",), + result="reaction_module", + ), + Call( + target="reaction_module", + method="load_labware", + args=(plan.containers[1].definition_id,), + result="reactions", + ), + *tips.setup, + Call( + target="protocol", + method="load_instrument", + args=("p20_single_gen2", profile.pipette_position), + kwargs=(("tip_racks", tuple(Reference(c.result) for c in tips.setup)),), + result="pipette", + ), + SetStartingTip(pipette="pipette", tip=Reference("tips_0", profile.initial_tip)), + ] + locations = { + placement.sample_id: placement.location for placement in plan.placements + } + samples = {sample.id: sample for sample in logical.samples} + for index, sample_id in enumerate(logical.input_sample_ids): + sample = samples[sample_id] + label = sample.liquid_label or sample.material.label + liquid_id = f"liquid_{index}" + instructions.extend( + ( + Call( + target="protocol", + method="define_liquid", + kwargs=( + ("name", label), + ("description", label), + ("display_color", COLORS[index % len(COLORS)]), + ), + result=liquid_id, + ), + Call( + target=f"well:{sample_id}", + method="load_liquid", + args=(Reference(liquid_id),), + kwargs=(("volume", sample.initial_volume_ul),), + ), + ) + ) + well_references = { + f"well:{sample_id}": Reference(location.container_id, location.well_name) + for sample_id, location in locations.items() + } + instructions.extend( + lower_operations( + plan, + capacities={"pipette": profile.max_volume_ul}, + pickups={"pipette": tips.pickups}, + ) + ) + return OpentronsProgram( + api_level=profile.api_level, + instructions=tuple(instructions), + wells=tuple(well_references.items()), + ) diff --git a/src/buildcompiler/protocols/backends/opentrons/operations.py b/src/buildcompiler/protocols/backends/opentrons/operations.py new file mode 100644 index 0000000..b34021b --- /dev/null +++ b/src/buildcompiler/protocols/backends/opentrons/operations.py @@ -0,0 +1,231 @@ +"""Shared lowering of liquid handling, tip lifecycle and module operations.""" + +import math +from collections.abc import Mapping + +from buildcompiler.protocols.allocation.models import AllocatedProtocolPlan +from buildcompiler.protocols.materials import SamplePoint +from buildcompiler.protocols.steps import ( + DeactivateSourceModule, + Distribute, + DropTip, + LidAction, + Mix, + OperatorAction, + PickUpTip, + RunTemperatureProgram, + SetTemperature, + Transfer, +) +from buildcompiler.protocols.backends.opentrons.program import Call, Record, Reference + + +def lower_operations( + plan: AllocatedProtocolPlan, + *, + capacities: Mapping[str, float], + pickups: Mapping[str, tuple[tuple[Call, ...], ...]] | None = None, +) -> tuple[Call, ...]: + locations = {p.sample_id: p.location for p in plan.placements} + instructions = [] + attached = {instrument: False for instrument in capacities} + tip_indices = dict.fromkeys(capacities, 0) + + def point(value: SamplePoint) -> Reference: + location = locations[value.sample_id] + return Reference( + location.container_id, + location.well_name, + value.bottom_mm, + value.top_mm, + value.track_conical_height, + ) + + def check_volume(instrument: str, *volumes: float) -> None: + for volume in volumes: + if not math.isfinite(volume) or not 0 <= volume <= capacities[instrument]: + raise ValueError( + f"Operation exceeds supported {instrument} pipette capacity." + ) + + def pickup(instrument: str) -> None: + if attached[instrument]: + raise ValueError(f"A tip is already attached to {instrument}.") + if pickups is None or instrument not in pickups: + instructions.append(Call(target=instrument, method="pick_up_tip")) + else: + instructions.extend(pickups[instrument][tip_indices[instrument]]) + tip_indices[instrument] += 1 + attached[instrument] = True + + def require_tip(instrument: str) -> None: + if not attached[instrument]: + raise ValueError(f"An attached tip is required on {instrument}.") + + def drop(instrument: str) -> None: + require_tip(instrument) + instructions.append(Call(target=instrument, method="drop_tip")) + attached[instrument] = False + + for step in plan.protocol.steps: + if isinstance(step, Transfer): + instrument = step.instrument + volume = ( + min(step.volume_ul, capacities[instrument]) + if step.limit_to_pipette_capacity + else step.volume_ul + ) + check_volume(instrument, volume, step.mix_before_ul) + source, destination = point(step.source), point(step.destination) + if step.new_tip: + pickup(instrument) + require_tip(instrument) + if step.mix_before_ul: + instructions.append( + Call( + target=instrument, + method="mix", + args=(step.mix_repetitions, step.mix_before_ul, source), + ) + ) + instructions.extend( + ( + Call( + target=instrument, + method="aspirate", + args=(volume, source), + kwargs=(("rate", step.aspiration_rate),), + ), + Call( + target=instrument, + method="dispense", + args=(volume, destination), + kwargs=(("rate", step.dispense_rate),), + ), + ) + ) + if step.blow_out: + instructions.append(Call(target=instrument, method="blow_out")) + if step.touch_tip: + instructions.append( + Call( + target=instrument, + method="touch_tip", + kwargs=(("radius", 0.5), ("v_offset", -14), ("speed", 20)), + ) + ) + if step.drop_tip: + drop(instrument) + elif isinstance(step, PickUpTip): + pickup(step.instrument) + elif isinstance(step, DropTip): + drop(step.instrument) + elif isinstance(step, Mix): + require_tip(step.instrument) + check_volume(step.instrument, step.volume_ul) + instructions.append( + Call( + target=step.instrument, + method="mix", + kwargs=( + ("repetitions", step.repetitions), + ("volume", step.volume_ul), + ("location", point(step.location)), + ), + ) + ) + elif isinstance(step, Distribute): + check_volume( + step.instrument, + step.volume_ul + step.disposal_volume_ul + (step.air_gap_ul or 0), + ) + if step.mix_before: + check_volume(step.instrument, step.mix_before[1]) + if step.new_tip == "never": + require_tip(step.instrument) + elif step.new_tip != "once" or attached[step.instrument]: + raise ValueError( + "A distribution with new_tip='once' requires an empty pipette." + ) + kwargs = [ + ("volume", step.volume_ul), + ("source", point(step.source)), + ("dest", tuple(point(d) for d in step.destinations)), + ("disposal_volume", step.disposal_volume_ul), + ("new_tip", step.new_tip), + ] + if step.mix_before: + kwargs.append(("mix_before", step.mix_before)) + if step.air_gap_ul is not None: + kwargs.append(("air_gap", step.air_gap_ul)) + instructions.append( + Call(target=step.instrument, method="distribute", kwargs=tuple(kwargs)) + ) + if step.track_liquid_sample_id: + for destination in step.destinations: + instructions.append( + Call( + target=f"well:{destination.sample_id}", + method="load_liquid", + kwargs=( + ( + "liquid", + Reference(f"liquid:{step.track_liquid_sample_id}"), + ), + ("volume", step.volume_ul), + ), + ) + ) + elif isinstance(step, LidAction): + instructions.append( + Call(target="reaction_module", method=f"{step.action}_lid") + ) + elif isinstance(step, SetTemperature): + target, method = { + "source": ("source_module", "set_temperature"), + "reaction": ("reaction_module", "set_block_temperature"), + "lid": ("reaction_module", "set_lid_temperature"), + }[step.module] + instructions.append( + Call( + target=target, + method=method, + args=(step.celsius,), + skip_during_simulation=step.skip_during_simulation, + ) + ) + elif isinstance(step, DeactivateSourceModule): + instructions.append(Call(target="source_module", method="deactivate")) + elif isinstance(step, RunTemperatureProgram): + instructions.append( + Call( + target="reaction_module", + method="execute_profile", + kwargs=( + ( + "steps", + tuple( + Record( + ( + ("temperature", s.celsius), + ("hold_time_minutes", s.minutes), + ) + ) + for s in step.program.steps + ), + ), + ("repetitions", step.program.repetitions), + ("block_max_volume", step.program.block_max_volume_ul), + ), + skip_during_simulation=step.skip_during_simulation, + ) + ) + elif isinstance(step, OperatorAction): + instructions.append( + Call(target="protocol", method="comment", args=(step.instruction,)) + ) + else: + raise TypeError(f"Unsupported protocol operation: {type(step).__name__}") + if any(attached.values()): + raise ValueError("Protocol ends with an attached tip.") + return tuple(instructions) diff --git a/src/buildcompiler/protocols/backends/opentrons/palette.py b/src/buildcompiler/protocols/backends/opentrons/palette.py new file mode 100644 index 0000000..eedfb05 --- /dev/null +++ b/src/buildcompiler/protocols/backends/opentrons/palette.py @@ -0,0 +1,29 @@ +"""Shared display metadata, independent of sample identity.""" + +# PUDU display palette (MIT, Rudge Lab); display metadata never controls identity. +COLORS = ( + "#4040BF", + "#BF4040", + "#40BF40", + "#A640BF", + "#BFBF40", + "#BF7340", + "#40BFBF", + "#BF40A6", + "#73BF40", + "#4073BF", + "#BF8C40", + "#40BF73", + "#7340BF", + "#A6BF40", + "#BF5940", + "#40A6BF", + "#BF4073", + "#59BF40", + "#BFA640", + "#40BFA6", + "#8CBF40", + "#40BF59", + "#40BF8C", + "#BF40A6", +) diff --git a/src/buildcompiler/protocols/backends/opentrons/plating.py b/src/buildcompiler/protocols/backends/opentrons/plating.py new file mode 100644 index 0000000..feb667e --- /dev/null +++ b/src/buildcompiler/protocols/backends/opentrons/plating.py @@ -0,0 +1,191 @@ +"""Plating resource allocation and setup, using shared liquid handling lowering.""" + +from collections import Counter + +from buildcompiler.protocols.allocation.models import ( + AllocatedProtocolPlan, + ContainerSpec, + OutputManifest, + SamplePlacement, + WellRef, +) +from buildcompiler.protocols.allocation.wells import PLATE_96, WellGrid +from buildcompiler.protocols.plans import ProtocolPlan +from buildcompiler.protocols.backends.opentrons.palette import COLORS +from buildcompiler.protocols.backends.opentrons.operations import lower_operations +from buildcompiler.protocols.backends.opentrons.profile import OpentronsPlatingProfile +from buildcompiler.protocols.backends.opentrons.program import ( + Call, + OpentronsProgram, + Reference, + SetStartingTip, +) +from buildcompiler.protocols.backends.opentrons.setup import ( + load_liquid, + validate_tip_capacity, + well_aliases, +) + + +def allocate_plating( + plan: ProtocolPlan, *, profile: OpentronsPlatingProfile, inputs: OutputManifest +) -> AllocatedProtocolPlan: + upstream = {p.sample_id: p.location for p in inputs.placements} + selected = [s for s in plan.samples if s.role == "bacteria"] + if len({upstream[s.source_sample_id].container_id for s in selected}) != 1: + raise ValueError("The plating target accepts one source plate.") + containers = [ + ContainerSpec( + id="reactions", + definition_id=profile.thermocycler_labware, + parent="reaction_module", + ), + ContainerSpec( + id="tube_rack", + definition_id="opentrons_15_tuberack_falcon_15ml_conical", + parent=profile.tube_rack_position, + ), + ] + counts = Counter((s.role, s.dilution) for s in plan.samples) + indices = Counter() + placements = [] + for sample in plan.samples: + if sample.role == "broth": + location = WellRef( + container_id="tube_rack", + well_name=WellGrid(rows=3, columns=5).name(profile.lb_tube_position), + ) + if sample.initial_volume_ul > 15000: + raise ValueError("Broth loading exceeds tube capacity.") + elif sample.role == "bacteria": + well = upstream[sample.source_sample_id].well_name + PLATE_96.index(well) + location = WellRef(container_id="reactions", well_name=well) + elif sample.role in ("dilution", "agar"): + per_dilution = counts[sample.role, sample.dilution] + if per_dilution > 96: + raise ValueError("A dilution exceeds plate capacity.") + two_plates = counts[sample.role, 2] and per_dilution > 48 + plate = sample.dilution if two_plates else 1 + index = indices[sample.role, sample.dilution] + indices[sample.role, sample.dilution] += 1 + if sample.dilution == 2 and not two_plates: + index += 48 + name = f"{sample.role}_{plate}" + if name not in {c.id for c in containers}: + containers.append( + ContainerSpec( + id=name, + definition_id="nest_96_wellplate_100ul_pcr_full_skirt", + parent=getattr(profile, f"{sample.role}_plate_position{plate}"), + ) + ) + location = WellRef(container_id=name, well_name=PLATE_96.name(index)) + else: + raise ValueError(f"Unknown plating sample role {sample.role!r}.") + placements.append(SamplePlacement(sample_id=sample.id, location=location)) + return AllocatedProtocolPlan( + protocol=plan, containers=tuple(containers), placements=tuple(placements) + ) + + +def lower_plating( + plan: AllocatedProtocolPlan, *, profile: OpentronsPlatingProfile +) -> OpentronsProgram: + validate_tip_capacity( + plan, + starts={"small": profile.initial_small_tip, "large": profile.initial_large_tip}, + ) + calls = [ + Call( + target="protocol", + method="load_module", + args=("thermocyclerModuleV1",), + result="reaction_module", + ), + Call( + target="reaction_module", + method="load_labware", + args=(profile.thermocycler_labware,), + result="reactions", + ), + Call( + target="protocol", + method="load_labware", + args=("opentrons_96_filtertiprack_20ul", profile.small_tiprack_position), + result="small_tips", + ), + Call( + target="protocol", + method="load_labware", + args=("opentrons_96_filtertiprack_200ul", profile.large_tiprack_position), + result="large_tips", + ), + ] + for instrument, pipette, mount, tip in ( + ("small", "p20_single_gen2", "left", profile.initial_small_tip), + ("large", "p300_single_gen2", "right", profile.initial_large_tip), + ): + calls.append( + Call( + target="protocol", + method="load_instrument", + args=(pipette, mount), + kwargs=(("tip_racks", (Reference(f"{instrument}_tips"),)),), + result=instrument, + ) + ) + if tip: + calls.append( + SetStartingTip( + pipette=instrument, tip=Reference(f"{instrument}_tips", tip) + ) + ) + tube = next(c for c in plan.containers if c.id == "tube_rack") + calls.append( + Call( + target="protocol", + method="load_labware", + args=(tube.definition_id, tube.parent), + result=tube.id, + ) + ) + bacteria_index = 0 + for sample in plan.protocol.samples: + if sample.role == "broth": + calls.extend( + load_liquid( + sample.id, + name="liquid_broth", + description="Liquid broth for dilutions", + color="#D2B48C", + volume=sample.initial_volume_ul, + ) + ) + elif sample.role == "bacteria": + calls.extend( + load_liquid( + sample.id, + name="transformed_bacteria", + description=sample.liquid_label, + color=COLORS[bacteria_index % len(COLORS)], + volume=sample.initial_volume_ul, + ) + ) + bacteria_index += 1 + for container in sorted( + (c for c in plan.containers if c.id.startswith(("dilution_", "agar_"))), + key=lambda c: (not c.id.startswith("dilution_"), c.id), + ): + calls.append( + Call( + target="protocol", + method="load_labware", + args=(container.definition_id, container.parent), + result=container.id, + ) + ) + calls.extend(lower_operations(plan, capacities={"small": 20, "large": 200})) + return OpentronsProgram( + api_level=profile.api_level, instructions=tuple(calls), wells=well_aliases(plan) + ) diff --git a/src/buildcompiler/protocols/backends/opentrons/profile.py b/src/buildcompiler/protocols/backends/opentrons/profile.py new file mode 100644 index 0000000..96d8eea --- /dev/null +++ b/src/buildcompiler/protocols/backends/opentrons/profile.py @@ -0,0 +1,131 @@ +"""The initial, explicitly supported PUDU assembly hardware profile.""" + +from dataclasses import dataclass + +from buildcompiler.protocols.allocation.wells import PLATE_96 + + +@dataclass(frozen=True, slots=True, kw_only=True) +class OpentronsAssemblyProfile: + api_level: str = "2.21" + temperature_module_position: str = "1" + thermocycler_starting_well: int = 0 + tiprack_positions: tuple[str, ...] = ("2", "3", "4", "5", "6", "9") + pipette_position: str = "left" + initial_tip: str = "A1" + + def __post_init__(self) -> None: + if self.api_level != "2.21": + raise ValueError("This backend currently supports API 2.21.") + if self.pipette_position not in ("left", "right"): + raise ValueError("Invalid pipette mount.") + PLATE_96.name(self.thermocycler_starting_well) + PLATE_96.index(self.initial_tip) + if not isinstance(self.tiprack_positions, tuple) or not self.tiprack_positions: + raise ValueError("tiprack_positions must be a nonempty tuple.") + slots = (self.temperature_module_position, *self.tiprack_positions) + if len(set(slots)) != len(slots) or not set(slots) <= { + "1", + "2", + "3", + "4", + "5", + "6", + "9", + }: + raise ValueError("Deck slots collide or overlap the thermocycler/trash.") + + @property + def max_volume_ul(self) -> float: + return 20 + + +def validate_slots(slots: tuple[str, ...]) -> None: + if len(set(slots)) != len(slots) or not set(slots) <= { + "1", + "2", + "3", + "4", + "5", + "6", + "9", + }: + raise ValueError("Deck slots collide or overlap the thermocycler/trash.") + + +@dataclass(frozen=True, slots=True, kw_only=True) +class OpentronsTransformationProfile: + api_level: str = "2.21" + temperature_module_position: str = "1" + dna_plate_position: str = "2" + tube_rack_position: str = "3" + tiprack_p20_position: str = "9" + tiprack_p200_position: str = "6" + thermocycler_starting_well: int = 0 + initial_dna_well: int = 0 + initial_tip_p20: str | None = None + initial_tip_p300: str | None = None + + def __post_init__(self) -> None: + from buildcompiler.protocols.allocation.wells import BLOCK_24 + + if self.api_level != "2.21": + raise ValueError("This backend currently supports API 2.21.") + PLATE_96.name(self.thermocycler_starting_well) + BLOCK_24.name(self.initial_dna_well) + for tip in (self.initial_tip_p20, self.initial_tip_p300): + if tip is not None: + PLATE_96.index(tip) + validate_slots( + ( + self.temperature_module_position, + self.dna_plate_position, + self.tube_rack_position, + self.tiprack_p20_position, + self.tiprack_p200_position, + ) + ) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class OpentronsPlatingProfile: + api_level: str = "2.21" + thermocycler_labware: str = "biorad_96_wellplate_200ul_pcr" + small_tiprack_position: str = "9" + large_tiprack_position: str = "1" + initial_small_tip: str | None = None + initial_large_tip: str | None = None + dilution_plate_position1: str = "2" + dilution_plate_position2: str = "3" + agar_plate_position1: str = "5" + agar_plate_position2: str = "6" + tube_rack_position: str = "4" + lb_tube_position: int = 0 + + def __post_init__(self) -> None: + if self.api_level != "2.21": + raise ValueError("This backend currently supports API 2.21.") + if self.thermocycler_labware not in ( + "biorad_96_wellplate_200ul_pcr", + "nest_96_wellplate_100ul_pcr_full_skirt", + ): + raise ValueError("Unsupported plating source labware.") + if ( + type(self.lb_tube_position) is not int + or not 0 <= self.lb_tube_position < 15 + ): + raise ValueError("lb_tube_position must index the 15-tube rack.") + for tip in (self.initial_small_tip, self.initial_large_tip): + if tip is not None: + PLATE_96.index(tip) + validate_slots( + ( + self.small_tiprack_position, + self.large_tiprack_position, + self.dilution_plate_position1, + self.dilution_plate_position2, + self.agar_plate_position1, + self.agar_plate_position2, + self.tube_rack_position, + ) + ) diff --git a/src/buildcompiler/protocols/backends/opentrons/program.py b/src/buildcompiler/protocols/backends/opentrons/program.py new file mode 100644 index 0000000..ede2aa9 --- /dev/null +++ b/src/buildcompiler/protocols/backends/opentrons/program.py @@ -0,0 +1,40 @@ +"""Immutable SDK call records produced by lowering; never live SDK objects.""" + +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class Reference: + name: str + well: str | None = None + bottom_mm: float | None = None + top_mm: float | None = None + track_conical_height: bool = False + + +@dataclass(frozen=True, slots=True) +class Record: + entries: tuple[tuple[str, object], ...] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Call: + target: str + method: str + args: tuple = () + kwargs: tuple[tuple[str, object], ...] = () + result: str | None = None + skip_during_simulation: bool = False + + +@dataclass(frozen=True, slots=True, kw_only=True) +class SetStartingTip: + pipette: str + tip: Reference + + +@dataclass(frozen=True, slots=True, kw_only=True) +class OpentronsProgram: + api_level: str + instructions: tuple[Call | SetStartingTip, ...] + wells: tuple[tuple[str, Reference], ...] = () diff --git a/src/buildcompiler/protocols/backends/opentrons/rendering.py b/src/buildcompiler/protocols/backends/opentrons/rendering.py new file mode 100644 index 0000000..a3a7bbb --- /dev/null +++ b/src/buildcompiler/protocols/backends/opentrons/rendering.py @@ -0,0 +1,120 @@ +"""Emit standalone Python with literal data and a closed set of SDK methods.""" + +import keyword +import math + +from buildcompiler.protocols.attribution import PUDU_LICENSE +from buildcompiler.protocols.backends.opentrons.program import ( + Call, + OpentronsProgram, + Record, + Reference, + SetStartingTip, +) + +METHODS = frozenset( + { + "load_module", + "load_labware", + "load_instrument", + "define_liquid", + "load_liquid", + "pick_up_tip", + "drop_tip", + "mix", + "aspirate", + "dispense", + "distribute", + "blow_out", + "touch_tip", + "open_lid", + "close_lid", + "set_temperature", + "set_block_temperature", + "set_lid_temperature", + "deactivate", + "execute_profile", + "comment", + "move_labware", + } +) + + +def _literal(value: object) -> str: + if isinstance(value, Reference): + expression = f"resources[{value.name!r}]" + if value.well is not None: + expression += f"[{value.well!r}]" + if value.bottom_mm is not None: + expression += f".bottom({_literal(value.bottom_mm)})" + if value.top_mm is not None: + expression += f".top({_literal(value.top_mm)})" + if value.track_conical_height: + expression = f"_conical_source({expression})" + return expression + if isinstance(value, Record): + return ( + "{" + + ", ".join(f"{key!r}: {_literal(item)}" for key, item in value.entries) + + "}" + ) + if isinstance(value, tuple): + return "[" + ", ".join(_literal(item) for item in value) + "]" + if value is None or type(value) in (str, bool, int): + return repr(value) + if type(value) is float and math.isfinite(value): + return repr(value) + raise TypeError(f"Unsupported program literal: {type(value).__name__}") + + +def render_python(program: OpentronsProgram, *, name: str) -> str: + """Return source; importing or calling this function performs no SDK work.""" + metadata = { + "protocolName": name, + "author": "BuildCompiler", + "apiLevel": program.api_level, + } + lines = [ + *(f"# {line}" for line in PUDU_LICENSE.splitlines()), + "# Generated by BuildCompiler. Protocol behavior derived from PUDU (MIT).", + "from opentrons import protocol_api", + "", + f"metadata = {metadata!r}", + "", + "def _conical_source(well):", + " # PUDU SmartPipette's conical-tube policy; requires API liquid tracking.", + " try:", + " volume = well.current_liquid_volume()", + " if volume is None or volume < well.max_volume * 0.2:", + " return well", + " return well.bottom(max((volume / well.max_volume) * (well.depth - 10) - 10, 3))", + " except Exception:", + " return well", + "", + "def run(protocol: protocol_api.ProtocolContext):", + " resources = {'protocol': protocol, 'OFF_DECK': protocol_api.OFF_DECK}", + ] + wells = dict(program.wells) + for instruction in program.instructions: + if isinstance(instruction, SetStartingTip): + lines.append( + f" resources[{instruction.pipette!r}].starting_tip = {_literal(instruction.tip)}" + ) + continue + if not isinstance(instruction, Call) or instruction.method not in METHODS: + raise ValueError("Unsupported SDK instruction.") + arguments = [_literal(value) for value in instruction.args] + for key, value in instruction.kwargs: + if not key.isidentifier() or keyword.iskeyword(key): + raise ValueError(f"Invalid keyword: {key!r}") + arguments.append(f"{key}={_literal(value)}") + target = _literal(wells.get(instruction.target, Reference(instruction.target))) + expression = f"{target}.{instruction.method}({', '.join(arguments)})" + if instruction.result is not None: + expression = f"resources[{instruction.result!r}] = {expression}" + if instruction.skip_during_simulation: + lines.append(" if not protocol.is_simulating():") + lines.append(f" {expression}") + else: + lines.append(f" {expression}") + return "\n".join(lines) + "\n" diff --git a/src/buildcompiler/protocols/backends/opentrons/setup.py b/src/buildcompiler/protocols/backends/opentrons/setup.py new file mode 100644 index 0000000..6358a63 --- /dev/null +++ b/src/buildcompiler/protocols/backends/opentrons/setup.py @@ -0,0 +1,71 @@ +"""Shared resource loading and validation for Opentrons targets.""" + +from buildcompiler.protocols.allocation.models import AllocatedProtocolPlan +from buildcompiler.protocols.allocation.wells import PLATE_96 +from buildcompiler.protocols.steps import Distribute, PickUpTip, Transfer +from buildcompiler.protocols.backends.opentrons.program import Call, Reference + + +def well_aliases(plan: AllocatedProtocolPlan) -> tuple[tuple[str, Reference], ...]: + return tuple( + ( + f"well:{p.sample_id}", + Reference(p.location.container_id, p.location.well_name), + ) + for p in plan.placements + ) + + +def load_liquid( + sample_id: str, + *, + name: str, + color: str, + volume: float, + description: str | None = None, + liquid_id: str | None = None, + define: bool = True, +) -> tuple[Call, ...]: + liquid_id = liquid_id or f"liquid:{sample_id}" + calls = [] + if define: + kwargs = [("name", name), ("display_color", color)] + if description is not None: + kwargs.append(("description", description)) + calls.append( + Call( + target="protocol", + method="define_liquid", + kwargs=tuple(kwargs), + result=liquid_id, + ) + ) + calls.append( + Call( + target=f"well:{sample_id}", + method="load_liquid", + kwargs=(("liquid", Reference(liquid_id)), ("volume", volume)), + ) + ) + return tuple(calls) + + +def validate_tip_capacity( + plan: AllocatedProtocolPlan, *, starts: dict[str, str | None] +) -> None: + counts = dict.fromkeys(starts, 0) + for step in plan.protocol.steps: + if ( + isinstance(step, PickUpTip) + or isinstance(step, Transfer) + and step.new_tip + or isinstance(step, Distribute) + and step.new_tip == "once" + ): + counts[step.instrument] += 1 + for instrument, count in counts.items(): + available = 96 - PLATE_96.index(starts[instrument] or "A1") + if count > available: + raise ValueError( + f"{instrument} requires {count} tips but only {available} remain." + ) diff --git a/src/buildcompiler/protocols/backends/opentrons/simulation.py b/src/buildcompiler/protocols/backends/opentrons/simulation.py new file mode 100644 index 0000000..fcc7f4f --- /dev/null +++ b/src/buildcompiler/protocols/backends/opentrons/simulation.py @@ -0,0 +1,80 @@ +"""Run the real simulator in an isolated process and temporary working directory.""" + +import json +import os +import subprocess +import sys +import tempfile +from dataclasses import dataclass +from pathlib import Path + + +class SimulationError(RuntimeError): + """The actual simulator failed or did not produce a trace.""" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class SimulationResult: + simulator_version: str + actions: tuple[dict, ...] + sdk_calls: tuple[dict, ...] + generated_json: dict[str, object] + stdout: str + stderr: str + + +def simulate_source( + source: str, + *, + python: str | Path = sys.executable, + python_paths: tuple[str | Path, ...] = (), + timeout: float = 120, +) -> SimulationResult: + """Simulate explicitly supplied source; never called during compilation. + + Source must be trusted just like any Python protocol. Process isolation + prevents SDK state and PUDU's simulation file writes leaking between runs; + it is not a security sandbox for arbitrary Python. + """ + worker = Path(__file__).with_name("_simulation_worker.py") + with tempfile.TemporaryDirectory(prefix="buildcompiler-simulation-") as temporary: + directory = Path(temporary) + (directory / "protocol.py").write_text(source, encoding="utf-8") + environment = os.environ.copy() + environment["OT_API_CONFIG_DIR"] = str(directory / "config") + environment["PYTHONDONTWRITEBYTECODE"] = "1" + if python_paths: + environment["PYTHONPATH"] = os.pathsep.join( + str(Path(p).resolve()) for p in python_paths + ) + try: + process = subprocess.run( + [str(python), str(worker)], + cwd=directory, + env=environment, + capture_output=True, + text=True, + timeout=timeout, + check=False, + ) + except (OSError, subprocess.TimeoutExpired) as exc: + raise SimulationError(f"Could not run Opentrons simulator: {exc}") from exc + result_path = directory / "trace.json" + if process.returncode or not result_path.exists(): + raise SimulationError( + f"Opentrons simulation failed ({process.returncode}):\n{process.stderr}\n{process.stdout}" + ) + data = json.loads(result_path.read_text(encoding="utf-8")) + generated = { + path.name: json.loads(path.read_text(encoding="utf-8")) + for path in directory.glob("*.json") + if path.name != "trace.json" + } + return SimulationResult( + simulator_version=data["simulator_version"], + actions=tuple(data["actions"]), + sdk_calls=tuple(data["sdk_calls"]), + generated_json=generated, + stdout=process.stdout, + stderr=process.stderr, + ) diff --git a/src/buildcompiler/protocols/backends/opentrons/tips.py b/src/buildcompiler/protocols/backends/opentrons/tips.py new file mode 100644 index 0000000..2e43118 --- /dev/null +++ b/src/buildcompiler/protocols/backends/opentrons/tips.py @@ -0,0 +1,76 @@ +"""Explicit tip schedules and rack replacement boundaries.""" + +from dataclasses import dataclass + +from buildcompiler.protocols.allocation.wells import PLATE_96 +from buildcompiler.protocols.backends.opentrons.profile import OpentronsAssemblyProfile +from buildcompiler.protocols.backends.opentrons.program import Call, Reference + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TipSchedule: + setup: tuple[Call, ...] + pickups: tuple[tuple[Call, ...], ...] + + +def schedule_tips(count: int, *, profile: OpentronsAssemblyProfile) -> TipSchedule: + """Assign exact tips, accounting for a partially used first rack.""" + first = PLATE_96.index(profile.initial_tip) + rack_count = (first + count + 95) // 96 + slots = profile.tiprack_positions + setup = tuple( + Call( + target="protocol", + method="load_labware", + args=( + "opentrons_96_tiprack_20ul", + slots[index] if index < len(slots) else Reference("OFF_DECK"), + ), + result=f"tips_{index}", + ) + for index in range(rack_count) + ) + pickups = [] + active_batch = 0 + for index in range(first, first + count): + rack, well = divmod(index, 96) + batch = rack // len(slots) + calls = [] + if batch != active_batch: + for old in range( + active_batch * len(slots), + min((active_batch + 1) * len(slots), rack_count), + ): + calls.append( + Call( + target="protocol", + method="move_labware", + kwargs=( + ("labware", Reference(f"tips_{old}")), + ("new_location", Reference("OFF_DECK")), + ), + ) + ) + for new in range( + batch * len(slots), min((batch + 1) * len(slots), rack_count) + ): + calls.append( + Call( + target="protocol", + method="move_labware", + kwargs=( + ("labware", Reference(f"tips_{new}")), + ("new_location", slots[new % len(slots)]), + ), + ) + ) + active_batch = batch + calls.append( + Call( + target="pipette", + method="pick_up_tip", + args=(Reference(f"tips_{rack}", PLATE_96.name(well)),), + ) + ) + pickups.append(tuple(calls)) + return TipSchedule(setup=setup, pickups=tuple(pickups)) diff --git a/src/buildcompiler/protocols/backends/opentrons/transformation.py b/src/buildcompiler/protocols/backends/opentrons/transformation.py new file mode 100644 index 0000000..c0e575f --- /dev/null +++ b/src/buildcompiler/protocols/backends/opentrons/transformation.py @@ -0,0 +1,220 @@ +"""Transformation allocation and resource setup; method ordering lives elsewhere.""" + +from buildcompiler.protocols.allocation.models import ( + AllocatedProtocolPlan, + ContainerSpec, + OutputManifest, + SamplePlacement, + WellRef, +) +from buildcompiler.protocols.allocation.wells import BLOCK_24, PLATE_96 +from buildcompiler.protocols.plans import ProtocolPlan +from buildcompiler.protocols.backends.opentrons.palette import COLORS +from buildcompiler.protocols.backends.opentrons.operations import lower_operations +from buildcompiler.protocols.backends.opentrons.profile import ( + OpentronsTransformationProfile, +) +from buildcompiler.protocols.backends.opentrons.program import ( + Call, + OpentronsProgram, + Reference, + SetStartingTip, +) +from buildcompiler.protocols.backends.opentrons.setup import ( + load_liquid, + validate_tip_capacity, + well_aliases, +) + + +def allocate_transformation( + plan: ProtocolPlan, + *, + profile: OpentronsTransformationProfile, + inputs: OutputManifest | None = None, +) -> AllocatedProtocolPlan: + containers = [ + ContainerSpec( + id="sources", + definition_id="opentrons_24_aluminumblock_nest_1.5ml_snapcap", + parent="source_module", + ), + ContainerSpec( + id="reactions", + definition_id="nest_96_wellplate_100ul_pcr_full_skirt", + parent="reaction_module", + ), + ] + upstream = {} + if inputs is not None: + if len({p.location.container_id for p in inputs.placements}) != 1: + raise ValueError("The transformation target accepts one source DNA plate.") + containers.append( + ContainerSpec( + id="dna_plate", + definition_id="nest_96_wellplate_100ul_pcr_full_skirt", + parent=profile.dna_plate_position, + ) + ) + upstream = {p.sample_id: p.location for p in inputs.placements} + containers.append( + ContainerSpec( + id="tube_rack", + definition_id="opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap", + parent=profile.tube_rack_position, + ) + ) + placements = [] + dna_index, tube_index, reaction_index = ( + profile.initial_dna_well, + 0, + profile.thermocycler_starting_well, + ) + for sample in plan.samples: + if sample.role == "dna": + if inputs is None: + location = WellRef( + container_id="sources", well_name=BLOCK_24.name(dna_index) + ) + dna_index += 1 + else: + well = upstream[sample.source_sample_id].well_name + PLATE_96.index(well) + location = WellRef(container_id="dna_plate", well_name=well) + elif sample.role in ("cells", "media"): + location = WellRef( + container_id="tube_rack", well_name=BLOCK_24.name(tube_index) + ) + tube_index += 1 + if sample.initial_volume_ul > 1500: + raise ValueError("Reagent loading exceeds tube capacity.") + elif sample.role == "reaction": + location = WellRef( + container_id="reactions", well_name=PLATE_96.name(reaction_index) + ) + reaction_index += 1 + else: + raise ValueError(f"Unknown transformation sample role {sample.role!r}.") + placements.append(SamplePlacement(sample_id=sample.id, location=location)) + return AllocatedProtocolPlan( + protocol=plan, containers=tuple(containers), placements=tuple(placements) + ) + + +def lower_transformation( + plan: AllocatedProtocolPlan, *, profile: OpentronsTransformationProfile +) -> OpentronsProgram: + validate_tip_capacity( + plan, + starts={"small": profile.initial_tip_p20, "large": profile.initial_tip_p300}, + ) + containers = {c.id: c for c in plan.containers} + calls = [ + Call( + target="protocol", + method="load_module", + args=("temperature module", profile.temperature_module_position), + result="source_module", + ), + Call( + target="source_module", + method="load_labware", + args=(containers["sources"].definition_id,), + result="sources", + ), + Call( + target="protocol", + method="load_module", + args=("thermocycler module",), + result="reaction_module", + ), + Call( + target="reaction_module", + method="load_labware", + args=(containers["reactions"].definition_id,), + result="reactions", + ), + ] + for name in ("dna_plate", "tube_rack"): + if name in containers: + container = containers[name] + calls.append( + Call( + target="protocol", + method="load_labware", + args=(container.definition_id, container.parent), + result=name, + ) + ) + calls.extend( + ( + Call( + target="protocol", + method="load_labware", + args=("opentrons_96_tiprack_20ul", profile.tiprack_p20_position), + result="small_tips", + ), + Call( + target="protocol", + method="load_labware", + args=( + "opentrons_96_filtertiprack_200ul", + profile.tiprack_p200_position, + ), + result="large_tips", + ), + ) + ) + for instrument, pipette, mount, tip in ( + ("small", "p20_single_gen2", "left", profile.initial_tip_p20), + ("large", "p300_single_gen2", "right", profile.initial_tip_p300), + ): + calls.append( + Call( + target="protocol", + method="load_instrument", + args=(pipette, mount), + kwargs=(("tip_racks", (Reference(f"{instrument}_tips"),)),), + result=instrument, + ) + ) + if tip: + calls.append( + SetStartingTip( + pipette=instrument, tip=Reference(f"{instrument}_tips", tip) + ) + ) + dna_liquids = {} + for sample in plan.protocol.samples: + if sample.role == "dna": + define = sample.material.identity not in dna_liquids + if define: + dna_liquids[sample.material.identity] = ( + f"liquid:{sample.id}", + len(dna_liquids), + ) + liquid_id, color_index = dna_liquids[sample.material.identity] + calls.extend( + load_liquid( + sample.id, + name=sample.material.label, + description=f"{sample.material.label} DNA construct", + color=COLORS[color_index % len(COLORS)], + volume=sample.initial_volume_ul, + liquid_id=liquid_id, + define=define, + ) + ) + elif sample.role in ("cells", "media"): + calls.extend( + load_liquid( + sample.id, + name=sample.liquid_label, + color=COLORS[(sample.replicate - 1) % len(COLORS)], + volume=sample.initial_volume_ul, + ) + ) + calls.extend(lower_operations(plan, capacities={"small": 20, "large": 200})) + return OpentronsProgram( + api_level=profile.api_level, instructions=tuple(calls), wells=well_aliases(plan) + ) diff --git a/src/buildcompiler/protocols/compiler.py b/src/buildcompiler/protocols/compiler.py new file mode 100644 index 0000000..fd491ed --- /dev/null +++ b/src/buildcompiler/protocols/compiler.py @@ -0,0 +1,190 @@ +"""Compilation facade; planning, rendering, writing and simulation are separate.""" + +import json +from dataclasses import asdict, dataclass + +from buildcompiler.domain.protocol_requests import ( + AssemblyRequest, + TransformationRequest, + PlatingRequest, + ProtocolRequest, +) + +from buildcompiler.protocols.allocation.models import ( + AllocatedProtocolPlan, + OutputManifest, +) +from buildcompiler.protocols.allocation.plating import plating_layout +from buildcompiler.protocols.artifacts import ArtifactBundle, ProtocolArtifact +from buildcompiler.protocols.attribution import PUDU_REVISION +from buildcompiler.protocols.backends.markdown import render_markdown +from buildcompiler.protocols.backends.opentrons.allocation import allocate +from buildcompiler.protocols.backends.opentrons.lowering import lower +from buildcompiler.protocols.backends.opentrons.profile import ( + OpentronsAssemblyProfile, + OpentronsTransformationProfile, + OpentronsPlatingProfile, +) +from buildcompiler.protocols.backends.opentrons.plating import ( + allocate_plating, + lower_plating, +) +from buildcompiler.protocols.backends.opentrons.transformation import ( + allocate_transformation, + lower_transformation, +) +from buildcompiler.protocols.backends.opentrons.program import OpentronsProgram +from buildcompiler.protocols.backends.opentrons.rendering import render_python +from buildcompiler.protocols.config import AssemblyConfig +from buildcompiler.protocols.methods.assembly import plan_assembly +from buildcompiler.protocols.methods.transformation import plan_transformation +from buildcompiler.protocols.methods.transformation_config import TransformationConfig +from buildcompiler.protocols.methods.plating import plan_plating +from buildcompiler.protocols.methods.plating_config import PlatingConfig +from buildcompiler.protocols.plans import ProtocolPlan + + +@dataclass(frozen=True, slots=True, kw_only=True) +class CompiledProtocol: + allocation: AllocatedProtocolPlan + program: OpentronsProgram + script: str + markdown: str + manifest: OutputManifest + artifacts: ArtifactBundle + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ProtocolCompiler: + assembly: AssemblyConfig = AssemblyConfig() + transformation: TransformationConfig = TransformationConfig() + plating: PlatingConfig = PlatingConfig() + + def plan( + self, + request: ProtocolRequest, + *, + inputs: OutputManifest | None = None, + ) -> ProtocolPlan: + if isinstance(request, AssemblyRequest): + if inputs is not None: + raise ValueError( + "Assembly does not accept an upstream output manifest." + ) + return plan_assembly(request, config=self.assembly) + if isinstance(request, TransformationRequest): + return plan_transformation( + request, + config=self.transformation, + inputs=inputs.samples if inputs is not None else None, + ) + if isinstance(request, PlatingRequest): + if inputs is None: + raise ValueError("Plating requires a source manifest.") + return plan_plating(request, config=self.plating, inputs=inputs.samples) + raise TypeError(f"Unsupported protocol request: {type(request).__name__}") + + def compile( + self, + request: ProtocolRequest, + *, + profile: OpentronsAssemblyProfile + | OpentronsTransformationProfile + | OpentronsPlatingProfile + | None = None, + inputs: OutputManifest | None = None, + ) -> CompiledProtocol: + plan = self.plan(request, inputs=inputs) + if isinstance(request, AssemblyRequest): + profile = profile or OpentronsAssemblyProfile() + if not isinstance(profile, OpentronsAssemblyProfile): + raise TypeError("Assembly requires an OpentronsAssemblyProfile.") + method, config = "assembly", self.assembly + allocated = allocate(plan, profile=profile) + program = lower(allocated, profile=profile) + handoff_name = "transformation_input.json" + handoff = allocated.output_manifest().plasmid_locations() + elif isinstance(request, TransformationRequest): + profile = profile or OpentronsTransformationProfile() + if not isinstance(profile, OpentronsTransformationProfile): + raise TypeError( + "Transformation requires an OpentronsTransformationProfile." + ) + method, config = "transformation", self.transformation + allocated = allocate_transformation(plan, profile=profile, inputs=inputs) + program = lower_transformation(allocated, profile=profile) + handoff_name = "plating_input.json" + handoff = { + "bacterium_locations": allocated.output_manifest().bacterium_locations() + } + else: + profile = profile or OpentronsPlatingProfile() + if not isinstance(profile, OpentronsPlatingProfile): + raise TypeError("Plating requires an OpentronsPlatingProfile.") + method, config = "plating", self.plating + if config.dilution_volume > 100: + raise ValueError( + "Dilution volume exceeds the supported plate capacity." + ) + allocated = allocate_plating(plan, profile=profile, inputs=inputs) + program = lower_plating(allocated, profile=profile) + handoff_name = "plating_layout.json" + handoff = plating_layout(allocated, dilution_factor=config.dilution_factor) + script = render_python(program, name=request.id) + markdown = render_markdown(plan, allocation=allocated) + manifest = allocated.output_manifest() + artifacts = ArtifactBundle( + artifacts=( + ProtocolArtifact( + name="protocol.py", media_type="text/x-python", content=script + ), + ProtocolArtifact( + name="compilation.json", + media_type="application/json", + content=json.dumps( + { + "schema_version": "1.0", + "method": method, + "reference_revision": PUDU_REVISION, + "configuration": asdict(config), + "target_profile": asdict(profile), + "plan": { + **asdict(plan), + "steps": [ + {"kind": type(s).__name__, **asdict(s)} + for s in plan.steps + ], + }, + "containers": [asdict(c) for c in allocated.containers], + "placements": [asdict(p) for p in allocated.placements], + }, + indent=2, + sort_keys=True, + allow_nan=False, + ) + + "\n", + ), + ProtocolArtifact( + name="protocol.md", media_type="text/markdown", content=markdown + ), + ProtocolArtifact( + name="manifest.json", + media_type="application/json", + content=json.dumps(manifest.to_dict(), indent=2, sort_keys=True) + + "\n", + ), + ProtocolArtifact( + name=handoff_name, + media_type="application/json", + content=json.dumps(handoff, indent=2, sort_keys=True) + "\n", + ), + ) + ) + return CompiledProtocol( + allocation=allocated, + program=program, + script=script, + markdown=markdown, + manifest=manifest, + artifacts=artifacts, + ) diff --git a/src/buildcompiler/protocols/config.py b/src/buildcompiler/protocols/config.py new file mode 100644 index 0000000..8121ce7 --- /dev/null +++ b/src/buildcompiler/protocols/config.py @@ -0,0 +1,7 @@ +"""Public method configurations; implementations live with their planners.""" + +from buildcompiler.protocols.methods.assembly_config import AssemblyConfig +from buildcompiler.protocols.methods.transformation_config import TransformationConfig +from buildcompiler.protocols.methods.plating_config import PlatingConfig + +__all__ = ["AssemblyConfig", "TransformationConfig", "PlatingConfig"] diff --git a/src/buildcompiler/protocols/handoffs.py b/src/buildcompiler/protocols/handoffs.py new file mode 100644 index 0000000..3a13916 --- /dev/null +++ b/src/buildcompiler/protocols/handoffs.py @@ -0,0 +1,82 @@ +"""Explicit adapters for PUDU's location-only handoff formats.""" + +from collections.abc import Mapping + +from buildcompiler.protocols.allocation.models import ( + OutputManifest, + SamplePlacement, + WellRef, +) +from buildcompiler.protocols.allocation.wells import PLATE_96 +from buildcompiler.protocols.inputs import material_ref +from buildcompiler.protocols.materials import Sample +from buildcompiler.domain.protocol_requests import MaterialRef + + +def plasmid_manifest_from_json( + payload: Mapping[str, list[str]], *, protocol_id: str = "imported-assembly" +) -> OutputManifest: + samples, placements = [], [] + for index, (identity, wells) in enumerate(payload.items()): + if not isinstance(wells, (list, tuple)) or not wells: + raise ValueError("Each plasmid requires a nonempty list of source wells.") + for replicate, well in enumerate(wells, 1): + PLATE_96.index(well) + sample = Sample( + id=f"{protocol_id}/{index}/{replicate}", + material=material_ref(identity), + replicate=replicate, + ) + samples.append(sample) + placements.append( + SamplePlacement( + sample_id=sample.id, + location=WellRef(container_id="source_plate", well_name=well), + ) + ) + return OutputManifest( + protocol_id=protocol_id, samples=tuple(samples), placements=tuple(placements) + ) + + +def bacterium_manifest_from_json( + payload: Mapping, *, protocol_id: str = "imported-transformation" +) -> OutputManifest: + locations = payload.get("bacterium_locations") + if not isinstance(locations, Mapping) or not locations: + raise ValueError("Plating JSON requires nonempty bacterium_locations.") + samples, placements = [], [] + for index, (well, contents) in enumerate(locations.items()): + PLATE_96.index(well) + if isinstance(contents, str): + labels = (contents,) + elif ( + isinstance(contents, (list, tuple)) + and contents + and all(isinstance(v, str) for v in contents) + ): + labels = tuple(contents) + else: + raise ValueError( + "Bacterium contents must be a string or nonempty list of strings." + ) + # The legacy format has labels only: do not pretend they are SBOL identities. + sample = Sample( + id=f"{protocol_id}/{index}", + material=MaterialRef( + identity=f"urn:buildcompiler:imported:{protocol_id}:{index}", + label=", ".join(labels), + ), + contents=labels, + liquid_label=str(contents), + ) + samples.append(sample) + placements.append( + SamplePlacement( + sample_id=sample.id, + location=WellRef(container_id="source_plate", well_name=well), + ) + ) + return OutputManifest( + protocol_id=protocol_id, samples=tuple(samples), placements=tuple(placements) + ) diff --git a/src/buildcompiler/protocols/inputs.py b/src/buildcompiler/protocols/inputs.py new file mode 100644 index 0000000..fbf8b44 --- /dev/null +++ b/src/buildcompiler/protocols/inputs.py @@ -0,0 +1,87 @@ +"""Explicit conversion from existing assembly JSON to typed protocol inputs.""" + +from collections.abc import Mapping, Sequence +from urllib.parse import urlsplit + +from buildcompiler.domain.protocol_requests import ( + AssemblyReaction, + AssemblyRequest, + MaterialRef, + TransformationReaction, + TransformationRequest, +) + + +def material_ref(identity: str, label: str | None = None) -> MaterialRef: + if not isinstance(identity, str) or not identity: + raise ValueError("Material identities must be nonempty strings.") + if label is None: + segments = [part for part in urlsplit(identity).path.split("/") if part] + label = ( + segments[-2] + if len(segments) > 1 and segments[-1].isdigit() + else (segments[-1] if segments else identity) + ) + return MaterialRef(identity=identity, label=label) + + +def assembly_request_from_json( + payload: Sequence[Mapping[str, object]], + *, + request_id: str, + source_stage_id: str | None = None, +) -> AssemblyRequest: + """Decode this one format explicitly; never guess a method from its keys.""" + if isinstance(payload, (str, bytes)) or not isinstance(payload, Sequence): + raise TypeError("Assembly JSON must be a sequence of reaction objects.") + reactions = [] + for index, entry in enumerate(payload): + if not isinstance(entry, Mapping): + raise TypeError("Each assembly entry must be an object.") + required = {"Product", "Backbone", "PartsList", "Restriction Enzyme"} + if missing := required - entry.keys(): + raise ValueError(f"Assembly {index} is missing {sorted(missing)}.") + parts = entry["PartsList"] + if not isinstance(parts, (list, tuple)): + raise TypeError("PartsList must be an ordered list.") + reactions.append( + AssemblyReaction( + id=f"{request_id}/reaction/{index}", + product=material_ref(entry["Product"]), + backbone=material_ref(entry["Backbone"]), + parts=tuple(material_ref(part) for part in parts), + restriction_enzyme=material_ref(entry["Restriction Enzyme"]), + ) + ) + return AssemblyRequest( + id=request_id, reactions=tuple(reactions), source_stage_id=source_stage_id + ) + + +def transformation_request_from_json( + payload: Sequence[Mapping[str, object]], + *, + request_id: str, + source_stage_id: str | None = None, +) -> TransformationRequest: + if isinstance(payload, (str, bytes)) or not isinstance(payload, Sequence): + raise TypeError("Transformation JSON must be a sequence of reaction objects.") + reactions = [] + for index, entry in enumerate(payload): + if not isinstance(entry, Mapping): + raise TypeError("Each transformation entry must be an object.") + if missing := {"Strain", "Chassis", "Plasmids"} - entry.keys(): + raise ValueError(f"Transformation {index} is missing {sorted(missing)}.") + if not isinstance(entry["Plasmids"], (list, tuple)): + raise TypeError("Plasmids must be an ordered list.") + reactions.append( + TransformationReaction( + id=f"{request_id}/reaction/{index}", + strain=material_ref(entry["Strain"]), + chassis=material_ref(entry["Chassis"]), + plasmids=tuple(material_ref(p) for p in entry["Plasmids"]), + ) + ) + return TransformationRequest( + id=request_id, reactions=tuple(reactions), source_stage_id=source_stage_id + ) diff --git a/src/buildcompiler/protocols/materials.py b/src/buildcompiler/protocols/materials.py new file mode 100644 index 0000000..f7fb24a --- /dev/null +++ b/src/buildcompiler/protocols/materials.py @@ -0,0 +1,40 @@ +"""Samples retain material identity independently of their physical placement.""" + +from dataclasses import dataclass + +from buildcompiler.domain.protocol_requests import MaterialRef + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Sample: + id: str + material: MaterialRef + parent_ids: tuple[str, ...] = () + replicate: int | None = None + initial_volume_ul: float | None = None + liquid_label: str | None = None + role: str = "material" + source_sample_id: str | None = None + contents: tuple[str, ...] = () + dilution: int | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class SamplePoint: + sample_id: str + bottom_mm: float | None = None + top_mm: float | None = None + track_conical_height: bool = False + + def __post_init__(self) -> None: + if ( + sum( + ( + self.bottom_mm is not None, + self.top_mm is not None, + self.track_conical_height, + ) + ) + > 1 + ): + raise ValueError("A sample point requires one location policy.") diff --git a/src/buildcompiler/protocols/methods/__init__.py b/src/buildcompiler/protocols/methods/__init__.py new file mode 100644 index 0000000..ee5bc75 --- /dev/null +++ b/src/buildcompiler/protocols/methods/__init__.py @@ -0,0 +1 @@ +"""Pure protocol planners, grouped by scientific method.""" diff --git a/src/buildcompiler/protocols/methods/assembly.py b/src/buildcompiler/protocols/methods/assembly.py new file mode 100644 index 0000000..d945e03 --- /dev/null +++ b/src/buildcompiler/protocols/methods/assembly.py @@ -0,0 +1,171 @@ +"""Assembly planning extracted from PUDU's SBOLLoopAssembly. + +The operation order and default parameters preserve that implementation. +No robot, filesystem, or mutable inventory is used while planning. +""" + +from buildcompiler.domain.protocol_requests import AssemblyRequest, MaterialRef + +from buildcompiler.protocols.config import AssemblyConfig +from buildcompiler.protocols.materials import Sample, SamplePoint +from buildcompiler.protocols.plans import ProtocolPlan +from buildcompiler.protocols.steps import ( + DeactivateSourceModule, + DropTip, + LidAction, + OperatorAction, + RunTemperatureProgram, + SetTemperature, + Step, + Transfer, +) + +WATER = MaterialRef(identity="urn:buildcompiler:water", label="Deionized Water") +BUFFER = MaterialRef( + identity="urn:buildcompiler:ligase-buffer", label="T4 DNA Ligase Buffer" +) +LIGASE = MaterialRef(identity="urn:buildcompiler:ligase", label="T4 DNA Ligase") + + +def plan_assembly(request: AssemblyRequest, *, config: AssemblyConfig) -> ProtocolPlan: + """Expand ordered reactions into a logical plan with stable sample lineage.""" + enzymes = {} + parts = {} + for reaction in request.reactions: + enzymes[reaction.restriction_enzyme.identity] = reaction.restriction_enzyme + for material in (reaction.backbone, *reaction.parts): + previous = parts.get(material.identity) + if previous is not None and previous != material: + raise ValueError(f"Conflicting labels for {material.identity}.") + parts[material.identity] = material + # Match PUDU's source ordering, but retain full identities as keys. + ordered = ( + WATER, + BUFFER, + LIGASE, + *sorted(enzymes.values(), key=lambda item: (item.label, item.identity)), + *sorted(parts.values(), key=lambda item: (item.label, item.identity)), + ) + if len({material.identity for material in ordered}) != len(ordered): + raise ValueError("Source roles require distinct material identities.") + samples = [ + Sample( + id=f"{request.id}/source/{index}", + material=material, + initial_volume_ul=1000, + liquid_label=f"Restriction Enzyme {material.label}" + if material.identity in enzymes + else material.label, + ) + for index, material in enumerate(ordered) + ] + sources = {sample.material.identity: sample for sample in samples} + input_ids = tuple(sample.id for sample in samples) + output_ids = [] + steps: list[Step] = [LidAction(id="open-lid", action="open")] + if not config.water_testing: + steps.extend( + ( + SetTemperature( + id="cool-source", module="source", celsius=config.source_temperature + ), + SetTemperature( + id="cool-reaction", + module="reaction", + celsius=config.source_temperature, + ), + ) + ) + + for index, reaction in enumerate(request.reactions): + components = (reaction.backbone, *reaction.parts) + fixed_volume = ( + config.volume_t4_dna_ligase_buffer + + config.volume_t4_dna_ligase + + config.volume_restriction_enzyme + ) + water_volume = ( + config.volume_total_reaction + - fixed_volume + - config.volume_part * len(components) + ) + if water_volume <= 0: + raise ValueError( + f"Reaction {reaction.id} has no remaining volume for water." + ) + additions = ( + (WATER, water_volume, False), + (BUFFER, config.volume_t4_dna_ligase_buffer, True), + (LIGASE, config.volume_t4_dna_ligase, True), + (reaction.restriction_enzyme, config.volume_restriction_enzyme, True), + *((part, config.volume_part, True) for part in components), + ) + for replicate in range(1, config.replicates + 1): + sample = Sample( + id=f"{request.id}/reaction/{index}/replicate/{replicate}", + material=reaction.product, + parent_ids=tuple(sources[part.identity].id for part in components), + replicate=replicate, + ) + samples.append(sample) + output_ids.append(sample.id) + for addition_index, (material, volume, mix) in enumerate(additions): + steps.append( + Transfer( + id=f"{sample.id}/addition/{addition_index}", + source=SamplePoint(sample_id=sources[material.identity].id), + destination=SamplePoint(sample_id=sample.id), + volume_ul=volume, + aspiration_rate=config.aspiration_rate, + dispense_rate=config.dispense_rate, + mix_before_ul=volume if mix else 0, + drop_tip=addition_index != len(additions) - 1, + ) + ) + # PUDU finishes using the final addition's tip. This is explicit + # in the plan rather than hidden inside a liquid-transfer helper. + for mix_index in range(int(config.volume_total_reaction / 10)): + steps.append( + Transfer( + id=f"{sample.id}/finish/{mix_index}", + source=SamplePoint(sample_id=sample.id, bottom_mm=0), + destination=SamplePoint(sample_id=sample.id, bottom_mm=8), + volume_ul=config.volume_total_reaction, + aspiration_rate=1, + dispense_rate=1, + new_tip=False, + drop_tip=False, + limit_to_pipette_capacity=True, + ) + ) + steps.append(DropTip(id=f"{sample.id}/drop-tip")) + + steps.append( + OperatorAction( + id="remove-reagents", + instruction="Take out the reagents since the temperature module will be turn off", + ) + ) + if not config.water_testing: + steps.extend( + ( + LidAction(id="close-lid", action="close"), + SetTemperature( + id="heat-lid", module="lid", celsius=config.lid_temperature + ), + DeactivateSourceModule(id="deactivate-source"), + RunTemperatureProgram(id="cycling", program=config.cycling), + RunTemperatureProgram(id="finishing", program=config.finishing), + SetTemperature( + id="hold", module="reaction", celsius=config.hold_temperature + ), + ) + ) + return ProtocolPlan( + id=request.id, + request_id=request.id, + samples=tuple(samples), + input_sample_ids=input_ids, + output_sample_ids=tuple(output_ids), + steps=tuple(steps), + ) diff --git a/src/buildcompiler/protocols/methods/assembly_config.py b/src/buildcompiler/protocols/methods/assembly_config.py new file mode 100644 index 0000000..9e04200 --- /dev/null +++ b/src/buildcompiler/protocols/methods/assembly_config.py @@ -0,0 +1,92 @@ +"""Resolved, immutable method configuration. Defaults preserve PUDU assembly. + +Assembly defaults and operation ordering originate in PUDU (MIT, Rudge Lab). +See buildcompiler.protocols.attribution. A method configuration intentionally contains no deck layout. +""" + +import math +from dataclasses import dataclass + +from buildcompiler.protocols.steps import TemperatureProgram, TemperatureStep +from buildcompiler.protocols.validation import ConfigOverrides + + +@dataclass(frozen=True, slots=True, kw_only=True) +class AssemblyConfig(ConfigOverrides): + volume_total_reaction: float = 20 + volume_part: float = 2 + volume_restriction_enzyme: float = 2 + volume_t4_dna_ligase: float = 4 + volume_t4_dna_ligase_buffer: float = 2 + replicates: int = 1 + aspiration_rate: float = 0.5 + dispense_rate: float = 1 + water_testing: bool = False + source_temperature: float = 4 + lid_temperature: float = 42 + hold_temperature: float = 4 + cycling: TemperatureProgram = TemperatureProgram( + steps=( + TemperatureStep(celsius=42, minutes=2), + TemperatureStep(celsius=16, minutes=5), + ), + repetitions=75, + block_max_volume_ul=30, + ) + finishing: TemperatureProgram = TemperatureProgram( + steps=( + TemperatureStep(celsius=60, minutes=10), + TemperatureStep(celsius=80, minutes=10), + ), + repetitions=1, + block_max_volume_ul=30, + ) + + def __post_init__(self) -> None: + if type(self.replicates) is not int or self.replicates < 1: + raise ValueError("replicates must be a positive integer.") + if type(self.water_testing) is not bool: + raise ValueError("water_testing must be a boolean.") + for name in ( + "volume_total_reaction", + "volume_part", + "volume_restriction_enzyme", + "volume_t4_dna_ligase", + "volume_t4_dna_ligase_buffer", + "aspiration_rate", + "dispense_rate", + ): + value = getattr(self, name) + if ( + type(value) not in (int, float) + or not math.isfinite(value) + or value <= 0 + ): + raise ValueError(f"{name} must be a finite positive number.") + for name in ("source_temperature", "lid_temperature", "hold_temperature"): + value = getattr(self, name) + if type(value) not in (int, float) or not math.isfinite(value): + raise ValueError(f"{name} must be a finite number.") + for program in (self.cycling, self.finishing): + if not isinstance(program, TemperatureProgram): + raise TypeError( + "Temperature programs must be TemperatureProgram records." + ) + if not isinstance(program.steps, tuple) or not program.steps: + raise ValueError( + "Temperature programs require a nonempty tuple of steps." + ) + if type(program.repetitions) is not int or program.repetitions < 1: + raise ValueError("Program repetitions must be positive integers.") + if ( + not math.isfinite(program.block_max_volume_ul) + or program.block_max_volume_ul <= 0 + ): + raise ValueError("Program block volume must be finite and positive.") + for step in program.steps: + if ( + not math.isfinite(step.celsius) + or not math.isfinite(step.minutes) + or step.minutes < 0 + ): + raise ValueError("Invalid temperature program step.") diff --git a/src/buildcompiler/protocols/methods/plating.py b/src/buildcompiler/protocols/methods/plating.py new file mode 100644 index 0000000..30a382a --- /dev/null +++ b/src/buildcompiler/protocols/methods/plating.py @@ -0,0 +1,169 @@ +"""Pure plating plan, with explicit intermediate samples and shared operations. + +Derived from pudu/plating.py (MIT); see protocols/attribution.py. +""" + +from buildcompiler.domain.protocol_requests import MaterialRef, PlatingRequest +from buildcompiler.protocols.materials import Sample, SamplePoint +from buildcompiler.protocols.methods.plating_config import PlatingConfig +from buildcompiler.protocols.plans import ProtocolPlan +from buildcompiler.protocols.steps import ( + Distribute, + DropTip, + LidAction, + Mix, + PickUpTip, + SetTemperature, + Transfer, +) + + +def plan_plating( + request: PlatingRequest, *, config: PlatingConfig, inputs: tuple[Sample, ...] +) -> ProtocolPlan: + available = {s.id: s for s in inputs} + if set(request.sample_ids) - available.keys(): + raise ValueError("Plating sources are missing from the source manifest.") + if ( + len(request.sample_ids) * config.replicates * config.number_dilutions + > config.max_colonies + ): + raise ValueError("Plating exceeds max_colonies.") + broth = Sample( + id=f"{request.id}/broth", + role="broth", + initial_volume_ul=config.volume_lb, + material=MaterialRef( + identity="urn:buildcompiler:liquid-broth", label="liquid_broth" + ), + ) + sources = [] + dilutions, spots = {}, {} + for index, sample_id in enumerate(request.sample_ids): + upstream = available[sample_id] + source = Sample( + id=f"{request.id}/source/{index}", + role="bacteria", + material=upstream.material, + initial_volume_ul=config.volume_total_reaction, + source_sample_id=upstream.id, + contents=upstream.contents or (upstream.material.label,), + liquid_label=upstream.liquid_label + or str(list(upstream.contents or (upstream.material.label,))), + ) + sources.append(source) + parent = source + for dilution in range(1, config.number_dilutions + 1): + diluted = Sample( + id=f"{request.id}/dilution/{index}/{dilution}", + role="dilution", + material=source.material, + parent_ids=(parent.id, broth.id), + dilution=dilution, + contents=source.contents, + ) + dilutions[index, dilution] = diluted + parent = diluted + for replicate in range(1, config.replicates + 1): + spots[index, dilution, replicate] = Sample( + id=f"{request.id}/agar/{index}/{dilution}/{replicate}", + role="agar", + material=source.material, + parent_ids=(diluted.id,), + dilution=dilution, + replicate=replicate, + contents=source.contents, + ) + steps = [ + SetTemperature(id="chill-reactions", module="reaction", celsius=4), + LidAction(id="open-lid", action="open"), + PickUpTip(id="broth-tip", instrument="large"), + ] + ordered_dilutions = [ + dilutions[index, dilution] + for dilution in range(1, config.number_dilutions + 1) + for index in range(len(sources)) + ] + for start in range(0, len(ordered_dilutions), 8): + steps.append( + Distribute( + id=f"broth/{start}", + source=SamplePoint(sample_id=broth.id, track_conical_height=True), + destinations=tuple( + SamplePoint(sample_id=s.id) + for s in ordered_dilutions[start : start + 8] + ), + volume_ul=config.volume_lb_transfer, + disposal_volume_ul=4, + new_tip="never", + track_liquid_sample_id=broth.id, + ) + ) + steps.append(DropTip(id="broth-tip-drop", instrument="large")) + + def transfer(id, source, destination, volume, *, agar=False): + return Transfer( + id=id, + source=SamplePoint(sample_id=source.id), + destination=SamplePoint( + sample_id=destination.id, top_mm=-8 if agar else None + ), + volume_ul=volume, + aspiration_rate=config.aspiration_rate, + dispense_rate=config.dispense_rate, + instrument="small", + new_tip=False, + drop_tip=False, + touch_tip=False, + blow_out=agar, + ) + + for index, source in enumerate(sources): + steps.append(PickUpTip(id=f"dilution-tip/{index}", instrument="small")) + parent = source + for dilution in range(1, config.number_dilutions + 1): + destination = dilutions[index, dilution] + steps.append( + transfer( + f"seed/{index}/{dilution}", + parent, + destination, + config.volume_bacteria_transfer, + ) + ) + steps.append( + Mix( + id=f"mix/{index}/{dilution}", + location=SamplePoint(sample_id=destination.id), + volume_ul=config.mix_volume, + repetitions=5, + instrument="small", + ) + ) + parent = destination + for dilution in range(1, config.number_dilutions + 1): + if dilution > 1: + steps.append( + PickUpTip(id=f"agar-tip/{index}/{dilution}", instrument="small") + ) + for replicate in range(1, config.replicates + 1): + steps.append( + transfer( + f"spot/{index}/{dilution}/{replicate}", + dilutions[index, dilution], + spots[index, dilution, replicate], + config.volume_colony, + agar=True, + ) + ) + steps.append( + DropTip(id=f"agar-drop/{index}/{dilution}", instrument="small") + ) + return ProtocolPlan( + id=request.id, + request_id=request.id, + samples=(broth, *sources, *ordered_dilutions, *spots.values()), + input_sample_ids=(broth.id, *(s.id for s in sources)), + output_sample_ids=tuple(s.id for s in spots.values()), + steps=tuple(steps), + ) diff --git a/src/buildcompiler/protocols/methods/plating_config.py b/src/buildcompiler/protocols/methods/plating_config.py new file mode 100644 index 0000000..374e1db --- /dev/null +++ b/src/buildcompiler/protocols/methods/plating_config.py @@ -0,0 +1,62 @@ +"""Immutable dilution and plating configuration, preserving PUDU defaults.""" + +from dataclasses import dataclass + +from buildcompiler.protocols.validation import ( + ConfigOverrides, + positive, + positive_integer, +) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class PlatingConfig(ConfigOverrides): + volume_total_reaction: float = 20 + volume_bacteria_transfer: float = 2 + volume_colony: float = 4 + dilution_factor: float = 10 + volume_lb: float = 10000 + replicates: int = 1 + number_dilutions: int = 2 + max_colonies: int = 192 + aspiration_rate: float = 0.5 + dispense_rate: float = 1 + + def __post_init__(self) -> None: + for name in ( + "volume_total_reaction", + "volume_bacteria_transfer", + "volume_colony", + "dilution_factor", + "volume_lb", + "aspiration_rate", + "dispense_rate", + ): + positive(getattr(self, name), name) + for name in ("replicates", "number_dilutions", "max_colonies"): + positive_integer(getattr(self, name), name) + if self.replicates > 8 or self.number_dilutions > 2: + raise ValueError( + "PUDU plating supports at most eight replicates and two dilutions." + ) + if self.dilution_factor <= 1 or self.dilution_volume <= 1: + raise ValueError("Dilution factor and dilution volume must exceed one.") + required = self.volume_colony * self.replicates + if self.number_dilutions == 2: + required += self.volume_bacteria_transfer + if self.dilution_volume < required: + raise ValueError( + "Dilution volume is insufficient for plating and subsequent seeding." + ) + + @property + def volume_lb_transfer(self) -> float: + return self.volume_bacteria_transfer * (self.dilution_factor - 1) + + @property + def dilution_volume(self) -> float: + return self.volume_bacteria_transfer * self.dilution_factor + + @property + def mix_volume(self) -> float: + return min(19, self.dilution_volume - 1) diff --git a/src/buildcompiler/protocols/methods/transformation.py b/src/buildcompiler/protocols/methods/transformation.py new file mode 100644 index 0000000..9af4e3f --- /dev/null +++ b/src/buildcompiler/protocols/methods/transformation.py @@ -0,0 +1,254 @@ +"""Pure heat-shock planning, preserving PUDU's ordering and tube grouping. + +Derived from pudu/transformation.py (MIT); see protocols/attribution.py. +Source-location replicates and method replicates are distinct axes. +""" + +from collections import Counter +from itertools import groupby +from math import ceil + +from buildcompiler.domain.protocol_requests import MaterialRef, TransformationRequest +from buildcompiler.protocols.materials import Sample, SamplePoint +from buildcompiler.protocols.methods.transformation_config import TransformationConfig +from buildcompiler.protocols.plans import ProtocolPlan +from buildcompiler.protocols.steps import ( + Distribute, + LidAction, + RunTemperatureProgram, + SetTemperature, + TemperatureProgram, + Transfer, +) + + +def plan_transformation( + request: TransformationRequest, + *, + config: TransformationConfig, + inputs: tuple[Sample, ...] | None = None, +) -> ProtocolPlan: + plasmids = {p.identity: p for r in request.reactions for p in r.plasmids} + chassis = {r.chassis.identity: r.chassis for r in request.reactions} + ordered_plasmids = sorted(plasmids.values(), key=lambda p: (p.label, p.identity)) + ordered_chassis = sorted(chassis.values(), key=lambda c: (c.label, c.identity)) + available = {identity: [] for identity in plasmids} + for sample in inputs or (): + if sample.material.identity in available: + available[sample.material.identity].append(sample) + if inputs is not None: + if any(not samples for samples in available.values()): + raise ValueError( + "The source manifest must contain every requested plasmid." + ) + counts = {len(samples) for samples in available.values()} + if len(counts) != 1: + raise ValueError("Plasmid source-location replicate counts must be equal.") + location_replicates = counts.pop() + else: + location_replicates = 1 + samples = [] + dna = {} + for index, plasmid in enumerate(ordered_plasmids): + dna[plasmid.identity] = [] + for location_index in range(location_replicates): + upstream = ( + available[plasmid.identity][location_index] + if inputs is not None + else None + ) + sample = Sample( + id=f"{request.id}/dna/{index}/{location_index}", + material=plasmid, + role="dna", + replicate=location_index + 1, + source_sample_id=upstream.id if upstream else None, + initial_volume_ul=config.volume_dna, + ) + samples.append(sample) + dna[plasmid.identity].append(sample) + per_cell_tube = int( + config.tube_volume_competent_cell // config.transfer_volume_competent_cell + ) + per_media_tube = int( + config.tube_volume_recovery_media // config.transfer_volume_recovery_media + ) + reaction_counts = Counter(r.chassis.identity for r in request.reactions) + cells = {} + for index, cell in enumerate(ordered_chassis): + count = reaction_counts[cell.identity] * location_replicates * config.replicates + cells[cell.identity] = [] + for tube in range(ceil(count / per_cell_tube)): + sample = Sample( + id=f"{request.id}/cells/{index}/{tube}", + material=cell, + role="cells", + liquid_label=f"Competent Cell {cell.label}_{tube + 1}", + replicate=tube + 1, + initial_volume_ul=config.tube_volume_competent_cell, + ) + samples.append(sample) + cells[cell.identity].append(sample) + total = len(request.reactions) * location_replicates * config.replicates + media = [] + for tube in range(ceil(total / per_media_tube)): + sample = Sample( + id=f"{request.id}/media/{tube}", + role="media", + material=MaterialRef( + identity="urn:buildcompiler:recovery-media", label="Media" + ), + liquid_label=f"Media_{tube + 1}", + replicate=tube + 1, + initial_volume_ul=config.tube_volume_recovery_media, + ) + samples.append(sample) + media.append(sample) + input_ids = tuple(s.id for s in samples) + outputs = [] + cell_transfers = [] + dna_transfers = [] + cell_counts = Counter() + for reaction in request.reactions: + for location_index in range(location_replicates): + for replicate in range(config.replicates): + cell = cells[reaction.chassis.identity][ + cell_counts[reaction.chassis.identity] // per_cell_tube + ] + cell_counts[reaction.chassis.identity] += 1 + parents = tuple( + dna[p.identity][location_index] for p in reaction.plasmids + ) + medium = media[len(outputs) // per_media_tube] + output = Sample( + id=f"{request.id}/reaction/{len(outputs)}", + material=reaction.strain, + role="reaction", + replicate=replicate + 1, + parent_ids=(cell.id, *(p.id for p in parents), medium.id), + contents=( + reaction.strain.label, + f"Competent_Cell_{reaction.chassis.label}", + *(p.label for p in reaction.plasmids), + medium.liquid_label, + ), + ) + outputs.append(output) + cell_transfers.append((cell, output)) + dna_transfers.extend((parent, output) for parent in parents) + steps = [LidAction(id="open-lid", action="open")] + if not config.water_testing: + steps.extend( + ( + SetTemperature( + id="chill-source", + module="source", + celsius=4, + skip_during_simulation=True, + ), + SetTemperature( + id="chill-reactions", + module="reaction", + celsius=4, + skip_during_simulation=True, + ), + ) + ) + for index, (source_id, group) in enumerate( + groupby(cell_transfers, key=lambda pair: pair[0].id) + ): + steps.append( + Distribute( + id=f"cells/{index}", + source=SamplePoint(sample_id=source_id), + destinations=tuple( + SamplePoint(sample_id=output.id) for _, output in group + ), + volume_ul=config.transfer_volume_competent_cell, + mix_before=(3, 50), + ) + ) + instrument = "large" if config.transfer_volume_dna > 20 else "small" + for index, (source, output) in enumerate(dna_transfers): + steps.append( + Transfer( + id=f"dna/{index}", + instrument=instrument, + source=SamplePoint(sample_id=source.id), + destination=SamplePoint(sample_id=output.id), + volume_ul=config.transfer_volume_dna, + mix_before_ul=config.transfer_volume_dna, + aspiration_rate=config.aspiration_rate, + dispense_rate=config.dispense_rate, + touch_tip=False, + drop_tip=False, + ) + ) + for movement in range(2): + steps.append( + Transfer( + id=f"dna/{index}/finish/{movement}", + instrument=instrument, + source=SamplePoint(sample_id=output.id, bottom_mm=0), + destination=SamplePoint(sample_id=output.id, bottom_mm=8), + volume_ul=20, + aspiration_rate=config.dispense_rate, + dispense_rate=config.dispense_rate, + new_tip=False, + blow_out=False, + touch_tip=movement == 1, + drop_tip=movement == 1, + ) + ) + steps.append(LidAction(id="close-before-incubation", action="close")) + if not config.water_testing: + steps.append( + RunTemperatureProgram( + id="heat-shock", + skip_during_simulation=True, + program=TemperatureProgram( + steps=( + config.cold_incubation1, + config.heat_shock, + config.cold_incubation2, + ), + repetitions=1, + block_max_volume_ul=30, + ), + ) + ) + steps.append(LidAction(id="open-for-recovery", action="open")) + for index, medium in enumerate(media): + destinations = outputs[index * per_media_tube : (index + 1) * per_media_tube] + steps.append( + Distribute( + id=f"media/{index}", + source=SamplePoint(sample_id=medium.id), + destinations=tuple( + SamplePoint(sample_id=s.id, top_mm=2) for s in destinations + ), + volume_ul=config.transfer_volume_recovery_media, + air_gap_ul=10, + ) + ) + steps.append(LidAction(id="close-for-recovery", action="close")) + if not config.water_testing: + steps.append( + RunTemperatureProgram( + id="recovery", + skip_during_simulation=True, + program=TemperatureProgram( + steps=(config.recovery_incubation,), + repetitions=1, + block_max_volume_ul=30, + ), + ) + ) + return ProtocolPlan( + id=request.id, + request_id=request.id, + samples=tuple(samples + outputs), + input_sample_ids=input_ids, + output_sample_ids=tuple(s.id for s in outputs), + steps=tuple(steps), + ) diff --git a/src/buildcompiler/protocols/methods/transformation_config.py b/src/buildcompiler/protocols/methods/transformation_config.py new file mode 100644 index 0000000..6807b77 --- /dev/null +++ b/src/buildcompiler/protocols/methods/transformation_config.py @@ -0,0 +1,56 @@ +"""PUDU heat-shock method parameters; no labware or deck configuration.""" + +from dataclasses import dataclass + +from buildcompiler.protocols.steps import TemperatureStep +from buildcompiler.protocols.validation import ( + ConfigOverrides, + positive, + positive_integer, +) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TransformationConfig(ConfigOverrides): + volume_dna: float = 20 + transfer_volume_dna: float = 2 + transfer_volume_competent_cell: float = 20 + tube_volume_competent_cell: float = 100 + transfer_volume_recovery_media: float = 60 + tube_volume_recovery_media: float = 1200 + replicates: int = 2 + aspiration_rate: float = 0.5 + dispense_rate: float = 1 + water_testing: bool = False + cold_incubation1: TemperatureStep = TemperatureStep(celsius=4, minutes=30) + heat_shock: TemperatureStep = TemperatureStep(celsius=42, minutes=1) + cold_incubation2: TemperatureStep = TemperatureStep(celsius=4, minutes=2) + recovery_incubation: TemperatureStep = TemperatureStep(celsius=37, minutes=60) + + def __post_init__(self) -> None: + positive_integer(self.replicates, "replicates") + for name in ( + "volume_dna", + "transfer_volume_dna", + "transfer_volume_competent_cell", + "tube_volume_competent_cell", + "transfer_volume_recovery_media", + "tube_volume_recovery_media", + "aspiration_rate", + "dispense_rate", + ): + positive(getattr(self, name), name) + if type(self.water_testing) is not bool: + raise ValueError("water_testing must be a boolean.") + if self.tube_volume_competent_cell < self.transfer_volume_competent_cell: + raise ValueError("A cell tube must supply at least one transfer.") + if self.tube_volume_recovery_media < self.transfer_volume_recovery_media: + raise ValueError("A media tube must supply at least one transfer.") + for step in ( + self.cold_incubation1, + self.heat_shock, + self.cold_incubation2, + self.recovery_incubation, + ): + if not isinstance(step, TemperatureStep): + raise TypeError("Incubations must be TemperatureStep records.") diff --git a/src/buildcompiler/protocols/plans.py b/src/buildcompiler/protocols/plans.py new file mode 100644 index 0000000..008eee3 --- /dev/null +++ b/src/buildcompiler/protocols/plans.py @@ -0,0 +1,43 @@ +"""Immutable logical plans with explicit input and output sample identities.""" + +from dataclasses import dataclass + +from buildcompiler.protocols.materials import Sample +from buildcompiler.protocols.steps import Distribute, Mix, Step, Transfer + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ProtocolPlan: + id: str + request_id: str + samples: tuple[Sample, ...] + input_sample_ids: tuple[str, ...] + output_sample_ids: tuple[str, ...] + steps: tuple[Step, ...] + + def __post_init__(self) -> None: + sample_ids = {sample.id for sample in self.samples} + if len(sample_ids) != len(self.samples): + raise ValueError("Sample ids must be unique within a plan.") + if len({step.id for step in self.steps}) != len(self.steps): + raise ValueError("Step ids must be unique within a plan.") + if not set(self.input_sample_ids + self.output_sample_ids) <= sample_ids: + raise ValueError("Plan inputs and outputs must reference declared samples.") + for sample in self.samples: + if not set(sample.parent_ids) <= sample_ids: + raise ValueError(f"Unknown parent for sample {sample.id}.") + for step in self.steps: + referenced = set() + if isinstance(step, Transfer): + referenced = {step.source.sample_id, step.destination.sample_id} + elif isinstance(step, Distribute): + referenced = { + step.source.sample_id, + *(d.sample_id for d in step.destinations), + } + if step.track_liquid_sample_id: + referenced.add(step.track_liquid_sample_id) + elif isinstance(step, Mix): + referenced = {step.location.sample_id} + if referenced - sample_ids: + raise ValueError(f"Unknown sample in operation {step.id}.") diff --git a/src/buildcompiler/protocols/steps.py b/src/buildcompiler/protocols/steps.py new file mode 100644 index 0000000..b2d6258 --- /dev/null +++ b/src/buildcompiler/protocols/steps.py @@ -0,0 +1,129 @@ +"""Serializable operations; no live robot or SBOL objects belong here.""" + +from dataclasses import dataclass +import math +from typing import Literal + +from buildcompiler.protocols.materials import SamplePoint + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Transfer: + id: str + source: SamplePoint + destination: SamplePoint + volume_ul: float + aspiration_rate: float = 0.5 + dispense_rate: float = 1.0 + mix_before_ul: float = 0.0 + mix_repetitions: int = 3 + new_tip: bool = True + drop_tip: bool = True + blow_out: bool = True + touch_tip: bool = True + limit_to_pipette_capacity: bool = False + instrument: str = "pipette" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DropTip: + id: str + instrument: str = "pipette" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class PickUpTip: + id: str + instrument: str = "pipette" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Mix: + id: str + location: SamplePoint + volume_ul: float + repetitions: int + instrument: str = "pipette" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Distribute: + id: str + source: SamplePoint + destinations: tuple[SamplePoint, ...] + volume_ul: float + instrument: str = "large" + disposal_volume_ul: float = 0 + new_tip: Literal["once", "never"] = "once" + mix_before: tuple[int, float] | None = None + air_gap_ul: float | None = None + track_liquid_sample_id: str | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TemperatureStep: + celsius: float + minutes: float + + def __post_init__(self) -> None: + if ( + not math.isfinite(self.celsius) + or not math.isfinite(self.minutes) + or self.minutes < 0 + ): + raise ValueError( + "Temperature steps require finite temperatures and nonnegative durations." + ) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TemperatureProgram: + steps: tuple[TemperatureStep, ...] + repetitions: int + block_max_volume_ul: float + + +@dataclass(frozen=True, slots=True, kw_only=True) +class RunTemperatureProgram: + id: str + program: TemperatureProgram + skip_during_simulation: bool = False + + +@dataclass(frozen=True, slots=True, kw_only=True) +class SetTemperature: + id: str + module: Literal["source", "reaction", "lid"] + celsius: float + skip_during_simulation: bool = False + + +@dataclass(frozen=True, slots=True, kw_only=True) +class LidAction: + id: str + action: Literal["open", "close"] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DeactivateSourceModule: + id: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class OperatorAction: + id: str + instruction: str + + +Step = ( + Transfer + | PickUpTip + | Mix + | Distribute + | DropTip + | RunTemperatureProgram + | SetTemperature + | LidAction + | DeactivateSourceModule + | OperatorAction +) diff --git a/src/buildcompiler/protocols/validation.py b/src/buildcompiler/protocols/validation.py new file mode 100644 index 0000000..33719e6 --- /dev/null +++ b/src/buildcompiler/protocols/validation.py @@ -0,0 +1,27 @@ +"""Small reusable configuration validators, independent of execution backends.""" + +import math +from collections.abc import Mapping +from dataclasses import fields, replace + + +def positive(value: object, name: str) -> None: + if type(value) not in (int, float) or not math.isfinite(value) or value <= 0: + raise ValueError(f"{name} must be a finite positive number.") + + +def positive_integer(value: object, name: str) -> None: + if type(value) is not int or value < 1: + raise ValueError(f"{name} must be a positive integer.") + + +class ConfigOverrides: + """Replace only explicitly supplied fields, including False and defaults.""" + + __slots__ = () + + def with_overrides(self, overrides: Mapping[str, object]): + unknown = set(overrides) - {field.name for field in fields(self)} + if unknown: + raise ValueError(f"Unknown configuration fields: {sorted(unknown)}") + return replace(self, **overrides) diff --git a/src/buildcompiler/robotutils.py b/src/buildcompiler/robotutils.py index db06337..e053b54 100644 --- a/src/buildcompiler/robotutils.py +++ b/src/buildcompiler/robotutils.py @@ -8,6 +8,8 @@ from pathlib import Path from typing import Any, Dict, List +from buildcompiler.adapters.native_protocols import compile_plating_payload + def load_json_or_dict(value): """Load a JSON file path/string into a dict, or return dict-like input as-is.""" @@ -170,24 +172,8 @@ def write_manual_plating_protocol(path, plate_id, plate_rows, advanced_params): def write_plating_protocol_script(path, plating_data, advanced_params): path = Path(path) - script = ( - "from pudu.plating import Plating\n" - "from opentrons import protocol_api\n\n" - "metadata = {\n" - ' "protocolName": "BuildCompiler Plating",\n' - ' "author": "BuildCompiler",\n' - ' "description": "Automated plating protocol generated from BuildCompiler transformation results",\n' - ' "apiLevel": "2.21",\n' - "}\n\n" - f"PLATING_DATA = {json.dumps(plating_data, indent=4)}\n" - f"ADVANCED_PARAMS = {json.dumps(advanced_params or {}, indent=4)}\n\n" - "def run(protocol: protocol_api.ProtocolContext):\n" - " plating = Plating(\n" - " plating_data=PLATING_DATA,\n" - " json_params=ADVANCED_PARAMS,\n" - " )\n" - " plating.run(protocol)\n" - ) + compiled = compile_plating_payload(plating_data, advanced_params=advanced_params) + script = compiled.script path.write_text(script, encoding="utf-8") return path diff --git a/src/buildcompiler/stages/assembly_lvl1.py b/src/buildcompiler/stages/assembly_lvl1.py index b6ecf78..8447dcb 100644 --- a/src/buildcompiler/stages/assembly_lvl1.py +++ b/src/buildcompiler/stages/assembly_lvl1.py @@ -8,6 +8,7 @@ import sbol2 from buildcompiler.adapters.pudu import assembly_route_to_pudu_json +from buildcompiler.adapters.protocol_requests import assembly_request_from_route from buildcompiler.api.options import BuildOptions from buildcompiler.domain import ( BuildRequest, @@ -206,6 +207,17 @@ def run( warnings=warnings, sbol_document=assembly_result.stage_document, json_intermediate=json_intermediate, + protocol_requests=( + assembly_request_from_route( + stage_id=f"{request.id}:{BuildStage.ASSEMBLY_LVL1.value}", + products=assembly_result.products, + parts=route.selected_part_plasmids, + backbone=route.backbone, + restriction_enzyme=restriction_enzyme, + ), + ) + if assembly_result.products + else (), protocol_artifacts={"assembly_spec": json_intermediate["Parameters"]}, logs=logs, ) diff --git a/src/buildcompiler/stages/assembly_lvl2.py b/src/buildcompiler/stages/assembly_lvl2.py index 3b8259e..61901cb 100644 --- a/src/buildcompiler/stages/assembly_lvl2.py +++ b/src/buildcompiler/stages/assembly_lvl2.py @@ -8,6 +8,7 @@ import sbol2 from buildcompiler.adapters.pudu import assembly_route_to_pudu_json +from buildcompiler.adapters.protocol_requests import assembly_request_from_route from buildcompiler.api.options import BuildOptions from buildcompiler.domain import ( BuildRequest, @@ -215,6 +216,17 @@ def run( products=assembly_result.products, sbol_document=assembly_result.stage_document, json_intermediate=json_intermediate, + protocol_requests=( + assembly_request_from_route( + stage_id=f"{request.id}:{BuildStage.ASSEMBLY_LVL2.value}", + products=assembly_result.products, + parts=route.selected_lvl1_plasmids, + backbone=route.backbone, + restriction_enzyme=restriction_enzyme, + ), + ) + if assembly_result.products + else (), protocol_artifacts=artifacts | {"assembly_spec": json_intermediate["Parameters"]}, logs=[ diff --git a/src/buildcompiler/stages/transformation.py b/src/buildcompiler/stages/transformation.py index 9342be8..2692eec 100644 --- a/src/buildcompiler/stages/transformation.py +++ b/src/buildcompiler/stages/transformation.py @@ -15,6 +15,7 @@ StageStatus, ) from buildcompiler.sbol.transformation import TransformationJob, TransformationService +from buildcompiler.protocols.inputs import transformation_request_from_json class TransformationStage: @@ -98,6 +99,13 @@ def run( json_intermediate=json_intermediate, protocol_artifacts=result.artifacts, logs=result.logs, + protocol_requests=( + transformation_request_from_json( + [json_intermediate], + request_id=f"{request_id}:{BuildStage.TRANSFORMATION.value}", + source_stage_id=f"{request_id}:{BuildStage.TRANSFORMATION.value}", + ), + ), ) def _plasmid_from_input( diff --git a/tests/automation/README.md b/tests/automation/README.md index b083daf..68cab2d 100644 --- a/tests/automation/README.md +++ b/tests/automation/README.md @@ -1,10 +1,19 @@ -# Automation test suite (optional/manual) +# Protocol equivalence acceptance -These tests are intentionally separated from core CI. +These tests run the independent PUDU reference and BuildCompiler's generated standalone Python through `opentrons.simulate.simulate`. They compare ordered structured robot commands, resource and liquid definitions, temperature-program arguments, touch-tip parameters, and JSON handoffs. Only informational `comment` commands are excluded. Unknown trace values and simulator failures fail the run. -Run only when optional dependencies are installed: +Use Python 3.10 and the reference revision recorded in [pudu_reference.json](pudu_reference.json): ```bash -python -m pip install -e '.[automation,test]' -pytest tests/automation +python -m pip install -e '.[test,simulation]' +PUDU_REPOSITORY=../PUDU BUILDCOMPILER_REQUIRE_EQUIVALENCE=1 \ + python -m pytest tests/automation -q --basetemp=/tmp/buildcompiler-equivalence ``` + +The fixture verifies the source hashes. `BUILDCOMPILER_REQUIRE_EQUIVALENCE=1` makes missing dependencies or a missing reference checkout fail instead of skip. CI checks out the pinned reference, runs this command, and uploads the traces. Normal core tests require neither PUDU nor Opentrons. + +Coverage includes assembly source ordering, replicates, offsets, rates, water testing and rack replacement; transformation source-location replicates, multiple plasmids, interleaved chassis, independent cell/media tube boundaries, both pipettes, offsets and thermal behavior; plating dilution counts, replicates, two agar plates, offsets and changing conical-source heights. A connected workflow checks assembly → transformation → plating using each implementation's own handoffs and verifies native sample lineage. A changed-flow negative control establishes that the comparator detects a behavioral difference. + +PUDU forces transformation into water-testing mode during simulation. Two additional transformation cases override only `ProtocolContext.is_simulating` in both entrypoints to exercise the ordinary temperature-program path and explicit water-testing override through the real simulator. These are separate from normal simulation parity. + +The generated scripts do not write files. Their expected handoffs are compiler artifacts compared against PUDU's simulation-generated JSON. This verifies the covered simulator behavior, not physical hardware or experimental outcomes. diff --git a/tests/automation/conftest.py b/tests/automation/conftest.py new file mode 100644 index 0000000..ab2cbec --- /dev/null +++ b/tests/automation/conftest.py @@ -0,0 +1,31 @@ +"""An independent pinned PUDU checkout is mandatory for acceptance runs.""" + +import hashlib +import importlib.util +import json +import os +from pathlib import Path + +import pytest + + +@pytest.fixture(scope="session") +def pudu_repository(): + required = os.environ.get("BUILDCOMPILER_REQUIRE_EQUIVALENCE") == "1" + repository = Path( + os.environ.get("PUDU_REPOSITORY", Path(__file__).resolve().parents[3] / "PUDU") + ) + if ( + importlib.util.find_spec("opentrons") is None + or not (repository / "src/pudu/assembly.py").is_file() + ): + if required: + pytest.fail("Acceptance requires Opentrons and a PUDU reference checkout.") + pytest.skip("Install the simulation extra and supply PUDU_REPOSITORY.") + reference = json.loads(Path(__file__).with_name("pudu_reference.json").read_text()) + for name, expected in reference["sha256"].items(): + path = repository / name + assert hashlib.sha256(path.read_bytes()).hexdigest() == expected, ( + f"PUDU reference changed: {path}" + ) + return repository diff --git a/tests/automation/pudu_reference.json b/tests/automation/pudu_reference.json new file mode 100644 index 0000000..cc77458 --- /dev/null +++ b/tests/automation/pudu_reference.json @@ -0,0 +1,11 @@ +{ + "repository": "https://github.com/MyersResearchGroup/PUDU", + "revision": "cdf7bde2128871c889fb25a4a0c168d5d8f0c1d3", + "sha256": { + "src/pudu/assembly.py": "8f9f105c452ab4eea03499e792d47ff6a5cb208f9c0eea3f7d7eb80f23ad5e95", + "src/pudu/utils.py": "75e469d0b25e40576efa09b32fb20ae2aa08c3d89763bc35831cad0d8e9323c3", + "src/pudu/transformation.py": "1723e05b08106c5a9d8aaf00a09c7358127a42e3686e912c5c2dcd1bcf0b5ff3", + "src/pudu/plating.py": "6dd82bca10a8f20e8ea70600145f4378dee465f6a06144dea29d6e990575f917" + }, + "simulator": "8.8.2" +} diff --git a/tests/automation/test_assembly_equivalence.py b/tests/automation/test_assembly_equivalence.py new file mode 100644 index 0000000..0de5889 --- /dev/null +++ b/tests/automation/test_assembly_equivalence.py @@ -0,0 +1,144 @@ +"""Acceptance: execute both independent implementations in the real simulator.""" + +import json +from dataclasses import asdict + +import pytest + +from buildcompiler.protocols import ( + AssemblyConfig, + OpentronsAssemblyProfile, + ProtocolCompiler, + assembly_request_from_json, +) +from buildcompiler.protocols.backends.opentrons.simulation import ( + SimulationError, + simulate_source, +) + +pytestmark = pytest.mark.automation + + +def assemblies(count=1): + return [ + { + "Product": f"https://example.org/product_{index}/1", + "Backbone": "https://example.org/backbone/1", + "PartsList": [ + "https://example.org/part_b/1", + f"https://example.org/part_{index}/1", + ], + "Restriction Enzyme": "https://example.org/enzyme/1", + } + for index in range(count) + ] + + +def reference_script(payload, config, profile): + fields = ( + "volume_total_reaction", + "volume_part", + "volume_restriction_enzyme", + "volume_t4_dna_ligase", + "volume_t4_dna_ligase_buffer", + "replicates", + "aspiration_rate", + "dispense_rate", + "water_testing", + ) + parameters = {name: getattr(config, name) for name in fields} + parameters.update( + {key: value for key, value in asdict(profile).items() if key != "api_level"} + ) + parameters["tiprack_positions"] = list(parameters["tiprack_positions"]) + parameters["output_xlsx"] = False + return ( + "from pudu.assembly import SBOLLoopAssembly\n" + "metadata = {'apiLevel': '2.21', 'protocolName': 'PUDU reference'}\n" + "def run(protocol):\n" + f" SBOLLoopAssembly(assemblies={payload!r}, json_params={parameters!r}).run(protocol)\n" + ) + + +@pytest.mark.parametrize( + "count,config,profile", + [ + (1, AssemblyConfig(), OpentronsAssemblyProfile()), + (3, AssemblyConfig(replicates=2), OpentronsAssemblyProfile()), + ( + 1, + AssemblyConfig(replicates=2), + OpentronsAssemblyProfile(initial_tip="G10", thermocycler_starting_well=9), + ), + ( + 1, + AssemblyConfig(replicates=15), + OpentronsAssemblyProfile(tiprack_positions=("2",)), + ), + (1, AssemblyConfig(water_testing=True), OpentronsAssemblyProfile()), + ( + 2, + AssemblyConfig( + volume_total_reaction=25, aspiration_rate=0.25, dispense_rate=0.75 + ), + OpentronsAssemblyProfile(), + ), + ], + ids=[ + "default", + "multiple-products-and-replicates", + "offsets", + "rack-swap", + "water-testing", + "configuration", + ], +) +def test_assembly_simulator_equivalence( + pudu_repository, tmp_path, count, config, profile +): + payload = assemblies(count) + compiled = ProtocolCompiler(assembly=config).compile( + assembly_request_from_json(payload, request_id="acceptance"), + profile=profile, + ) + expected = simulate_source( + reference_script(payload, config, profile), + python_paths=(pudu_repository / "src",), + ) + actual = simulate_source(compiled.script) + for label, trace in (("reference", expected), ("migrated", actual)): + (tmp_path / f"{label}.json").write_text( + json.dumps(asdict(trace), indent=2, sort_keys=True) + ) + assert actual.simulator_version == expected.simulator_version == "8.8.2" + assert actual.actions, "An empty simulation is not equivalence evidence." + assert actual.actions == expected.actions + assert actual.sdk_calls == expected.sdk_calls + assert ( + expected.generated_json["transformation_input.json"] + == compiled.manifest.plasmid_locations() + ) + assert actual.generated_json == {}, ( + "Generated scripts must not write handoff files at runtime." + ) + print( + f"{len(actual.actions)} actions; {len(actual.sdk_calls)} SDK calls; handoff equal" + ) + + +def test_trace_detects_a_changed_flow_rate(pudu_repository): + request = assembly_request_from_json(assemblies(), request_id="negative-control") + original = simulate_source(ProtocolCompiler().compile(request).script) + changed = simulate_source( + ProtocolCompiler(assembly=AssemblyConfig(aspiration_rate=0.25)) + .compile(request) + .script + ) + assert original.actions != changed.actions + + +def test_simulator_failure_is_reported(pudu_repository): + with pytest.raises(SimulationError, match="deliberate failure"): + simulate_source( + "metadata={'apiLevel':'2.21'}\ndef run(protocol):\n raise ValueError('deliberate failure')\n" + ) diff --git a/tests/automation/test_opentrons_simulation_optional.py b/tests/automation/test_opentrons_simulation_optional.py deleted file mode 100644 index 014ddec..0000000 --- a/tests/automation/test_opentrons_simulation_optional.py +++ /dev/null @@ -1,7 +0,0 @@ -import pytest - -pytestmark = pytest.mark.automation - - -def test_opentrons_simulation_manual_only(): - pytest.skip("Manual/optional automation validation only. TODO(#67): wire real simulation checks in automation environment.") diff --git a/tests/automation/test_plating_equivalence.py b/tests/automation/test_plating_equivalence.py new file mode 100644 index 0000000..636fd8d --- /dev/null +++ b/tests/automation/test_plating_equivalence.py @@ -0,0 +1,93 @@ +"""Real simulator comparisons for plating and its allocation manifest.""" + +import json +from dataclasses import asdict + +import pytest + +from buildcompiler.protocols import ( + OpentronsPlatingProfile, + PlatingConfig, + PlatingRequest, + ProtocolCompiler, + bacterium_manifest_from_json, +) +from buildcompiler.protocols.allocation.wells import PLATE_96 +from buildcompiler.protocols.backends.opentrons.simulation import simulate_source + +pytestmark = pytest.mark.automation + + +def plating_data(count): + return { + "bacterium_locations": { + PLATE_96.name(i + 3): [f"strain_{i}", "cell", f"plasmid_{i}"] + for i in range(count) + } + } + + +def reference_script(payload, config, profile): + parameters = asdict(config) | { + key: value for key, value in asdict(profile).items() if key != "api_level" + } + return ( + "from pudu.plating import Plating\nmetadata={'apiLevel': '2.21'}\ndef run(protocol):\n" + f" Plating(plating_data={payload!r}, json_params={parameters!r}).run(protocol)\n" + ) + + +@pytest.mark.parametrize( + "count,config,profile", + [ + (1, PlatingConfig(), OpentronsPlatingProfile()), + ( + 5, + PlatingConfig(replicates=2), + OpentronsPlatingProfile( + initial_small_tip="C4", initial_large_tip="B2", lb_tube_position=4 + ), + ), + (25, PlatingConfig(replicates=2), OpentronsPlatingProfile()), + ( + 3, + PlatingConfig(number_dilutions=1, aspiration_rate=0.25, dispense_rate=0.75), + OpentronsPlatingProfile(), + ), + (9, PlatingConfig(volume_lb=3100), OpentronsPlatingProfile()), + ], + ids=[ + "default", + "replicates-and-offsets", + "two-agar-plates", + "one-dilution", + "conical-height-threshold", + ], +) +def test_plating_equivalence(pudu_repository, tmp_path, count, config, profile): + payload = plating_data(count) + inputs = bacterium_manifest_from_json(payload) + compiled = ProtocolCompiler(plating=config).compile( + PlatingRequest(id="plating", sample_ids=tuple(s.id for s in inputs.samples)), + inputs=inputs, + profile=profile, + ) + expected = simulate_source( + reference_script(payload, config, profile), + python_paths=(pudu_repository / "src",), + ) + actual = simulate_source(compiled.script) + for name, result in (("reference", expected), ("migrated", actual)): + (tmp_path / f"{name}.json").write_text( + json.dumps(asdict(result), indent=2, sort_keys=True) + ) + assert actual.actions and actual.actions == expected.actions + assert actual.sdk_calls == expected.sdk_calls + handoff = next( + a for a in compiled.artifacts.artifacts if a.name == "plating_layout.json" + ) + assert expected.generated_json["plating_layout.json"] == json.loads(handoff.content) + assert actual.generated_json == {} + print( + f"{len(actual.actions)} actions; {len(actual.sdk_calls)} SDK calls; handoff equal" + ) diff --git a/tests/automation/test_transformation_equivalence.py b/tests/automation/test_transformation_equivalence.py new file mode 100644 index 0000000..a817148 --- /dev/null +++ b/tests/automation/test_transformation_equivalence.py @@ -0,0 +1,167 @@ +"""Transformation acceptance covers mappings, grouping and both thermal paths.""" + +import json +from dataclasses import asdict + +import pytest + +from buildcompiler.protocols import ( + OpentronsTransformationProfile, + ProtocolCompiler, + TransformationConfig, + plasmid_manifest_from_json, + transformation_request_from_json, +) +from buildcompiler.protocols.backends.opentrons.simulation import simulate_source + +pytestmark = pytest.mark.automation + + +def transformations(chassis=("chassis_a",), *, multiple_plasmids=False): + return [ + { + "Strain": f"https://example.org/strain_{i}/1", + "Chassis": f"https://example.org/{cell}/1", + "Plasmids": [f"https://example.org/plasmid_{i}/1"] + + (["https://example.org/shared/1"] if multiple_plasmids else []), + } + for i, cell in enumerate(chassis) + ] + + +def reference_script(payload, config, profile, locations=None): + parameters = asdict(config) + for name in ( + "cold_incubation1", + "heat_shock", + "cold_incubation2", + "recovery_incubation", + ): + step = getattr(config, name) + parameters[name] = { + "temperature": step.celsius, + "hold_time_minutes": step.minutes, + } + parameters.update( + {name: value for name, value in asdict(profile).items() if name != "api_level"} + ) + return ( + "from pudu.transformation import HeatShockTransformation\n" + "metadata = {'apiLevel': '2.21', 'protocolName': 'PUDU reference'}\n" + "def run(protocol):\n" + f" HeatShockTransformation(transformation_data={payload!r}, plasmid_locations={locations!r}, json_params={parameters!r}).run(protocol)\n" + ) + + +def force_thermal_path(source): + # Still runs through opentrons.simulate.simulate; only the conditional changes. + return ( + source.replace("def run(", "def _original_run(", 1) + + "\ndef run(protocol):\n protocol.is_simulating = lambda: False\n _original_run(protocol)\n" + ) + + +@pytest.mark.parametrize( + "payload,config,profile,locations,thermal", + [ + ( + transformations(), + TransformationConfig(), + OpentronsTransformationProfile(), + None, + False, + ), + ( + transformations( + ("chassis_b", "chassis_a", "chassis_b"), multiple_plasmids=True + ), + TransformationConfig(replicates=3, tube_volume_recovery_media=180), + OpentronsTransformationProfile(), + None, + False, + ), + ( + transformations(), + TransformationConfig(replicates=3), + OpentronsTransformationProfile( + thermocycler_starting_well=9, + initial_tip_p20="E3", + initial_tip_p300="B2", + ), + {"https://example.org/plasmid_0/1": ["G3", "A5"]}, + False, + ), + ( + transformations(), + TransformationConfig( + transfer_volume_dna=25, + volume_dna=100, + aspiration_rate=0.25, + dispense_rate=0.75, + ), + OpentronsTransformationProfile(initial_dna_well=5), + None, + False, + ), + ( + transformations(), + TransformationConfig(), + OpentronsTransformationProfile(), + None, + True, + ), + ( + transformations(), + TransformationConfig(water_testing=True), + OpentronsTransformationProfile(), + None, + True, + ), + ], + ids=[ + "default", + "mixed-chassis-and-tube-boundaries", + "source-replicates-and-offsets", + "large-dna-pipette", + "thermal-path", + "explicit-water-testing", + ], +) +def test_transformation_equivalence( + pudu_repository, tmp_path, payload, config, profile, locations, thermal +): + compiled = ProtocolCompiler(transformation=config).compile( + transformation_request_from_json(payload, request_id="transformation"), + profile=profile, + inputs=plasmid_manifest_from_json(locations) if locations is not None else None, + ) + expected_source = reference_script(payload, config, profile, locations) + actual_source = compiled.script + if thermal: + expected_source, actual_source = ( + force_thermal_path(expected_source), + force_thermal_path(actual_source), + ) + expected = simulate_source(expected_source, python_paths=(pudu_repository / "src",)) + actual = simulate_source(actual_source) + for name, result in (("reference", expected), ("migrated", actual)): + (tmp_path / f"{name}.json").write_text( + json.dumps(asdict(result), indent=2, sort_keys=True) + ) + assert actual.actions and actual.actions == expected.actions + assert actual.sdk_calls == expected.sdk_calls + if not thermal: + assert expected.generated_json["plating_input.json"] == { + "bacterium_locations": compiled.manifest.bacterium_locations() + } + if thermal and not config.water_testing: + assert ( + sum( + call["method"].endswith(".execute_profile") for call in actual.sdk_calls + ) + == 2 + ) + assert actual.generated_json == {} + print( + f"{len(actual.actions)} actions; {len(actual.sdk_calls)} SDK calls; thermal={thermal}" + ) diff --git a/tests/automation/test_workflow_equivalence.py b/tests/automation/test_workflow_equivalence.py new file mode 100644 index 0000000..34f4917 --- /dev/null +++ b/tests/automation/test_workflow_equivalence.py @@ -0,0 +1,130 @@ +"""Connected acceptance: each implementation consumes its own previous handoff.""" + +import json +from dataclasses import asdict + +import pytest + +from buildcompiler.protocols import ( + AssemblyConfig, + OpentronsPlatingProfile, + PlatingConfig, + PlatingRequest, + ProtocolCompiler, + TransformationConfig, + assembly_request_from_json, + transformation_request_from_json, +) +from buildcompiler.protocols.backends.opentrons.simulation import simulate_source + +pytestmark = pytest.mark.automation + + +def test_connected_workflow_equivalence(pudu_repository, tmp_path): + assemblies = [ + { + "Product": f"https://example.org/plasmid_{i}/1", + "Backbone": "https://example.org/backbone/1", + "PartsList": [f"https://example.org/part_{i}/1"], + "Restriction Enzyme": "https://example.org/enzyme/1", + } + for i in range(2) + ] + transformations = [ + { + "Strain": f"https://example.org/strain_{i}/1", + "Chassis": "https://example.org/chassis/1", + "Plasmids": [a["Product"]], + } + for i, a in enumerate(assemblies) + ] + compiler = ProtocolCompiler( + assembly=AssemblyConfig(replicates=2), + transformation=TransformationConfig(replicates=2), + plating=PlatingConfig(replicates=2), + ) + reference_preamble = "metadata={'apiLevel':'2.21'}\ndef run(protocol):\n" + + def compare(name, reference_source, compiled): + expected = simulate_source( + reference_source, python_paths=(pudu_repository / "src",) + ) + actual = simulate_source(compiled.script) + for label, result in (("reference", expected), ("migrated", actual)): + (tmp_path / f"{name}-{label}.json").write_text( + json.dumps(asdict(result), indent=2, sort_keys=True) + ) + assert actual.actions and actual.actions == expected.actions + assert actual.sdk_calls == expected.sdk_calls + assert actual.generated_json == {} + return expected.generated_json + + assembly = compiler.compile( + assembly_request_from_json(assemblies, request_id="assembly") + ) + expected_assembly = compare( + "assembly", + "from pudu.assembly import SBOLLoopAssembly\n" + + reference_preamble + + f" SBOLLoopAssembly(assemblies={assemblies!r}, replicates=2, output_xlsx=False).run(protocol)\n", + assembly, + ) + assert ( + expected_assembly["transformation_input.json"] + == assembly.manifest.plasmid_locations() + ) + transformation = compiler.compile( + transformation_request_from_json(transformations, request_id="transformation"), + inputs=assembly.manifest, + ) + expected_transformation = compare( + "transformation", + "from pudu.transformation import HeatShockTransformation\n" + + reference_preamble + + f" HeatShockTransformation(transformation_data={transformations!r}, plasmid_locations={expected_assembly['transformation_input.json']!r}, replicates=2).run(protocol)\n", + transformation, + ) + assert expected_transformation["plating_input.json"] == { + "bacterium_locations": transformation.manifest.bacterium_locations() + } + # Explicitly retain the physical plate definition from transformation. + plating_profile = OpentronsPlatingProfile( + thermocycler_labware="nest_96_wellplate_100ul_pcr_full_skirt" + ) + plating = compiler.compile( + PlatingRequest( + id="plating", + sample_ids=tuple(s.id for s in transformation.manifest.samples), + ), + inputs=transformation.manifest, + profile=plating_profile, + ) + expected_plating = compare( + "plating", + "from pudu.plating import Plating\n" + + reference_preamble + + f" Plating(plating_data={expected_transformation['plating_input.json']!r}, replicates=2, thermocycler_labware={plating_profile.thermocycler_labware!r}).run(protocol)\n", + plating, + ) + assert expected_plating["plating_layout.json"] == json.loads( + next( + a.content + for a in plating.artifacts.artifacts + if a.name == "plating_layout.json" + ) + ) + assert [len(p.manifest.samples) for p in (assembly, transformation, plating)] == [ + 4, + 8, + 32, + ] + assert { + s.source_sample_id + for s in transformation.allocation.protocol.samples + if s.role == "dna" + } == {s.id for s in assembly.manifest.samples} + assert { + s.source_sample_id + for s in plating.allocation.protocol.samples + if s.role == "bacteria" + } == {s.id for s in transformation.manifest.samples} diff --git a/tests/test_plating.py b/tests/test_plating.py index 9989538..8f4eaf0 100644 --- a/tests/test_plating.py +++ b/tests/test_plating.py @@ -120,7 +120,8 @@ def test_plating_automated_script_and_sim_zip(self, mock_run): script = script_path.read_text(encoding="utf-8") self.assertIn("def run(protocol: protocol_api.ProtocolContext):", script) self.assertIn("from opentrons import protocol_api", script) - self.assertIn("json_params=ADVANCED_PARAMS", script) + self.assertNotIn("from pudu", script) + self.assertIn(".distribute(", script) self.assertNotIn("advanced_params=ADVANCED_PARAMS", script) self.assertTrue(Path(result["protocol_artifacts"]["simulation_zip"]).exists()) diff --git a/tests/unit/adapters/opentrons/test_simulation_boundary.py b/tests/unit/adapters/opentrons/test_simulation_boundary.py index 49d53bb..ed5efcc 100644 --- a/tests/unit/adapters/opentrons/test_simulation_boundary.py +++ b/tests/unit/adapters/opentrons/test_simulation_boundary.py @@ -1,5 +1,6 @@ +import importlib.util import sys -from subprocess import CompletedProcess +from types import SimpleNamespace import pytest @@ -9,6 +10,7 @@ ProtocolSimulationError, ) from buildcompiler.api import ProtocolOptions +from buildcompiler.adapters.opentrons import simulation def test_opentrons_import_is_lazy(): @@ -26,18 +28,31 @@ def test_simulate_false_does_not_import_opentrons(): def test_simulate_true_missing_dependency_raises(monkeypatch): adapter = OpentronsSimulationAdapter() - monkeypatch.setattr("shutil.which", lambda _: None) + real_find_spec = importlib.util.find_spec + + def fake_find_spec(name, *args, **kwargs): + if name == "opentrons": + return None + return real_find_spec(name, *args, **kwargs) + + monkeypatch.setattr(importlib.util, "find_spec", fake_find_spec) with pytest.raises(OptionalAutomationDependencyError): adapter.simulate("protocol.py", options=ProtocolOptions(simulate=True)) -def test_simulate_runs_cli_and_captures_evidence(monkeypatch, tmp_path): +def test_simulate_captures_evidence(monkeypatch, tmp_path): protocol = tmp_path / "protocol.py" protocol.write_text("from opentrons import protocol_api\n", encoding="utf-8") - monkeypatch.setattr("shutil.which", lambda _: "/usr/bin/opentrons_simulate") + monkeypatch.setattr(importlib.util, "find_spec", lambda _: object()) monkeypatch.setattr( - "subprocess.run", - lambda *args, **kwargs: CompletedProcess(args[0], 0, "ok\n", ""), + simulation, + "simulate_source", + lambda source: SimpleNamespace( + actions=[{"command": "test"}], + simulator_version="8.8.2", + stdout="ok\n", + stderr="", + ), ) result = OpentronsSimulationAdapter().simulate( @@ -46,18 +61,20 @@ def test_simulate_runs_cli_and_captures_evidence(monkeypatch, tmp_path): assert result.ran is True assert result.metadata["returncode"] == 0 - assert result.metadata["command"][-1] == "protocol.py" - assert result.logs == ["ok\n"] + assert result.metadata["command_count"] == 1 + assert result.metadata["simulator_version"] == "8.8.2" + assert "ok" in result.logs def test_simulation_failure_is_not_reported_as_success(monkeypatch, tmp_path): protocol = tmp_path / "protocol.py" protocol.write_text("broken", encoding="utf-8") - monkeypatch.setattr("shutil.which", lambda _: "/usr/bin/opentrons_simulate") - monkeypatch.setattr( - "subprocess.run", - lambda *args, **kwargs: CompletedProcess(args[0], 2, "", "invalid\n"), - ) + monkeypatch.setattr(importlib.util, "find_spec", lambda _: object()) + + def fail(source): + raise RuntimeError("Opentrons simulation failed with exit code 2: invalid") + + monkeypatch.setattr(simulation, "simulate_source", fail) with pytest.raises(ProtocolSimulationError, match="exit code 2"): OpentronsSimulationAdapter().simulate( diff --git a/tests/unit/adapters/test_native_protocols.py b/tests/unit/adapters/test_native_protocols.py new file mode 100644 index 0000000..a85a49c --- /dev/null +++ b/tests/unit/adapters/test_native_protocols.py @@ -0,0 +1,30 @@ +import json + +import pytest + +from buildcompiler.adapters.native_protocols import compile_plating_payload + + +def test_legacy_plating_overrides_are_explicit_and_use_native_compilation(): + compiled = compile_plating_payload( + {"bacterium_locations": {"A2": "strain"}, "replicates": 2}, + advanced_params={"replicates": 1, "number_dilutions": 1}, + ) + assert len(compiled.manifest.samples) == 1 + assert "from pudu" not in compiled.script + assert ".distribute(" in compiled.script + resolved = json.loads( + next( + a.content + for a in compiled.artifacts.artifacts + if a.name == "compilation.json" + ) + ) + assert resolved["configuration"]["replicates"] == 1 + + +def test_unsupported_legacy_parameters_fail_instead_of_being_ignored(): + with pytest.raises(ValueError, match="Unsupported native plating parameters"): + compile_plating_payload( + {"bacterium_locations": {"A1": "strain"}}, advanced_params={"unknown": 1} + ) diff --git a/tests/unit/protocols/test_compilation.py b/tests/unit/protocols/test_compilation.py new file mode 100644 index 0000000..7b1e0e9 --- /dev/null +++ b/tests/unit/protocols/test_compilation.py @@ -0,0 +1,221 @@ +import ast +import dataclasses +import json +import subprocess +import sys + +import pytest + +from buildcompiler.api.protocols import compile_assembly +from buildcompiler.api.serialization import serialize_stage_result +from buildcompiler.domain import BuildStage, StageResult, StageStatus +from buildcompiler.protocols import ( + AssemblyConfig, + OpentronsAssemblyProfile, + ProtocolCompiler, + assembly_request_from_json, +) +from buildcompiler.protocols.allocation.wells import BLOCK_24, PLATE_96 +from buildcompiler.protocols.backends.markdown import render_markdown +from buildcompiler.protocols.steps import Transfer + + +def payload(): + return [ + { + "Product": "https://example.org/product/1", + "Backbone": "https://example.org/backbone/1", + "PartsList": [ + "https://example.org/part_b/1", + "https://example.org/part_a/1", + ], + "Restriction Enzyme": "https://example.org/enzyme/1", + } + ] + + +def request(): + return assembly_request_from_json(payload(), request_id="example") + + +def test_compile_is_deterministic_and_never_writes(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + compiler = ProtocolCompiler() + first = compiler.compile(request()) + second = compiler.compile(request()) + assert first == second + assert list(tmp_path.iterdir()) == [] + assert ast.parse(first.script) + imports = [ + node.module + for node in ast.walk(ast.parse(first.script)) + if isinstance(node, ast.ImportFrom) + ] + assert imports == ["opentrons"] + with pytest.raises(dataclasses.FrozenInstanceError): + first.allocation.protocol.id = "changed" + + +def test_config_overrides_preserve_explicit_default_and_false(): + config = AssemblyConfig(replicates=3, water_testing=True) + resolved = config.with_overrides({"replicates": 1, "water_testing": False}) + assert resolved.replicates == 1 + assert not resolved.water_testing + assert config.replicates == 3 and config.water_testing + with pytest.raises(ValueError, match="Unknown"): + config.with_overrides({"typo": 2}) + + +@pytest.mark.parametrize( + "values", + [ + {"replicates": 0}, + {"replicates": True}, + {"replicates": 1.5}, + {"volume_part": float("nan")}, + {"volume_part": float("inf")}, + {"volume_part": -1}, + {"water_testing": "false"}, + ], +) +def test_invalid_configuration_is_rejected(values): + with pytest.raises((TypeError, ValueError)): + AssemblyConfig(**values) + + +def test_order_identity_replicates_and_locations_survive_compilation(): + compiled = ProtocolCompiler(assembly=AssemblyConfig(replicates=2)).compile( + request(), + profile=OpentronsAssemblyProfile(thermocycler_starting_well=9), + ) + plan = compiled.allocation.protocol + samples = {sample.id: sample for sample in plan.samples} + outputs = [samples[identity] for identity in plan.output_sample_ids] + assert [output.replicate for output in outputs] == [1, 2] + assert outputs[0].id != outputs[1].id + assert [samples[parent].material.identity for parent in outputs[0].parent_ids] == [ + payload()[0]["Backbone"], + *payload()[0]["PartsList"], + ] + assert compiled.manifest.plasmid_locations() == { + payload()[0]["Product"]: ["B2", "C2"] + } + assert compiled.manifest.to_dict()["state"] == "planned" + + +def test_same_labels_do_not_collapse_different_material_identities(): + data = payload() + data[0]["PartsList"] = ["https://a.example/part/1", "https://b.example/part/1"] + plan = ProtocolCompiler().plan( + assembly_request_from_json(data, request_id="collision") + ) + parts = [sample for sample in plan.samples if sample.material.label == "part"] + assert len(parts) == 2 + assert parts[0].id != parts[1].id + + +@pytest.mark.parametrize( + "profile", + [ + {"thermocycler_starting_well": -1}, + {"thermocycler_starting_well": 96}, + {"initial_tip": "I1"}, + {"tiprack_positions": ()}, + {"tiprack_positions": ("1",)}, + {"tiprack_positions": ("7",)}, + {"tiprack_positions": ("2", "2")}, + {"api_level": "2.1"}, + ], +) +def test_bad_hardware_profiles_fail_before_rendering(profile): + with pytest.raises(ValueError): + OpentronsAssemblyProfile(**profile) + + +def test_capacity_and_invalid_reaction_are_rejected(): + with pytest.raises(ValueError, match="plate capacity"): + ProtocolCompiler(assembly=AssemblyConfig(replicates=97)).compile(request()) + with pytest.raises(ValueError, match="remaining volume"): + ProtocolCompiler(assembly=AssemblyConfig(volume_total_reaction=10)).compile( + request() + ) + with pytest.raises(ValueError, match="pipette capacity"): + ProtocolCompiler(assembly=AssemblyConfig(volume_total_reaction=50)).compile( + request() + ) + + +def test_manual_render_and_script_use_the_same_configuration(): + compiled = ProtocolCompiler(assembly=AssemblyConfig(replicates=2)).compile( + request() + ) + transfers = [ + step + for step in compiled.allocation.protocol.steps + if isinstance(step, Transfer) + ] + assert len(transfers) == compiled.markdown.count("Transfer ") + assert "replicate 1" in compiled.markdown and "replicate 2" in compiled.markdown + assert "reactions/A1" in compiled.markdown and "reactions/B1" in compiled.markdown + assert "75" in compiled.markdown + assert "not an execution record" in compiled.markdown + assert render_markdown(compiled.allocation.protocol) + + +def test_compiled_artifacts_require_explicit_write_and_refuse_overwrite(tmp_path): + compiled = ProtocolCompiler().compile(request()) + paths = compiled.artifacts.write(tmp_path) + assert {path.name for path in paths} == { + "protocol.py", + "compilation.json", + "protocol.md", + "manifest.json", + "transformation_input.json", + } + assert ( + json.loads((tmp_path / "manifest.json").read_text()) + == compiled.manifest.to_dict() + ) + with pytest.raises(FileExistsError): + compiled.artifacts.write(tmp_path) + + +def test_stage_to_compiled_protocol_preserves_serializable_requests(): + result = StageResult( + id="assembly-stage", + stage=BuildStage.ASSEMBLY_LVL1, + status=StageStatus.SUCCESS, + protocol_requests=(request(),), + ) + compiled = compile_assembly(result) + assert compiled.manifest.protocol_id == result.id + dto = serialize_stage_result(result) + assert ( + dto["protocol_requests"][0]["reactions"][0]["parts"][0]["identity"] + == payload()[0]["PartsList"][0] + ) + result.status = StageStatus.BLOCKED + with pytest.raises(ValueError, match="successful"): + compile_assembly(result) + + +def test_well_order_is_explicit_for_both_geometries(): + assert [PLATE_96.name(i) for i in (0, 7, 8, 95)] == ["A1", "H1", "A2", "H12"] + assert [BLOCK_24.name(i) for i in (0, 3, 4, 23)] == ["A1", "D1", "A2", "D6"] + + +def test_core_import_does_not_load_automation_or_reporting_dependencies(): + process = subprocess.run( + [ + sys.executable, + "-c", + ( + "import sys; import buildcompiler.protocols; " + "assert not any(name.split('.')[0] in {'opentrons', 'pudu', 'xlsxwriter'} for name in sys.modules)" + ), + ], + capture_output=True, + text=True, + check=False, + ) + assert process.returncode == 0, process.stderr diff --git a/tests/unit/protocols/test_transformation_plating.py b/tests/unit/protocols/test_transformation_plating.py new file mode 100644 index 0000000..4d3ae7a --- /dev/null +++ b/tests/unit/protocols/test_transformation_plating.py @@ -0,0 +1,225 @@ +import ast +import dataclasses +import json + +import pytest + +from buildcompiler.protocols import ( + OpentronsPlatingProfile, + OpentronsTransformationProfile, + PlatingConfig, + PlatingRequest, + ProtocolCompiler, + TransformationConfig, + bacterium_manifest_from_json, + plasmid_manifest_from_json, + transformation_request_from_json, +) +from buildcompiler.protocols.steps import Distribute, RunTemperatureProgram + + +def request(): + return transformation_request_from_json( + [ + { + "Strain": "https://example.org/strain/1", + "Chassis": "https://example.org/chassis/1", + "Plasmids": [ + "https://example.org/first/1", + "https://example.org/second/1", + ], + }, + ], + request_id="transform", + ) + + +def sources(): + return plasmid_manifest_from_json( + { + "https://example.org/first/1": ["D2", "A4"], + "https://example.org/second/1": ["F3", "G8"], + } + ) + + +def test_source_replicates_lineage_and_explicit_locations(): + upstream = sources() + compiled = ProtocolCompiler( + transformation=TransformationConfig(replicates=3) + ).compile( + request(), + inputs=upstream, + profile=OpentronsTransformationProfile(thermocycler_starting_well=8), + ) + assert len(compiled.manifest.samples) == 6 + assert [p.location.well_name for p in compiled.manifest.placements] == [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + ] + samples = {s.id: s for s in compiled.allocation.protocol.samples} + first_sources = compiled.manifest.samples[0].parent_ids[1:-1] + second_sources = compiled.manifest.samples[3].parent_ids[1:-1] + assert [samples[s].source_sample_id for s in first_sources] == [ + upstream.samples[0].id, + upstream.samples[2].id, + ] + assert [samples[s].source_sample_id for s in second_sources] == [ + upstream.samples[1].id, + upstream.samples[3].id, + ] + dna_locations = { + p.sample_id: p.location.well_name for p in compiled.allocation.placements + } + assert [dna_locations[s] for s in first_sources] == ["D2", "F3"] + assert [dna_locations[s] for s in second_sources] == ["A4", "G8"] + + +@pytest.mark.parametrize( + "locations", + [ + {"https://example.org/first/1": ["A1"]}, + { + "https://example.org/first/1": ["A1", "B1"], + "https://example.org/second/1": ["C1"], + }, + ], +) +def test_missing_and_unequal_plasmid_source_replicates_fail(locations): + with pytest.raises(ValueError, match="source|Source"): + ProtocolCompiler().compile( + request(), inputs=plasmid_manifest_from_json(locations) + ) + + +def test_duplicate_physical_sources_are_rejected(): + with pytest.raises(ValueError, match="locations must be unique"): + plasmid_manifest_from_json({"first": ["A1"], "second": ["A1"]}) + + +def test_transformation_thermal_steps_are_reviewable_but_simulation_guarded(): + compiler = ProtocolCompiler() + compiled = compiler.compile(request()) + programs = [ + s + for s in compiled.allocation.protocol.steps + if isinstance(s, RunTemperatureProgram) + ] + assert len(programs) == 2 and all(p.skip_during_simulation for p in programs) + assert "Omitted during Opentrons simulation" in compiled.markdown + assert "if not protocol.is_simulating():" in compiled.script + wet = compiler.transformation.with_overrides({"water_testing": True}) + assert not any( + isinstance(s, RunTemperatureProgram) + for s in ProtocolCompiler(transformation=wet).plan(request()).steps + ) + + +def test_transformation_tube_and_tip_capacity_checked_before_rendering(): + with pytest.raises(ValueError, match="outside this grid"): + ProtocolCompiler( + transformation=TransformationConfig( + replicates=60, tube_volume_competent_cell=20 + ) + ).compile(request()) + with pytest.raises(ValueError, match="tips"): + ProtocolCompiler().compile( + request(), profile=OpentronsTransformationProfile(initial_tip_p20="H12") + ) + + +@pytest.mark.parametrize( + "config_type,values", + [ + (TransformationConfig, {"replicates": True}), + (TransformationConfig, {"transfer_volume_dna": float("nan")}), + (TransformationConfig, {"tube_volume_competent_cell": 1}), + (TransformationConfig, {"tube_volume_recovery_media": 1}), + (TransformationConfig, {"water_testing": "false"}), + (PlatingConfig, {"number_dilutions": 3}), + (PlatingConfig, {"replicates": 9}), + (PlatingConfig, {"dilution_factor": 1}), + (PlatingConfig, {"volume_colony": 20}), + (PlatingConfig, {"volume_lb": float("inf")}), + ], +) +def test_invalid_method_configuration(config_type, values): + with pytest.raises((ValueError, TypeError)): + config_type(**values) + + +def test_plating_has_lineage_for_dilutions_and_each_replicate(): + transformed = ProtocolCompiler().compile(request()) + compiled = ProtocolCompiler(plating=PlatingConfig(replicates=2)).compile( + PlatingRequest( + id="plate", sample_ids=tuple(s.id for s in transformed.manifest.samples) + ), + inputs=transformed.manifest, + ) + assert len(compiled.manifest.samples) == 8 + samples = {s.id: s for s in compiled.allocation.protocol.samples} + for output in compiled.manifest.samples: + parent = samples[output.parent_ids[0]] + assert parent.role == "dilution" and parent.dilution == output.dilution + while parent.role == "dilution": + parent = samples[parent.parent_ids[0]] + assert parent.source_sample_id in {s.id for s in transformed.manifest.samples} + broth = [s for s in compiled.allocation.protocol.steps if isinstance(s, Distribute)] + assert all(s.new_tip == "never" and s.source.track_conical_height for s in broth) + + +def test_plating_import_does_not_fabricate_sbol_identity(): + manifest = bacterium_manifest_from_json( + {"bacterium_locations": {"B4": ["strain", "plasmid"]}} + ) + assert manifest.samples[0].material.identity.startswith( + "urn:buildcompiler:imported:" + ) + assert manifest.samples[0].contents == ("strain", "plasmid") + + +def test_compilation_is_deterministic_and_has_no_file_or_sdk_effects( + tmp_path, monkeypatch +): + monkeypatch.chdir(tmp_path) + compiler = ProtocolCompiler() + transformed = compiler.compile(request()) + plated = compiler.compile( + PlatingRequest( + id="plating", sample_ids=tuple(s.id for s in transformed.manifest.samples) + ), + inputs=transformed.manifest, + ) + assert compiler.compile(request()) == transformed + assert list(tmp_path.iterdir()) == [] + for compiled in (transformed, plated): + with pytest.raises(dataclasses.FrozenInstanceError): + compiled.manifest.protocol_id = "changed" + imports = [ + n.module + for n in ast.walk(ast.parse(compiled.script)) + if isinstance(n, ast.ImportFrom) + ] + assert imports == ["opentrons"] + metadata = json.loads( + next( + a.content + for a in compiled.artifacts.artifacts + if a.name == "compilation.json" + ) + ) + assert all("kind" in operation for operation in metadata["plan"]["steps"]) + + +def test_plating_requires_matching_profile_and_manifest(): + req = PlatingRequest(id="plate", sample_ids=("unknown",)) + with pytest.raises(ValueError, match="source manifest"): + ProtocolCompiler().compile(req) + with pytest.raises(ValueError, match="missing"): + ProtocolCompiler().compile(req, inputs=sources()) + with pytest.raises(ValueError, match="Deck slots"): + OpentronsPlatingProfile(large_tiprack_position="9") diff --git a/tests/unit/stages/test_assembly_lvl1.py b/tests/unit/stages/test_assembly_lvl1.py index f2a2c1f..364c29a 100644 --- a/tests/unit/stages/test_assembly_lvl1.py +++ b/tests/unit/stages/test_assembly_lvl1.py @@ -140,6 +140,13 @@ def test_assembly_lvl1_success_returns_products_json_and_indexes_generated(): result.json_intermediate and result.json_intermediate["Product"] == "https://example.org/designs/d1" ) + assert ( + result.protocol_requests[0].reactions[0].product.identity + == result.products[0].identity + ) + assert [ + part.identity for part in result.protocol_requests[0].reactions[0].parts + ] == result.json_intermediate["PartsList"] assert result.sbol_document is not None assert inv.find_lvl1_region_plasmids("https://example.org/designs/d1") assert result.logs diff --git a/tests/unit/stages/test_assembly_lvl2.py b/tests/unit/stages/test_assembly_lvl2.py index 84d8580..ab6a8c9 100644 --- a/tests/unit/stages/test_assembly_lvl2.py +++ b/tests/unit/stages/test_assembly_lvl2.py @@ -122,6 +122,13 @@ def test_assembly_lvl2_success_routes_and_indexes_generated_product(): result.json_intermediate and result.json_intermediate["Product"] == module.identity ) + assert ( + result.protocol_requests[0].reactions[0].product.identity + == result.products[0].identity + ) + assert [ + part.identity for part in result.protocol_requests[0].reactions[0].parts + ] == result.json_intermediate["PartsList"] assert result.protocol_artifacts["selected_route"] is not None assert inv.find_lvl1_region_plasmids(module.identity) diff --git a/tests/unit/stages/test_transformation.py b/tests/unit/stages/test_transformation.py index 13cdb05..311ede8 100644 --- a/tests/unit/stages/test_transformation.py +++ b/tests/unit/stages/test_transformation.py @@ -1,6 +1,7 @@ import sbol2 -from buildcompiler.api import BuildOptions +from buildcompiler.api import BuildOptions, compile_transformation, compile_plating +from buildcompiler.protocols import TransformationConfig from buildcompiler.domain import IndexedPlasmid, StageStatus from buildcompiler.stages import TransformationStage @@ -38,3 +39,12 @@ def test_transformation_stage_returns_json_and_sbol_product(): assert result.products assert result.json_intermediate["Chassis"] == "dh5alpha" assert result.sbol_document.find(result.products[0].identity) is not None + request = result.protocol_requests[0] + assert request.source_stage_id == result.id + assert request.reactions[0].strain.identity == result.products[0].identity + assert request.reactions[0].plasmids[0].identity == plasmid.identity + compiled = compile_transformation(result, config=TransformationConfig(replicates=1)) + assert compiled.manifest.samples[0].material.identity == result.products[0].identity + plated = compile_plating(compiled.manifest) + assert len(plated.manifest.samples) == 2 + assert "from pudu" not in compiled.script + plated.script diff --git a/uv.lock b/uv.lock index c240036..f7c7bfd 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 1 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14'", @@ -12,18 +12,18 @@ resolution-markers = [ name = "aionotify" version = "0.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/16/81a26a64d728e76eea073cd0316f3e8885cca312247a9ba9af64d7c47e64/aionotify-0.3.1.tar.gz", hash = "sha256:9651e1373873c75786101330e302e114f85b6e8b5ad70b491497c8b3609a8449", size = 11291, upload-time = "2024-05-15T17:05:41.999Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/16/81a26a64d728e76eea073cd0316f3e8885cca312247a9ba9af64d7c47e64/aionotify-0.3.1.tar.gz", hash = "sha256:9651e1373873c75786101330e302e114f85b6e8b5ad70b491497c8b3609a8449", size = 11291 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/6a/b43cb72f72ad650225854b630ab569b74071764fb562b88264aed4c9e350/aionotify-0.3.1-py2.py3-none-any.whl", hash = "sha256:25816a9eef030c774beaee22189a24e29bc43f81cebe574ef723851eaf89ddee", size = 7429, upload-time = "2024-05-15T17:05:27.184Z" }, + { url = "https://files.pythonhosted.org/packages/6b/6a/b43cb72f72ad650225854b630ab569b74071764fb562b88264aed4c9e350/aionotify-0.3.1-py2.py3-none-any.whl", hash = "sha256:25816a9eef030c774beaee22189a24e29bc43f81cebe574ef723851eaf89ddee", size = 7429 }, ] [[package]] name = "annotated-types" version = "0.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893 } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427 }, ] [[package]] @@ -35,27 +35,27 @@ dependencies = [ { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.15'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966 } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079 }, ] [[package]] name = "appdirs" version = "1.4.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566 }, ] [[package]] name = "appnope" version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/f7/a82489c2b6ebe32d3e2831895ae19c77861f0eadf3bb16034484d965dbb2/appnope-1.0.0.tar.gz", hash = "sha256:685db59cb6043c3c2e528adc0b3bce3a5f8d09bcf7492c6ea650d1b7421f3c49", size = 5454, upload-time = "2026-08-20T21:36:13.748Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/f7/a82489c2b6ebe32d3e2831895ae19c77861f0eadf3bb16034484d965dbb2/appnope-1.0.0.tar.gz", hash = "sha256:685db59cb6043c3c2e528adc0b3bce3a5f8d09bcf7492c6ea650d1b7421f3c49", size = 5454 } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/c7/6b687cb0f83d2a51017d47953f2ee430ffad6fec2a8ebc964e0718a33eb1/appnope-1.0.0-py3-none-any.whl", hash = "sha256:6fe0c04218aab65c54c4ff81638cdbf848d89f5653b74d68638a137f200dd16e", size = 4158, upload-time = "2026-08-20T21:36:12.444Z" }, + { url = "https://files.pythonhosted.org/packages/46/c7/6b687cb0f83d2a51017d47953f2ee430ffad6fec2a8ebc964e0718a33eb1/appnope-1.0.0-py3-none-any.whl", hash = "sha256:6fe0c04218aab65c54c4ff81638cdbf848d89f5653b74d68638a137f200dd16e", size = 4158 }, ] [[package]] @@ -65,9 +65,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argon2-cffi-bindings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657 }, ] [[package]] @@ -77,48 +77,48 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/43/bb8b6e8708d49a5ab36781333af092d9f483b198a2710d01281204640055/argon2_cffi_bindings-26.1.0.tar.gz", hash = "sha256:63505c71542a44b68b1e38060450fb006404170da375feb31af153e7f9c6205d", size = 1790807, upload-time = "2026-08-20T07:44:22.492Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/d2/0ae991f1b2181e5be49007c574710a800ad36c2978683addb3e67c474e55/argon2_cffi_bindings-26.1.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:21ca0396fe5ec995dd54431c32698189666f9224810acfa752e50d2bd94d9df2", size = 25521, upload-time = "2026-08-20T07:32:43.019Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e4/ad91d8297638aa2258aad4501c306aca99480dfe76ccd638173fa3702db9/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78de2d65e0b9ea7ce9d1b1c3e87297b2d7305a02c266ee2a2d6910daddd7ee69", size = 27177, upload-time = "2026-08-20T07:32:44.158Z" }, - { url = "https://files.pythonhosted.org/packages/6f/86/5363df11b86d02cf3662208e7406496327649cc90eb365bf6f4e8a54a41f/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27f1821903e2ceadcb88ec2b45ef190897b7682449c772f4d9b53e42c520cf29", size = 26597, upload-time = "2026-08-20T07:32:45.172Z" }, - { url = "https://files.pythonhosted.org/packages/f4/b5/a14dcc592652347dad23ee93b278a4da5d2a25c9ed3ebd10d68eea823a4f/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d88e5f7e60f28ae0b0cc6b2f16c43e87cd642a196a86f85e0d8bb6fe016fc16d", size = 27403, upload-time = "2026-08-20T07:32:46.13Z" }, - { url = "https://files.pythonhosted.org/packages/b3/81/b4a20d4902af7f796390bf9245ff83c5217dfa7367efa1d14986956c482b/argon2_cffi_bindings-26.1.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:34b7d9c24a4165a2c61cc8ae11d44d48c9ce2830fb536cb7914e11fdd9962728", size = 27132, upload-time = "2026-08-20T07:32:47.13Z" }, - { url = "https://files.pythonhosted.org/packages/7e/1b/c8de358af07b1c490e0fcb863ef98e46ddb486e45567aca5a60bd68d9daa/argon2_cffi_bindings-26.1.0-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:224865cbbcb7a2bd1356741dff12b0134df726b6d44bb7b500df8e303cbd9e81", size = 27588, upload-time = "2026-08-20T07:32:48.087Z" }, - { url = "https://files.pythonhosted.org/packages/48/2f/7ee62a6e79f9309f9d9982d301b22a00010adb580c05c8109b94d7b33de0/argon2_cffi_bindings-26.1.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ffff613aaa9ce6236766e2fc6dc560bb5abde7a2e2416e3db1f9ae395a2b4dd4", size = 26785, upload-time = "2026-08-20T07:32:48.977Z" }, - { url = "https://files.pythonhosted.org/packages/e9/10/960d0ee93d4897741bcaf4799c697dae2d81499f66fd1ed042a7dd54c1f4/argon2_cffi_bindings-26.1.0-cp310-abi3-win32.whl", hash = "sha256:a86c069c91a747a2c4e5c51473590aeb48172fff9b2130d23729a42d98665ecb", size = 23898, upload-time = "2026-08-20T07:32:50.114Z" }, - { url = "https://files.pythonhosted.org/packages/6d/3a/0cc14a05810e6add9bce5e87693334baa2222de5f647fa31781885b6573f/argon2_cffi_bindings-26.1.0-cp310-abi3-win_amd64.whl", hash = "sha256:2c36ff87b5dfaa477d0bd51e9d7f6abdae7c8955d2983c97419085d842154b3e", size = 25730, upload-time = "2026-08-20T07:32:51.091Z" }, - { url = "https://files.pythonhosted.org/packages/4e/db/d83cf2af140547f0b9cdaece05b2dc2dcbf991be4667331d073eff771435/argon2_cffi_bindings-26.1.0-cp310-abi3-win_arm64.whl", hash = "sha256:f9c4420a7a864fe1b86ce35befc95b8e39fb852493b81cf798671ddc265de638", size = 24478, upload-time = "2026-08-20T07:32:52.111Z" }, - { url = "https://files.pythonhosted.org/packages/bb/5f/f652055e18d2627e2eed94c7f31a792127cfe38df786635395d742321674/argon2_cffi_bindings-26.1.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:af11ac37a7c53dc16cb7950a6190851b0870fe218b6c60c0bb7ac355234e3083", size = 15434, upload-time = "2026-08-20T07:32:53.143Z" }, - { url = "https://files.pythonhosted.org/packages/76/38/de696045960f5b846d428c0fb6c130ed3da87aac2af209b05c193815404c/argon2_cffi_bindings-26.1.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:db0fcd827ca61622a01b220aadfbece01939acf53888f2cb98cd93e9b1e2c97e", size = 15449, upload-time = "2026-08-20T07:32:54.075Z" }, - { url = "https://files.pythonhosted.org/packages/91/0a/c25af768f6b75a5a71e31207f87c540656b2808c015260444a22763221ad/argon2_cffi_bindings-26.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:28524438cd3e723f25412f63d4fd516ff5bae9ae5aa56acbe2a1404398a0cf31", size = 25683, upload-time = "2026-08-20T07:32:55.05Z" }, - { url = "https://files.pythonhosted.org/packages/a8/7e/be212c751ab0bcea7f646615f933bf262e8e50b3f7bef32f861d0a2d066b/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac82fc756a446b6ccd7139ce70efa9d8bbe541e7ad579a12dcb52764b7175c5f", size = 27311, upload-time = "2026-08-20T07:32:56.166Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ee/f84b28e4afd13d3cac36c1d8fa8c239d2dc2c51cd978d02ee5d5ad98d9bb/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a4e68eed961a8de6928d1c17ff3dc2a547e0e923c17f8f1cd79fb7bc9502f98", size = 26771, upload-time = "2026-08-20T07:32:57.206Z" }, - { url = "https://files.pythonhosted.org/packages/21/c3/95c07a023691ecd529da9cb6a8f0779e13ebc1bdfaa86d145fdc1c6e7e79/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:151dfaad9de753f4af2a7854e707e4784f2acc434340ade64239c5b104b2d605", size = 27568, upload-time = "2026-08-20T07:32:58.361Z" }, - { url = "https://files.pythonhosted.org/packages/e6/31/3a18e31406d8694b4d6a31573c3e572fff6bed318bb744453eb653766d22/argon2_cffi_bindings-26.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:061a6919145bbf282ebf1f9c59d3135d4833c25313c8595c0d68cf7712ddfce2", size = 27280, upload-time = "2026-08-20T07:32:59.343Z" }, - { url = "https://files.pythonhosted.org/packages/0b/39/d4be4577e178b2397aa5b5575c8a309bf0da2afe05fe0c72c8f398662d63/argon2_cffi_bindings-26.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:62ff20cd130c956c7c9144d5fe35228f98b51c579b2439e988b27ef93e16c02a", size = 27776, upload-time = "2026-08-20T07:33:00.325Z" }, - { url = "https://files.pythonhosted.org/packages/71/47/78f4dd96f7411339f723b96fe24039c1bd5835102b8a5ba71ac4ec712ac7/argon2_cffi_bindings-26.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:19423e5d7ac1cc354baab59eaabf18db2ec04ef6593b5abe5a34f323c4a8f87a", size = 26932, upload-time = "2026-08-20T07:33:01.272Z" }, - { url = "https://files.pythonhosted.org/packages/3b/cd/96bfd37434cc0a848a9066c291d84b28846c4c9ea289ed9866b1164d622b/argon2_cffi_bindings-26.1.0-cp314-cp314t-win32.whl", hash = "sha256:4f84cdd868978d7b7350a566c254042d44216d9e37f241f3a6d3b1dfebeede35", size = 24878, upload-time = "2026-08-20T07:33:02.189Z" }, - { url = "https://files.pythonhosted.org/packages/f1/42/d8b6810abd9b1bd2f47ebbccf460da59c9f32e94888bea4f7b137d998797/argon2_cffi_bindings-26.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2b741888c93147444fdfc851abd81cc207f37f7f7da42062a00deb3888e57da8", size = 26656, upload-time = "2026-08-20T07:33:03.222Z" }, - { url = "https://files.pythonhosted.org/packages/a9/d1/095d95eaf2ed1d9f77268cf3291bde148c6cd56121f8db2c74c1ba618a0e/argon2_cffi_bindings-26.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6ab674f668d5962a3a4136ae0812519b0f1586874263723a32181d60d64137e1", size = 25378, upload-time = "2026-08-20T07:33:04.332Z" }, - { url = "https://files.pythonhosted.org/packages/66/cb/214092c39c4dbcb72cf98b12234ddac2221f8fe2c0acf29c6a70fa83be53/argon2_cffi_bindings-26.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1d98e33bd8bd67d7206c124e200bf2229c4cfa8c9c19f7b44a897f0fc71837eb", size = 25683, upload-time = "2026-08-20T07:33:05.337Z" }, - { url = "https://files.pythonhosted.org/packages/83/e5/02015b83e9b05ccb85ff2ced424cf6e83a12d3810bc7f66d679a92b69ffb/argon2_cffi_bindings-26.1.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ccaf0a46cbb380f1fd102a874e32aa629fd3cb0c0e94f4943fa1f6d5edc5dac6", size = 27310, upload-time = "2026-08-20T07:33:06.344Z" }, - { url = "https://files.pythonhosted.org/packages/c3/4a/85e612787d0796878b3b4f6bd53dcd5484b6fe7b64cc6fc7b6e6a04cf835/argon2_cffi_bindings-26.1.0-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0c3103fcff20183e593459cfea6e012281c0e76ae3ed8b5565ad1b92eac3990", size = 26771, upload-time = "2026-08-20T07:33:07.429Z" }, - { url = "https://files.pythonhosted.org/packages/f6/84/ccb003b6f9969820e87656398f4d49c857def71a85ca1588a0e809afd7ce/argon2_cffi_bindings-26.1.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c49e853a3bef9dd10329f31f702e7fa9b5c58229ff9c2ff6d069efaf09177c08", size = 27569, upload-time = "2026-08-20T07:33:08.598Z" }, - { url = "https://files.pythonhosted.org/packages/88/07/c26b76debf0998ee08fbe947ab2058ac5de37d4b9d46b06c17abaa6c4ce9/argon2_cffi_bindings-26.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:6376d4b3aca039375ca8bf92f770da0ec424a1ce3a37077a8d3c557411aa56ca", size = 27279, upload-time = "2026-08-20T07:33:09.518Z" }, - { url = "https://files.pythonhosted.org/packages/ee/0d/ead6ddc029f91bc9b9390686dad3c808ab08100d348f6266b5f93f8970ee/argon2_cffi_bindings-26.1.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:9bacedc04b0402837586a17f0919e3dfdd95291f441f1f56bd80ec274c2840a1", size = 27774, upload-time = "2026-08-20T07:33:10.728Z" }, - { url = "https://files.pythonhosted.org/packages/7d/47/c108530d9eb86036b78d3af4de28b83b4a2d9a70512bd10ff8e59966aab4/argon2_cffi_bindings-26.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:76ae29acace5d33355344612844d588e19deaaba4639d8bb01601e4b1418ef36", size = 26933, upload-time = "2026-08-20T07:33:11.661Z" }, - { url = "https://files.pythonhosted.org/packages/a9/02/0bfc59e781c89acf64c31c388aade9d9d1c1ea38aa1ba1292fe07f607fe9/argon2_cffi_bindings-26.1.0-cp315-cp315t-win32.whl", hash = "sha256:df612391feca41c44d20118f3b88d1b86419465cd1f5496859f715ca60ec2210", size = 24875, upload-time = "2026-08-20T07:33:12.616Z" }, - { url = "https://files.pythonhosted.org/packages/61/c7/c3e46068cddffccecb8ad94d71135e9bf62bbc789589e7dfadc7c6f59214/argon2_cffi_bindings-26.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:1a0a29ed86960e44eaace7e081bdfab4f08b012fd96ec8edba71e2ad020939e4", size = 26655, upload-time = "2026-08-20T07:33:13.521Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ca/18b9c8c45fecf34b9100ec6d7946057f14a158f2eaa20ea123a3e82351cb/argon2_cffi_bindings-26.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d157ddfab1e8b21f2f1dedda9c09645d98b5ed0b667b0626be600a345d426440", size = 25376, upload-time = "2026-08-20T07:33:14.491Z" }, - { url = "https://files.pythonhosted.org/packages/94/66/7ff138b7a61a6ec4eb8ad4a98696498915492a5ffa190e937ca5f2827e0a/argon2_cffi_bindings-26.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7014ab7e6f5d8511af92544667a0346ea6dfc314ea9a7cad1dba9fdb5c9a6e33", size = 23059, upload-time = "2026-08-20T07:33:15.45Z" }, - { url = "https://files.pythonhosted.org/packages/de/6d/f120f8b4882da540b5e1375a11c85cfe37b3c671cfae1cdac797ea23e76b/argon2_cffi_bindings-26.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:242bb0cda2ae3650764fc194593d9ea45fc9e72729acd89778c7cfe184cec2a5", size = 24869, upload-time = "2026-08-20T07:33:16.528Z" }, - { url = "https://files.pythonhosted.org/packages/04/50/92811103e1042af1379741db7fd4a6d0f6e4ee4512e2c50cbd0d344cf0d8/argon2_cffi_bindings-26.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b70225b5fd1e0d2ef4f7fd30d24658454535f0924dff0caca5dc08efbbbadfbb", size = 24219, upload-time = "2026-08-20T07:33:17.617Z" }, - { url = "https://files.pythonhosted.org/packages/47/f2/1f8548c44c0036ae8ac1d6197300570b0af8ec120fc10b5bd8188f507376/argon2_cffi_bindings-26.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1af817e84578ef8b7295ad17de0f9896e4c8520dbf2233c7aa5aa3d487256fc4", size = 23589, upload-time = "2026-08-20T07:33:18.594Z" }, - { url = "https://files.pythonhosted.org/packages/a0/b9/97f0370f99611b14efd384918613dd5cbda75f28d9bb1b677aacfeaa17df/argon2_cffi_bindings-26.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:19b562b1de4b9052ef1214a2821c44b6e6f22945daa102c32ae4eff929d8b6d8", size = 23055, upload-time = "2026-08-20T07:33:19.716Z" }, - { url = "https://files.pythonhosted.org/packages/ae/70/7eb3fe7bf00103cbbb569c51aef150661f22b734a782673a600ff0f52309/argon2_cffi_bindings-26.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49d525938467d52c923a890153c99087c9d5a937d1f6b585dbdba34ec82e397a", size = 24869, upload-time = "2026-08-20T07:33:20.671Z" }, - { url = "https://files.pythonhosted.org/packages/5b/4b/9d5919c6cb1f15df7406af0f99b048bd93936f112e3e8f4c8077bc2a9110/argon2_cffi_bindings-26.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b0bcac4d490a237e18cf91f57352920c29f77f2fa39efd0813fb81298bf17ba", size = 24216, upload-time = "2026-08-20T07:33:21.653Z" }, - { url = "https://files.pythonhosted.org/packages/a3/34/32109943bace7729233cc4ee78530baa306d8cc3c6501a64ba8cb3b58129/argon2_cffi_bindings-26.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:0cc40f7b4050bb93eb67de95d2d759322fc7ce4930b9d645581ecf4913ec651e", size = 23584, upload-time = "2026-08-20T07:33:22.613Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/0b/43/bb8b6e8708d49a5ab36781333af092d9f483b198a2710d01281204640055/argon2_cffi_bindings-26.1.0.tar.gz", hash = "sha256:63505c71542a44b68b1e38060450fb006404170da375feb31af153e7f9c6205d", size = 1790807 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/d2/0ae991f1b2181e5be49007c574710a800ad36c2978683addb3e67c474e55/argon2_cffi_bindings-26.1.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:21ca0396fe5ec995dd54431c32698189666f9224810acfa752e50d2bd94d9df2", size = 25521 }, + { url = "https://files.pythonhosted.org/packages/7e/e4/ad91d8297638aa2258aad4501c306aca99480dfe76ccd638173fa3702db9/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78de2d65e0b9ea7ce9d1b1c3e87297b2d7305a02c266ee2a2d6910daddd7ee69", size = 27177 }, + { url = "https://files.pythonhosted.org/packages/6f/86/5363df11b86d02cf3662208e7406496327649cc90eb365bf6f4e8a54a41f/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27f1821903e2ceadcb88ec2b45ef190897b7682449c772f4d9b53e42c520cf29", size = 26597 }, + { url = "https://files.pythonhosted.org/packages/f4/b5/a14dcc592652347dad23ee93b278a4da5d2a25c9ed3ebd10d68eea823a4f/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d88e5f7e60f28ae0b0cc6b2f16c43e87cd642a196a86f85e0d8bb6fe016fc16d", size = 27403 }, + { url = "https://files.pythonhosted.org/packages/b3/81/b4a20d4902af7f796390bf9245ff83c5217dfa7367efa1d14986956c482b/argon2_cffi_bindings-26.1.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:34b7d9c24a4165a2c61cc8ae11d44d48c9ce2830fb536cb7914e11fdd9962728", size = 27132 }, + { url = "https://files.pythonhosted.org/packages/7e/1b/c8de358af07b1c490e0fcb863ef98e46ddb486e45567aca5a60bd68d9daa/argon2_cffi_bindings-26.1.0-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:224865cbbcb7a2bd1356741dff12b0134df726b6d44bb7b500df8e303cbd9e81", size = 27588 }, + { url = "https://files.pythonhosted.org/packages/48/2f/7ee62a6e79f9309f9d9982d301b22a00010adb580c05c8109b94d7b33de0/argon2_cffi_bindings-26.1.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ffff613aaa9ce6236766e2fc6dc560bb5abde7a2e2416e3db1f9ae395a2b4dd4", size = 26785 }, + { url = "https://files.pythonhosted.org/packages/e9/10/960d0ee93d4897741bcaf4799c697dae2d81499f66fd1ed042a7dd54c1f4/argon2_cffi_bindings-26.1.0-cp310-abi3-win32.whl", hash = "sha256:a86c069c91a747a2c4e5c51473590aeb48172fff9b2130d23729a42d98665ecb", size = 23898 }, + { url = "https://files.pythonhosted.org/packages/6d/3a/0cc14a05810e6add9bce5e87693334baa2222de5f647fa31781885b6573f/argon2_cffi_bindings-26.1.0-cp310-abi3-win_amd64.whl", hash = "sha256:2c36ff87b5dfaa477d0bd51e9d7f6abdae7c8955d2983c97419085d842154b3e", size = 25730 }, + { url = "https://files.pythonhosted.org/packages/4e/db/d83cf2af140547f0b9cdaece05b2dc2dcbf991be4667331d073eff771435/argon2_cffi_bindings-26.1.0-cp310-abi3-win_arm64.whl", hash = "sha256:f9c4420a7a864fe1b86ce35befc95b8e39fb852493b81cf798671ddc265de638", size = 24478 }, + { url = "https://files.pythonhosted.org/packages/bb/5f/f652055e18d2627e2eed94c7f31a792127cfe38df786635395d742321674/argon2_cffi_bindings-26.1.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:af11ac37a7c53dc16cb7950a6190851b0870fe218b6c60c0bb7ac355234e3083", size = 15434 }, + { url = "https://files.pythonhosted.org/packages/76/38/de696045960f5b846d428c0fb6c130ed3da87aac2af209b05c193815404c/argon2_cffi_bindings-26.1.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:db0fcd827ca61622a01b220aadfbece01939acf53888f2cb98cd93e9b1e2c97e", size = 15449 }, + { url = "https://files.pythonhosted.org/packages/91/0a/c25af768f6b75a5a71e31207f87c540656b2808c015260444a22763221ad/argon2_cffi_bindings-26.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:28524438cd3e723f25412f63d4fd516ff5bae9ae5aa56acbe2a1404398a0cf31", size = 25683 }, + { url = "https://files.pythonhosted.org/packages/a8/7e/be212c751ab0bcea7f646615f933bf262e8e50b3f7bef32f861d0a2d066b/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac82fc756a446b6ccd7139ce70efa9d8bbe541e7ad579a12dcb52764b7175c5f", size = 27311 }, + { url = "https://files.pythonhosted.org/packages/a6/ee/f84b28e4afd13d3cac36c1d8fa8c239d2dc2c51cd978d02ee5d5ad98d9bb/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a4e68eed961a8de6928d1c17ff3dc2a547e0e923c17f8f1cd79fb7bc9502f98", size = 26771 }, + { url = "https://files.pythonhosted.org/packages/21/c3/95c07a023691ecd529da9cb6a8f0779e13ebc1bdfaa86d145fdc1c6e7e79/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:151dfaad9de753f4af2a7854e707e4784f2acc434340ade64239c5b104b2d605", size = 27568 }, + { url = "https://files.pythonhosted.org/packages/e6/31/3a18e31406d8694b4d6a31573c3e572fff6bed318bb744453eb653766d22/argon2_cffi_bindings-26.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:061a6919145bbf282ebf1f9c59d3135d4833c25313c8595c0d68cf7712ddfce2", size = 27280 }, + { url = "https://files.pythonhosted.org/packages/0b/39/d4be4577e178b2397aa5b5575c8a309bf0da2afe05fe0c72c8f398662d63/argon2_cffi_bindings-26.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:62ff20cd130c956c7c9144d5fe35228f98b51c579b2439e988b27ef93e16c02a", size = 27776 }, + { url = "https://files.pythonhosted.org/packages/71/47/78f4dd96f7411339f723b96fe24039c1bd5835102b8a5ba71ac4ec712ac7/argon2_cffi_bindings-26.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:19423e5d7ac1cc354baab59eaabf18db2ec04ef6593b5abe5a34f323c4a8f87a", size = 26932 }, + { url = "https://files.pythonhosted.org/packages/3b/cd/96bfd37434cc0a848a9066c291d84b28846c4c9ea289ed9866b1164d622b/argon2_cffi_bindings-26.1.0-cp314-cp314t-win32.whl", hash = "sha256:4f84cdd868978d7b7350a566c254042d44216d9e37f241f3a6d3b1dfebeede35", size = 24878 }, + { url = "https://files.pythonhosted.org/packages/f1/42/d8b6810abd9b1bd2f47ebbccf460da59c9f32e94888bea4f7b137d998797/argon2_cffi_bindings-26.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2b741888c93147444fdfc851abd81cc207f37f7f7da42062a00deb3888e57da8", size = 26656 }, + { url = "https://files.pythonhosted.org/packages/a9/d1/095d95eaf2ed1d9f77268cf3291bde148c6cd56121f8db2c74c1ba618a0e/argon2_cffi_bindings-26.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6ab674f668d5962a3a4136ae0812519b0f1586874263723a32181d60d64137e1", size = 25378 }, + { url = "https://files.pythonhosted.org/packages/66/cb/214092c39c4dbcb72cf98b12234ddac2221f8fe2c0acf29c6a70fa83be53/argon2_cffi_bindings-26.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1d98e33bd8bd67d7206c124e200bf2229c4cfa8c9c19f7b44a897f0fc71837eb", size = 25683 }, + { url = "https://files.pythonhosted.org/packages/83/e5/02015b83e9b05ccb85ff2ced424cf6e83a12d3810bc7f66d679a92b69ffb/argon2_cffi_bindings-26.1.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ccaf0a46cbb380f1fd102a874e32aa629fd3cb0c0e94f4943fa1f6d5edc5dac6", size = 27310 }, + { url = "https://files.pythonhosted.org/packages/c3/4a/85e612787d0796878b3b4f6bd53dcd5484b6fe7b64cc6fc7b6e6a04cf835/argon2_cffi_bindings-26.1.0-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0c3103fcff20183e593459cfea6e012281c0e76ae3ed8b5565ad1b92eac3990", size = 26771 }, + { url = "https://files.pythonhosted.org/packages/f6/84/ccb003b6f9969820e87656398f4d49c857def71a85ca1588a0e809afd7ce/argon2_cffi_bindings-26.1.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c49e853a3bef9dd10329f31f702e7fa9b5c58229ff9c2ff6d069efaf09177c08", size = 27569 }, + { url = "https://files.pythonhosted.org/packages/88/07/c26b76debf0998ee08fbe947ab2058ac5de37d4b9d46b06c17abaa6c4ce9/argon2_cffi_bindings-26.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:6376d4b3aca039375ca8bf92f770da0ec424a1ce3a37077a8d3c557411aa56ca", size = 27279 }, + { url = "https://files.pythonhosted.org/packages/ee/0d/ead6ddc029f91bc9b9390686dad3c808ab08100d348f6266b5f93f8970ee/argon2_cffi_bindings-26.1.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:9bacedc04b0402837586a17f0919e3dfdd95291f441f1f56bd80ec274c2840a1", size = 27774 }, + { url = "https://files.pythonhosted.org/packages/7d/47/c108530d9eb86036b78d3af4de28b83b4a2d9a70512bd10ff8e59966aab4/argon2_cffi_bindings-26.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:76ae29acace5d33355344612844d588e19deaaba4639d8bb01601e4b1418ef36", size = 26933 }, + { url = "https://files.pythonhosted.org/packages/a9/02/0bfc59e781c89acf64c31c388aade9d9d1c1ea38aa1ba1292fe07f607fe9/argon2_cffi_bindings-26.1.0-cp315-cp315t-win32.whl", hash = "sha256:df612391feca41c44d20118f3b88d1b86419465cd1f5496859f715ca60ec2210", size = 24875 }, + { url = "https://files.pythonhosted.org/packages/61/c7/c3e46068cddffccecb8ad94d71135e9bf62bbc789589e7dfadc7c6f59214/argon2_cffi_bindings-26.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:1a0a29ed86960e44eaace7e081bdfab4f08b012fd96ec8edba71e2ad020939e4", size = 26655 }, + { url = "https://files.pythonhosted.org/packages/f4/ca/18b9c8c45fecf34b9100ec6d7946057f14a158f2eaa20ea123a3e82351cb/argon2_cffi_bindings-26.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d157ddfab1e8b21f2f1dedda9c09645d98b5ed0b667b0626be600a345d426440", size = 25376 }, + { url = "https://files.pythonhosted.org/packages/94/66/7ff138b7a61a6ec4eb8ad4a98696498915492a5ffa190e937ca5f2827e0a/argon2_cffi_bindings-26.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7014ab7e6f5d8511af92544667a0346ea6dfc314ea9a7cad1dba9fdb5c9a6e33", size = 23059 }, + { url = "https://files.pythonhosted.org/packages/de/6d/f120f8b4882da540b5e1375a11c85cfe37b3c671cfae1cdac797ea23e76b/argon2_cffi_bindings-26.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:242bb0cda2ae3650764fc194593d9ea45fc9e72729acd89778c7cfe184cec2a5", size = 24869 }, + { url = "https://files.pythonhosted.org/packages/04/50/92811103e1042af1379741db7fd4a6d0f6e4ee4512e2c50cbd0d344cf0d8/argon2_cffi_bindings-26.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b70225b5fd1e0d2ef4f7fd30d24658454535f0924dff0caca5dc08efbbbadfbb", size = 24219 }, + { url = "https://files.pythonhosted.org/packages/47/f2/1f8548c44c0036ae8ac1d6197300570b0af8ec120fc10b5bd8188f507376/argon2_cffi_bindings-26.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1af817e84578ef8b7295ad17de0f9896e4c8520dbf2233c7aa5aa3d487256fc4", size = 23589 }, + { url = "https://files.pythonhosted.org/packages/a0/b9/97f0370f99611b14efd384918613dd5cbda75f28d9bb1b677aacfeaa17df/argon2_cffi_bindings-26.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:19b562b1de4b9052ef1214a2821c44b6e6f22945daa102c32ae4eff929d8b6d8", size = 23055 }, + { url = "https://files.pythonhosted.org/packages/ae/70/7eb3fe7bf00103cbbb569c51aef150661f22b734a782673a600ff0f52309/argon2_cffi_bindings-26.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49d525938467d52c923a890153c99087c9d5a937d1f6b585dbdba34ec82e397a", size = 24869 }, + { url = "https://files.pythonhosted.org/packages/5b/4b/9d5919c6cb1f15df7406af0f99b048bd93936f112e3e8f4c8077bc2a9110/argon2_cffi_bindings-26.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b0bcac4d490a237e18cf91f57352920c29f77f2fa39efd0813fb81298bf17ba", size = 24216 }, + { url = "https://files.pythonhosted.org/packages/a3/34/32109943bace7729233cc4ee78530baa306d8cc3c6501a64ba8cb3b58129/argon2_cffi_bindings-26.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:0cc40f7b4050bb93eb67de95d2d759322fc7ce4930b9d645581ecf4913ec651e", size = 23584 }, ] [[package]] @@ -129,18 +129,18 @@ dependencies = [ { name = "python-dateutil" }, { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797 }, ] [[package]] name = "asttokens" version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/1e/faf0f247f6f881b98fc4d6d07e14085cb89d13665084e6d6ac1dc2c03d0b/asttokens-3.0.2.tar.gz", hash = "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2", size = 63136, upload-time = "2026-07-12T03:31:49.084Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/1e/faf0f247f6f881b98fc4d6d07e14085cb89d13665084e6d6ac1dc2c03d0b/asttokens-3.0.2.tar.gz", hash = "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2", size = 63136 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/2b/04b8a15f3a1c77bc79ddf5c73875327f34b4fa75982df2b76e45e402d364/asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933", size = 28702, upload-time = "2026-07-12T03:31:47.542Z" }, + { url = "https://files.pythonhosted.org/packages/d4/2b/04b8a15f3a1c77bc79ddf5c73875327f34b4fa75982df2b76e45e402d364/asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933", size = 28702 }, ] [[package]] @@ -150,36 +150,36 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e8/1f/989ecfef8e64109a489fff357450cb73fa73a865a92bd8c272170a6922c2/async_lru-2.3.0.tar.gz", hash = "sha256:89bdb258a0140d7313cf8f4031d816a042202faa61d0ab310a0a538baa1c24b6", size = 16332, upload-time = "2026-03-19T01:04:32.413Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/1f/989ecfef8e64109a489fff357450cb73fa73a865a92bd8c272170a6922c2/async_lru-2.3.0.tar.gz", hash = "sha256:89bdb258a0140d7313cf8f4031d816a042202faa61d0ab310a0a538baa1c24b6", size = 16332 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl", hash = "sha256:eea27b01841909316f2cc739807acea1c623df2be8c5cfad7583286397bb8315", size = 8403, upload-time = "2026-03-19T01:04:30.883Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl", hash = "sha256:eea27b01841909316f2cc739807acea1c623df2be8c5cfad7583286397bb8315", size = 8403 }, ] [[package]] name = "attrs" version = "25.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615 }, ] [[package]] name = "babel" version = "2.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554 } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845 }, ] [[package]] name = "backports-tarfile" version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181 }, ] [[package]] @@ -190,9 +190,9 @@ dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571 } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924 }, ] [[package]] @@ -202,33 +202,33 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/ca/1d5fab0fedaf5c2f376d9746d447cdce04241c433602c3861693361ce54c/biopython-1.85.tar.gz", hash = "sha256:5dafab74059de4e78f49f6b5684eddae6e7ce46f09cfa059c1d1339e8b1ea0a6", size = 19909902, upload-time = "2025-01-15T15:06:51.997Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/de/79824470fdc5c2aedeb1bab637d24eab654a7e9fbb7070f779fd944fd1ca/biopython-1.85-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6308053a61f3bdbb11504ece4cf24e264c6f1d6fad278f7e59e6b84b0d9a7b4", size = 2787511, upload-time = "2025-01-15T15:11:56.656Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d6/3c90df2ebc4f2a522235f4391392253e528f19f5fb6a52396a2316e17064/biopython-1.85-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:434dd23e972b0c89e128f2ebbd16b38075d609184f4f1fd16368035f923019c2", size = 2765456, upload-time = "2025-01-15T15:12:05.177Z" }, - { url = "https://files.pythonhosted.org/packages/ff/d7/34233c4ee906b088b199eb2af9c4107384a547d44b5960257ef2bf2cc3c8/biopython-1.85-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a08d082e85778259a83501063871e00ba57336136403b75050eea14d523c00a", size = 3225137, upload-time = "2025-01-15T15:12:15.847Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6b/e7a5ae49ef7e8f81720593db22e884c1c8e3504e0e235da0395758a5c7d0/biopython-1.85-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93464e629950e4df87d810125dc4e904538a4344b924f340908ea5bc95db986", size = 3245258, upload-time = "2025-01-15T15:12:21.012Z" }, - { url = "https://files.pythonhosted.org/packages/68/2c/0035656dbe51ac0bbe81321172b43c37b38408f8d136abaac443e9f44b62/biopython-1.85-cp310-cp310-win32.whl", hash = "sha256:f2f45ab3f1e43fdaa697fd753148999090298623278097c19c2c3c0ba134e57c", size = 2786505, upload-time = "2025-01-15T15:12:26.545Z" }, - { url = "https://files.pythonhosted.org/packages/32/a4/f20d5830dbd8654c13e763daef429fa32ef0b2b09749ac427d045cc81976/biopython-1.85-cp310-cp310-win_amd64.whl", hash = "sha256:7c8326cd2825ba166abecaf72843b9b15823affd6cec04fde65f0d2526767da4", size = 2820961, upload-time = "2025-01-15T15:12:32.364Z" }, - { url = "https://files.pythonhosted.org/packages/c3/73/c3a1323a3fe0d07212b09c04fb903e2cbb98aebfbb58e55e8717473e1bc0/biopython-1.85-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db8822adab0cd75a6e6ae845acf312addd8eab5f9b731c191454b961fc2c2cdc", size = 2787585, upload-time = "2025-01-15T15:12:36.927Z" }, - { url = "https://files.pythonhosted.org/packages/ff/cf/299524e896fa49beb7588143e1509cce4848572215ebafb8eea83a861820/biopython-1.85-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e2bbe58cc1a592b239ef6d68396745d3fbfaafc668ce38283871d8ff070dbab", size = 2765608, upload-time = "2025-01-15T15:12:43.853Z" }, - { url = "https://files.pythonhosted.org/packages/b1/dd/be3e95b72a35ee6d52c84412e15af828951e5c69175080d4619985fd54ce/biopython-1.85-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5916eb56df7ecd4a3babc07a48d4894c40cfb45dc18ccda1c148d0131017ce04", size = 3237552, upload-time = "2025-01-15T15:12:49.046Z" }, - { url = "https://files.pythonhosted.org/packages/25/9c/612821b946930b6caa5d795cfe4169ed6a522562eced9776914be7efaf21/biopython-1.85-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0cec8833bf3036049129944ee5382dd576dac9670c3814ff2314b52aa94f199", size = 3257287, upload-time = "2025-01-15T15:12:56.168Z" }, - { url = "https://files.pythonhosted.org/packages/a1/77/316e51dd42fd8225429574a268bdc627ce4f42067a3976c4c8c457a42023/biopython-1.85-cp311-cp311-win32.whl", hash = "sha256:cf88a4c8d8af13138be115949639a5e4a201618185a72ff09adbe175b7946b28", size = 2786411, upload-time = "2025-01-15T15:13:02.754Z" }, - { url = "https://files.pythonhosted.org/packages/f2/11/3c4e8c049b91998bbbd51ddebc6f790b1aa66211babfbf5ff008a72fb1f9/biopython-1.85-cp311-cp311-win_amd64.whl", hash = "sha256:d3c99db65d57ae4fc5034e42ac6cd8ddce069e664903f04c8a4f684d7609d6fa", size = 2820912, upload-time = "2025-01-15T15:13:10.499Z" }, - { url = "https://files.pythonhosted.org/packages/a3/25/e46f05359df7f0049c3adc5eaeb9aee0f5fbde1d959d05c78eb1de8f4d12/biopython-1.85-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc5b981b9e3060db7c355b6145dfe3ce0b6572e1601b31211f6d742b10543874", size = 2789327, upload-time = "2025-01-15T15:13:17.086Z" }, - { url = "https://files.pythonhosted.org/packages/54/5b/8b3b029c94c63ab4c1781d141615b4a837e658422381d460c5573d5d8262/biopython-1.85-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6fe47d704c2d3afac99aeb461219ec5f00273120d2d99835dc0a9a617f520141", size = 2765805, upload-time = "2025-01-15T15:13:26.92Z" }, - { url = "https://files.pythonhosted.org/packages/69/0a/9a8a38eff03c4607b9cec8d0e08c76b346b1cee1f77bc6d00efebfc7ec83/biopython-1.85-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54e495239e623660ad367498c2f7a1a294b1997ba603f2ceafb36fd18f0eba6", size = 3249276, upload-time = "2025-01-15T15:13:36.639Z" }, - { url = "https://files.pythonhosted.org/packages/b4/0d/b7a0f10f5100dcf51ae36ba31490169bfa45617323bd82af43e1fb0098fb/biopython-1.85-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d024ad48997ad53d53a77da24b072aaba8a550bd816af8f2e7e606a9918a3b43", size = 3268869, upload-time = "2025-01-15T15:13:44.009Z" }, - { url = "https://files.pythonhosted.org/packages/07/51/646a4b7bdb4c1153786a70d33588ed09178bfcdda0542dfdc976294f4312/biopython-1.85-cp312-cp312-win32.whl", hash = "sha256:6985e17a2911defcbd30275a12f5ed5de2765e4bc91a60439740d572fdbfdf43", size = 2787011, upload-time = "2025-01-15T15:13:48.958Z" }, - { url = "https://files.pythonhosted.org/packages/c1/84/c583fa2ac6e7d392d24ebdc5c99e95e517507de22cf143efb6cf1fc93ff5/biopython-1.85-cp312-cp312-win_amd64.whl", hash = "sha256:d6f8efb2db03f2ec115c5e8c764dbadf635e0c9ecd4c0e91fc8216c1b62f85f5", size = 2820972, upload-time = "2025-01-15T15:13:54.475Z" }, - { url = "https://files.pythonhosted.org/packages/c3/3f/65814bf221f0bfdd2633830e573ac8594794686f54110571dce98cc32fd3/biopython-1.85-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bae6f17262f20e9587d419ba5683e9dc93a31ee1858b98fa0cff203694d5b786", size = 2789844, upload-time = "2025-01-15T15:14:01.171Z" }, - { url = "https://files.pythonhosted.org/packages/be/61/1443ce34226e261c20ae4a154b2bab72c109cf31415c92c54c1aada36b00/biopython-1.85-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1e4918e6399ab0183dd863527fec18b53b7c61b6f0ef95db84b4adfa430ce75", size = 2765767, upload-time = "2025-01-15T15:14:07.096Z" }, - { url = "https://files.pythonhosted.org/packages/53/51/bec4c763c704e2715691bb087cfab5907804a1bbef5873588698cece1a30/biopython-1.85-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86e487b6fe0f20a2b0138cb53f3d4dc26a7e4060ac4cb6fb1d19e194d445ef46", size = 3248867, upload-time = "2025-01-15T15:14:12.822Z" }, - { url = "https://files.pythonhosted.org/packages/9e/a4/552f20253a7c95988067c4955831bd17dd9b864fd5c215d15c2f63f2d415/biopython-1.85-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:327184048b5a50634ae0970119bcb8a35b76d7cefb2658a01f772915f2fb7686", size = 3268479, upload-time = "2025-01-15T15:14:20.414Z" }, - { url = "https://files.pythonhosted.org/packages/97/f4/6dfc6ef3e0997f792f93893551d4a9bf7f6052a70d34f4e1b1e5e4a359f6/biopython-1.85-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66b08905fb1b3f5194f908aaf04bbad474c4e3eaebad6d9f889a04e64dd1faf4", size = 3192338, upload-time = "2025-01-15T15:14:26.843Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/73da6588fab3a7d734118f21ac619c0a949f51d3cf7b1b8343d175c33460/biopython-1.85-cp313-cp313-win32.whl", hash = "sha256:5a236ab1e2797c7dcf1577d80fdaafabead2908bc338eaed0aa1509dab769fef", size = 2787017, upload-time = "2025-01-15T15:14:33.802Z" }, - { url = "https://files.pythonhosted.org/packages/60/ff/fe8f03ac0ccc7219e0959010750c6ac1a5b1411c91c7f4ec3a37d8313673/biopython-1.85-cp313-cp313-win_amd64.whl", hash = "sha256:1b61593765e9ebdb71d73307d55fd4b46eb976608d329ae6803c084d90ed34c7", size = 2821012, upload-time = "2025-01-15T15:14:40.825Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/db/ca/1d5fab0fedaf5c2f376d9746d447cdce04241c433602c3861693361ce54c/biopython-1.85.tar.gz", hash = "sha256:5dafab74059de4e78f49f6b5684eddae6e7ce46f09cfa059c1d1339e8b1ea0a6", size = 19909902 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/de/79824470fdc5c2aedeb1bab637d24eab654a7e9fbb7070f779fd944fd1ca/biopython-1.85-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a6308053a61f3bdbb11504ece4cf24e264c6f1d6fad278f7e59e6b84b0d9a7b4", size = 2787511 }, + { url = "https://files.pythonhosted.org/packages/4b/d6/3c90df2ebc4f2a522235f4391392253e528f19f5fb6a52396a2316e17064/biopython-1.85-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:434dd23e972b0c89e128f2ebbd16b38075d609184f4f1fd16368035f923019c2", size = 2765456 }, + { url = "https://files.pythonhosted.org/packages/ff/d7/34233c4ee906b088b199eb2af9c4107384a547d44b5960257ef2bf2cc3c8/biopython-1.85-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a08d082e85778259a83501063871e00ba57336136403b75050eea14d523c00a", size = 3225137 }, + { url = "https://files.pythonhosted.org/packages/ae/6b/e7a5ae49ef7e8f81720593db22e884c1c8e3504e0e235da0395758a5c7d0/biopython-1.85-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93464e629950e4df87d810125dc4e904538a4344b924f340908ea5bc95db986", size = 3245258 }, + { url = "https://files.pythonhosted.org/packages/68/2c/0035656dbe51ac0bbe81321172b43c37b38408f8d136abaac443e9f44b62/biopython-1.85-cp310-cp310-win32.whl", hash = "sha256:f2f45ab3f1e43fdaa697fd753148999090298623278097c19c2c3c0ba134e57c", size = 2786505 }, + { url = "https://files.pythonhosted.org/packages/32/a4/f20d5830dbd8654c13e763daef429fa32ef0b2b09749ac427d045cc81976/biopython-1.85-cp310-cp310-win_amd64.whl", hash = "sha256:7c8326cd2825ba166abecaf72843b9b15823affd6cec04fde65f0d2526767da4", size = 2820961 }, + { url = "https://files.pythonhosted.org/packages/c3/73/c3a1323a3fe0d07212b09c04fb903e2cbb98aebfbb58e55e8717473e1bc0/biopython-1.85-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db8822adab0cd75a6e6ae845acf312addd8eab5f9b731c191454b961fc2c2cdc", size = 2787585 }, + { url = "https://files.pythonhosted.org/packages/ff/cf/299524e896fa49beb7588143e1509cce4848572215ebafb8eea83a861820/biopython-1.85-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e2bbe58cc1a592b239ef6d68396745d3fbfaafc668ce38283871d8ff070dbab", size = 2765608 }, + { url = "https://files.pythonhosted.org/packages/b1/dd/be3e95b72a35ee6d52c84412e15af828951e5c69175080d4619985fd54ce/biopython-1.85-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5916eb56df7ecd4a3babc07a48d4894c40cfb45dc18ccda1c148d0131017ce04", size = 3237552 }, + { url = "https://files.pythonhosted.org/packages/25/9c/612821b946930b6caa5d795cfe4169ed6a522562eced9776914be7efaf21/biopython-1.85-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0cec8833bf3036049129944ee5382dd576dac9670c3814ff2314b52aa94f199", size = 3257287 }, + { url = "https://files.pythonhosted.org/packages/a1/77/316e51dd42fd8225429574a268bdc627ce4f42067a3976c4c8c457a42023/biopython-1.85-cp311-cp311-win32.whl", hash = "sha256:cf88a4c8d8af13138be115949639a5e4a201618185a72ff09adbe175b7946b28", size = 2786411 }, + { url = "https://files.pythonhosted.org/packages/f2/11/3c4e8c049b91998bbbd51ddebc6f790b1aa66211babfbf5ff008a72fb1f9/biopython-1.85-cp311-cp311-win_amd64.whl", hash = "sha256:d3c99db65d57ae4fc5034e42ac6cd8ddce069e664903f04c8a4f684d7609d6fa", size = 2820912 }, + { url = "https://files.pythonhosted.org/packages/a3/25/e46f05359df7f0049c3adc5eaeb9aee0f5fbde1d959d05c78eb1de8f4d12/biopython-1.85-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc5b981b9e3060db7c355b6145dfe3ce0b6572e1601b31211f6d742b10543874", size = 2789327 }, + { url = "https://files.pythonhosted.org/packages/54/5b/8b3b029c94c63ab4c1781d141615b4a837e658422381d460c5573d5d8262/biopython-1.85-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6fe47d704c2d3afac99aeb461219ec5f00273120d2d99835dc0a9a617f520141", size = 2765805 }, + { url = "https://files.pythonhosted.org/packages/69/0a/9a8a38eff03c4607b9cec8d0e08c76b346b1cee1f77bc6d00efebfc7ec83/biopython-1.85-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54e495239e623660ad367498c2f7a1a294b1997ba603f2ceafb36fd18f0eba6", size = 3249276 }, + { url = "https://files.pythonhosted.org/packages/b4/0d/b7a0f10f5100dcf51ae36ba31490169bfa45617323bd82af43e1fb0098fb/biopython-1.85-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d024ad48997ad53d53a77da24b072aaba8a550bd816af8f2e7e606a9918a3b43", size = 3268869 }, + { url = "https://files.pythonhosted.org/packages/07/51/646a4b7bdb4c1153786a70d33588ed09178bfcdda0542dfdc976294f4312/biopython-1.85-cp312-cp312-win32.whl", hash = "sha256:6985e17a2911defcbd30275a12f5ed5de2765e4bc91a60439740d572fdbfdf43", size = 2787011 }, + { url = "https://files.pythonhosted.org/packages/c1/84/c583fa2ac6e7d392d24ebdc5c99e95e517507de22cf143efb6cf1fc93ff5/biopython-1.85-cp312-cp312-win_amd64.whl", hash = "sha256:d6f8efb2db03f2ec115c5e8c764dbadf635e0c9ecd4c0e91fc8216c1b62f85f5", size = 2820972 }, + { url = "https://files.pythonhosted.org/packages/c3/3f/65814bf221f0bfdd2633830e573ac8594794686f54110571dce98cc32fd3/biopython-1.85-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bae6f17262f20e9587d419ba5683e9dc93a31ee1858b98fa0cff203694d5b786", size = 2789844 }, + { url = "https://files.pythonhosted.org/packages/be/61/1443ce34226e261c20ae4a154b2bab72c109cf31415c92c54c1aada36b00/biopython-1.85-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1e4918e6399ab0183dd863527fec18b53b7c61b6f0ef95db84b4adfa430ce75", size = 2765767 }, + { url = "https://files.pythonhosted.org/packages/53/51/bec4c763c704e2715691bb087cfab5907804a1bbef5873588698cece1a30/biopython-1.85-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86e487b6fe0f20a2b0138cb53f3d4dc26a7e4060ac4cb6fb1d19e194d445ef46", size = 3248867 }, + { url = "https://files.pythonhosted.org/packages/9e/a4/552f20253a7c95988067c4955831bd17dd9b864fd5c215d15c2f63f2d415/biopython-1.85-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:327184048b5a50634ae0970119bcb8a35b76d7cefb2658a01f772915f2fb7686", size = 3268479 }, + { url = "https://files.pythonhosted.org/packages/97/f4/6dfc6ef3e0997f792f93893551d4a9bf7f6052a70d34f4e1b1e5e4a359f6/biopython-1.85-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66b08905fb1b3f5194f908aaf04bbad474c4e3eaebad6d9f889a04e64dd1faf4", size = 3192338 }, + { url = "https://files.pythonhosted.org/packages/9d/8c/73da6588fab3a7d734118f21ac619c0a949f51d3cf7b1b8343d175c33460/biopython-1.85-cp313-cp313-win32.whl", hash = "sha256:5a236ab1e2797c7dcf1577d80fdaafabead2908bc338eaed0aa1509dab769fef", size = 2787017 }, + { url = "https://files.pythonhosted.org/packages/60/ff/fe8f03ac0ccc7219e0959010750c6ac1a5b1411c91c7f4ec3a37d8313673/biopython-1.85-cp313-cp313-win_amd64.whl", hash = "sha256:1b61593765e9ebdb71d73307d55fd4b46eb976608d329ae6803c084d90ed34c7", size = 2821012 }, ] [[package]] @@ -238,9 +238,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "webencodings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/3c/e12ac860709702bd5ebeb9b56a4fe334f1001246ee1b8f2b7ee28912df7d/bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", size = 204857, upload-time = "2026-06-05T13:01:13.734Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/3c/e12ac860709702bd5ebeb9b56a4fe334f1001246ee1b8f2b7ee28912df7d/bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", size = 204857 } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl", hash = "sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081", size = 165109, upload-time = "2026-06-05T13:01:12.504Z" }, + { url = "https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl", hash = "sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081", size = 165109 }, ] [package.optional-dependencies] @@ -259,18 +259,18 @@ dependencies = [ { name = "pyproject-hooks" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/67/4898a44ea4f3f8e213b0954ec0aa0a16971d62a6212d6ea3931e97115b99/build-1.6.1.tar.gz", hash = "sha256:51cc11666391ab6f092070437ac747002ff46f3e4113a3622177ee6b488bfc53", size = 113427, upload-time = "2026-09-10T07:56:00.417Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/67/4898a44ea4f3f8e213b0954ec0aa0a16971d62a6212d6ea3931e97115b99/build-1.6.1.tar.gz", hash = "sha256:51cc11666391ab6f092070437ac747002ff46f3e4113a3622177ee6b488bfc53", size = 113427 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/9b/9fb3585dabcd73a1b2a6267f63f62649347c9e6d072c9fde365b105abb2c/build-1.6.1-py3-none-any.whl", hash = "sha256:ecd351a4be9d35a9eaaba244a7687143c9c7d4aea6ac964e7e7ddab20cbcf4e7", size = 31179, upload-time = "2026-09-10T07:55:59.148Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9b/9fb3585dabcd73a1b2a6267f63f62649347c9e6d072c9fde365b105abb2c/build-1.6.1-py3-none-any.whl", hash = "sha256:ecd351a4be9d35a9eaaba244a7687143c9c7d4aea6ac964e7e7ddab20cbcf4e7", size = 31179 }, ] [[package]] name = "certifi" version = "2025.10.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286 }, ] [[package]] @@ -280,297 +280,297 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/d2/2cde336b375f55c76ca670f0be3978cc048e31e24f3b4d7ce8473150a388/cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be", size = 183779, upload-time = "2026-08-03T21:19:15.602Z" }, - { url = "https://files.pythonhosted.org/packages/94/1a/4b2f7c92293ba05cbd4a9a1b28faaf0326272d9488e6354657571c48a7aa/cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b", size = 184178, upload-time = "2026-08-03T21:19:16.67Z" }, - { url = "https://files.pythonhosted.org/packages/17/0b/ba385d8ccedf926c3cd06e8e2f327027da5afe5f0eb30f1f7bc43ac55125/cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004", size = 211037, upload-time = "2026-08-03T21:19:17.705Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b9/0f2e58b2cefa33255bff36935d42b13180fe559bba82596540eb404bde7d/cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9", size = 218652, upload-time = "2026-08-03T21:19:18.735Z" }, - { url = "https://files.pythonhosted.org/packages/37/15/180e0dab27b9312c7479003d14c9e547634b7dcb934e2cc4650e1b131a7a/cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98", size = 205422, upload-time = "2026-08-03T21:19:19.96Z" }, - { url = "https://files.pythonhosted.org/packages/18/d4/03026f0c850cbbaa9030750490225b4a7f4d524ea4df72c3cc740a90f4ef/cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9", size = 205444, upload-time = "2026-08-03T21:19:21.246Z" }, - { url = "https://files.pythonhosted.org/packages/75/77/60bebf6f818bec84210ac5b6979ce4eeadce6fbbaabc9c7ab23e506d1ce5/cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6", size = 218742, upload-time = "2026-08-03T21:19:22.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ae/679bf47e73fd77b352171727f07de559a003f14de5d02b904a6ec1fa73ca/cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf", size = 221054, upload-time = "2026-08-03T21:19:23.694Z" }, - { url = "https://files.pythonhosted.org/packages/09/b8/eefc0e06913b70aa153bf74c946094a18f58fd4aff11b7f372bfdfdca050/cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659", size = 213489, upload-time = "2026-08-03T21:19:24.922Z" }, - { url = "https://files.pythonhosted.org/packages/6f/13/4e56852824a03cdf68523a35686f1c28eacd4bd30a7b0a78e682e6e6e1d3/cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9", size = 220241, upload-time = "2026-08-03T21:19:26.214Z" }, - { url = "https://files.pythonhosted.org/packages/99/7f/040f9e163e4acac3ee3d85b02d00b2576e7ca980d8785f0a3a5f1a9bf7f5/cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41", size = 174578, upload-time = "2026-08-03T21:19:27.338Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0b/644a2ec1a4eaba49c2939410bb1eb1d25b09d6d0582f5d2f95c537043725/cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1", size = 185082, upload-time = "2026-08-03T21:19:28.409Z" }, - { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" }, - { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" }, - { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" }, - { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" }, - { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" }, - { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" }, - { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" }, - { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" }, - { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" }, - { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" }, - { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" }, - { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" }, - { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, - { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, - { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, - { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, - { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, - { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, - { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, - { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, - { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, - { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, - { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, - { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, - { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, - { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, - { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, - { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, - { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, - { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, - { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, - { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, - { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, - { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, - { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, - { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, - { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, - { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, - { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, - { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, - { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, - { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, - { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, - { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, - { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, - { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, - { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, - { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, - { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, - { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, - { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, - { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, - { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, - { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, - { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, - { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, - { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, - { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, - { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, - { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, - { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, - { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, - { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, - { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, - { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, - { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, - { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, - { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, - { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, - { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, - { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, - { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, - { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, - { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, - { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, - { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, - { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, - { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, - { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, - { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, - { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, - { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/d2/2cde336b375f55c76ca670f0be3978cc048e31e24f3b4d7ce8473150a388/cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be", size = 183779 }, + { url = "https://files.pythonhosted.org/packages/94/1a/4b2f7c92293ba05cbd4a9a1b28faaf0326272d9488e6354657571c48a7aa/cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b", size = 184178 }, + { url = "https://files.pythonhosted.org/packages/17/0b/ba385d8ccedf926c3cd06e8e2f327027da5afe5f0eb30f1f7bc43ac55125/cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004", size = 211037 }, + { url = "https://files.pythonhosted.org/packages/a3/b9/0f2e58b2cefa33255bff36935d42b13180fe559bba82596540eb404bde7d/cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9", size = 218652 }, + { url = "https://files.pythonhosted.org/packages/37/15/180e0dab27b9312c7479003d14c9e547634b7dcb934e2cc4650e1b131a7a/cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98", size = 205422 }, + { url = "https://files.pythonhosted.org/packages/18/d4/03026f0c850cbbaa9030750490225b4a7f4d524ea4df72c3cc740a90f4ef/cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9", size = 205444 }, + { url = "https://files.pythonhosted.org/packages/75/77/60bebf6f818bec84210ac5b6979ce4eeadce6fbbaabc9c7ab23e506d1ce5/cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6", size = 218742 }, + { url = "https://files.pythonhosted.org/packages/b0/ae/679bf47e73fd77b352171727f07de559a003f14de5d02b904a6ec1fa73ca/cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf", size = 221054 }, + { url = "https://files.pythonhosted.org/packages/09/b8/eefc0e06913b70aa153bf74c946094a18f58fd4aff11b7f372bfdfdca050/cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659", size = 213489 }, + { url = "https://files.pythonhosted.org/packages/6f/13/4e56852824a03cdf68523a35686f1c28eacd4bd30a7b0a78e682e6e6e1d3/cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9", size = 220241 }, + { url = "https://files.pythonhosted.org/packages/99/7f/040f9e163e4acac3ee3d85b02d00b2576e7ca980d8785f0a3a5f1a9bf7f5/cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41", size = 174578 }, + { url = "https://files.pythonhosted.org/packages/ba/0b/644a2ec1a4eaba49c2939410bb1eb1d25b09d6d0582f5d2f95c537043725/cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1", size = 185082 }, + { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838 }, + { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168 }, + { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805 }, + { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716 }, + { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569 }, + { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907 }, + { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807 }, + { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252 }, + { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214 }, + { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408 }, + { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470 }, + { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096 }, + { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941 }, + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821 }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719 }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799 }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389 }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249 }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775 }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822 }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232 }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597 }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292 }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919 }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093 }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248 }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908 }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805 }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764 }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722 }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369 }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175 }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670 }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824 }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148 }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564 }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263 }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688 }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078 }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064 }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720 }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964 }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962 }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328 }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985 }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530 }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525 }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053 }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213 }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682 }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949 }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947 }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504 }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259 }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864 }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538 }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688 }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803 }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763 }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688 }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868 }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104 }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402 }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043 }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737 }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933 }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002 }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271 }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919 }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529 }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630 }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134 }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197 }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683 }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897 }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935 }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464 }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262 }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779 }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520 }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673 }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835 }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705 }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539 }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707 }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772 }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360 }, ] [[package]] name = "charset-normalizer" version = "3.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, - { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, - { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, - { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, - { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, - { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, - { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, - { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, - { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, - { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, - { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, - { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, - { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, - { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, - { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, - { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, - { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, - { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, - { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, - { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, - { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, - { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, - { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, - { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, - { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, - { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, - { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, - { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, - { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, - { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, - { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, - { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, - { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, - { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, - { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, - { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, - { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, - { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, - { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695 }, + { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153 }, + { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428 }, + { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627 }, + { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388 }, + { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077 }, + { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631 }, + { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210 }, + { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739 }, + { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825 }, + { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452 }, + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483 }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520 }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876 }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083 }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295 }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379 }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018 }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430 }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600 }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616 }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108 }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655 }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223 }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366 }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104 }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830 }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854 }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670 }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501 }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173 }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822 }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543 }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326 }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008 }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196 }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819 }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350 }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644 }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468 }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187 }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699 }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580 }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366 }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342 }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995 }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640 }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636 }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939 }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580 }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870 }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797 }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224 }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086 }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400 }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175 }, ] [[package]] name = "click" version = "8.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235 } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251 }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] [[package]] name = "comm" version = "0.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319 } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294 }, ] [[package]] name = "coverage" version = "7.10.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, - { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, - { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, - { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, - { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, - { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, - { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, - { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, - { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, - { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, - { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, - { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, - { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, - { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, - { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, - { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, - { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, - { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, - { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, - { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, - { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, - { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, - { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, - { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, - { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, - { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, - { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, - { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, - { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, - { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, - { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, - { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, - { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, - { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, - { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, - { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, - { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, - { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, - { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, - { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, - { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, - { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, - { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, - { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, - { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, - { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, - { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, - { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, - { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, - { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, - { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, - { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, - { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, - { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, - { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, - { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, - { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, - { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, - { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, - { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, - { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, - { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, - { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, - { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, - { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, - { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, - { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, - { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, - { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, - { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, - { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, - { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, - { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, - { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, - { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, - { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, - { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, - { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, - { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987 }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388 }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148 }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958 }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819 }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754 }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860 }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877 }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108 }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752 }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497 }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392 }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102 }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505 }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898 }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831 }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937 }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021 }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626 }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682 }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402 }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320 }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536 }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425 }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103 }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290 }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515 }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020 }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769 }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901 }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413 }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820 }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941 }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519 }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375 }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699 }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512 }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147 }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320 }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575 }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568 }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174 }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447 }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779 }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604 }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497 }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350 }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111 }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746 }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541 }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170 }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029 }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259 }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592 }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768 }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995 }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546 }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544 }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308 }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920 }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434 }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403 }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469 }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731 }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302 }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578 }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629 }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162 }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517 }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632 }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520 }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455 }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287 }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946 }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009 }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804 }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384 }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047 }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266 }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767 }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931 }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186 }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470 }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626 }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386 }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852 }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534 }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784 }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905 }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922 }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952 }, ] [package.optional-dependencies] @@ -586,90 +586,96 @@ dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" }, - { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" }, - { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" }, - { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" }, - { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" }, - { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" }, - { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" }, - { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" }, - { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" }, - { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" }, - { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" }, - { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" }, - { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" }, - { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" }, - { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" }, - { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" }, - { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" }, - { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" }, - { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" }, - { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" }, - { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" }, - { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" }, - { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" }, - { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" }, - { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" }, - { url = "https://files.pythonhosted.org/packages/14/9a/6d3a4d7852e22d657438b7bf51f66102c7d71c0e1fafeec652281d0403e5/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f", size = 4698675, upload-time = "2026-08-25T19:45:28.658Z" }, - { url = "https://files.pythonhosted.org/packages/73/35/5c3717edf9e68a0550ce04e28eab493fe545eccd81742af03f6a75fe260b/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671", size = 4707410, upload-time = "2026-08-25T19:45:30.816Z" }, - { url = "https://files.pythonhosted.org/packages/1d/e0/e786934472e3ac4ecdecc7b129a0ca1a2a40dffdafcf2c3ea9d4397f8def/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e", size = 4698378, upload-time = "2026-08-25T19:45:33.043Z" }, - { url = "https://files.pythonhosted.org/packages/51/cf/5b3f53a0b74d122f023476ede40ba5d3e70d5cf475f73b899740d26a4fb2/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6", size = 4706889, upload-time = "2026-08-25T19:45:35.086Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133 }, + { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478 }, + { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726 }, + { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524 }, + { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720 }, + { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866 }, + { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028 }, + { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405 }, + { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230 }, + { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596 }, + { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082 }, + { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817 }, + { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300 }, + { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039 }, + { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388 }, + { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293 }, + { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031 }, + { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344 }, + { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201 }, + { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023 }, + { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362 }, + { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247 }, + { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900 }, + { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357 }, + { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474 }, + { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862 }, + { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942 }, + { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347 }, + { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050 }, + { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955 }, + { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694 }, + { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413 }, + { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355 }, + { url = "https://files.pythonhosted.org/packages/14/9a/6d3a4d7852e22d657438b7bf51f66102c7d71c0e1fafeec652281d0403e5/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f", size = 4698675 }, + { url = "https://files.pythonhosted.org/packages/73/35/5c3717edf9e68a0550ce04e28eab493fe545eccd81742af03f6a75fe260b/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671", size = 4707410 }, + { url = "https://files.pythonhosted.org/packages/1d/e0/e786934472e3ac4ecdecc7b129a0ca1a2a40dffdafcf2c3ea9d4397f8def/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e", size = 4698378 }, + { url = "https://files.pythonhosted.org/packages/51/cf/5b3f53a0b74d122f023476ede40ba5d3e70d5cf475f73b899740d26a4fb2/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6", size = 4706889 }, ] [[package]] name = "debugpy" version = "1.8.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/9d/3cb6693342acf96802dba89934a5b8da43c201d764ebd1f2c0514a3d42bd/debugpy-1.8.22.tar.gz", hash = "sha256:e489c7268e1c7b41e13b438d9c533d2a7af73fb59bf8cd30fead8286c1c39c4e", size = 1710877, upload-time = "2026-09-15T21:44:02.578Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/e9/ced401ba1088e1d2b3228dbbcae33e15aa8c08a1368433bedebfa8efcb02/debugpy-1.8.22-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:fa47099176d1d612bef69e9f74658a5687a9f05bd80fb5710f99959bace85735", size = 2128369, upload-time = "2026-09-15T21:44:04.775Z" }, - { url = "https://files.pythonhosted.org/packages/4f/97/5c8e839a7f73e92cd4a733408bb85bb764e829e52a9fb097f99d6e2cbbc0/debugpy-1.8.22-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:7bf29e0d8ce80b100d37fb333e1b193b790d962739f3067796c2b03ffdc9afee", size = 3022081, upload-time = "2026-09-15T21:44:06.382Z" }, - { url = "https://files.pythonhosted.org/packages/11/ed/fbb045d18a8b733a798bb834a03a20cd64f1446206d844b674bb638da6c3/debugpy-1.8.22-cp310-cp310-win32.whl", hash = "sha256:56b877b37ed73f0bf53ba7afc394816ff1eb5d701bae24a24ed9e1ea6f7ce34f", size = 5325298, upload-time = "2026-09-15T21:44:08.267Z" }, - { url = "https://files.pythonhosted.org/packages/82/7f/633160132b80b424c6816391f239682435c6b77cdd58288d04b3443b9f80/debugpy-1.8.22-cp310-cp310-win_amd64.whl", hash = "sha256:1bd0c6df3c68c0a3f71db8baa3780a953abb65537ec4b3bc6b935ad5b3b3d45c", size = 5353598, upload-time = "2026-09-15T21:44:10.125Z" }, - { url = "https://files.pythonhosted.org/packages/26/4a/8c24c588088c622df6dacdc0110e9cc81b44781fe7a408113441e76aa955/debugpy-1.8.22-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:66e4ac3d6e7026e83e7d93d7ee2f51dd4a4e8dff673578d424e60796893e5b2c", size = 2217799, upload-time = "2026-09-15T21:44:11.57Z" }, - { url = "https://files.pythonhosted.org/packages/c7/14/1c9ff33eba51da70a8dfe2277cc13153bb3ffec79282b9acfcdbe1641d30/debugpy-1.8.22-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:192b73e8d53bbd60225220c0943627bf249ac93d0ea3090e23c45f3e0ceb6a35", size = 3074375, upload-time = "2026-09-15T21:44:13.349Z" }, - { url = "https://files.pythonhosted.org/packages/10/e8/1fa16a94af7d8b86c3ca336d7d32b78cb2c475266d0bb889732d2bc50310/debugpy-1.8.22-cp311-cp311-win32.whl", hash = "sha256:745e1800ec2961e5660c1a317c0a20e28c0fef4de36c04f17a21c33f2b37a92a", size = 5258392, upload-time = "2026-09-15T21:44:15.182Z" }, - { url = "https://files.pythonhosted.org/packages/32/1a/c086b883a4561017bfdd83c1bc70bf2b05ab6540ff767dedf207cadab938/debugpy-1.8.22-cp311-cp311-win_amd64.whl", hash = "sha256:1e76339d5510bc17e9181dba9577508afcb21aad5728f1a55ef74d7d97d255f3", size = 5277955, upload-time = "2026-09-15T21:44:16.986Z" }, - { url = "https://files.pythonhosted.org/packages/73/21/5c7f5c66eaeee57c9dadb0bbff22efddc8d99cdbd3d036c806175f1828cc/debugpy-1.8.22-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ad076f4f66cb8acb79e4384d48b1ea60a0b1957aa0b1112d4887ea3a4df60e0", size = 2499375, upload-time = "2026-09-15T21:44:18.478Z" }, - { url = "https://files.pythonhosted.org/packages/7e/ec/7389798843643ea5bc6a08480ba1b855377c70e2ed90189c0a03594e401e/debugpy-1.8.22-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:8a697acec45dbc70d17fb5d9f4f61989fc294d273c69de487fd10cb35fdd75eb", size = 4000941, upload-time = "2026-09-15T21:44:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/20/f4/64ce7a2499550929e08654360b270409eeda3e321b7c27027d9c919aa36b/debugpy-1.8.22-cp312-cp312-win32.whl", hash = "sha256:12bc7f368182b517cf26c76a2393fff65354e365fa1552b6241e66edff17997b", size = 5359040, upload-time = "2026-09-15T21:44:22.218Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fe/e7d3b72874cdd327bda295d1995a01e426857ae9cbaaba74fd997bc3afbc/debugpy-1.8.22-cp312-cp312-win_amd64.whl", hash = "sha256:371a4ba4a5975eb958393903f3254cf983da7b1c7c178b3f987ee427f42515e3", size = 5398304, upload-time = "2026-09-15T21:44:24.119Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f4/fee6d40f5865aa84e1100c9df1a0f2cf8991776b61f11f433ff277df4415/debugpy-1.8.22-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:c21dd7e1ec22556bb41dc3bf6b86c603e440ec889c4acb27d7206354b2106c54", size = 2493531, upload-time = "2026-09-15T21:44:25.835Z" }, - { url = "https://files.pythonhosted.org/packages/bc/4f/8df91270afbf61fc5ab202951f28451194a7eb093704b26da11cb0ecd308/debugpy-1.8.22-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:c1ffb9953708b648ecf6acd2e5ad2c002bf68785ed618197dd4463a2a7226f39", size = 3994351, upload-time = "2026-09-15T21:44:27.31Z" }, - { url = "https://files.pythonhosted.org/packages/4f/11/2e34a4f21d6155b0c58003fd6fca9819df7d6fe7d7acd83b146b648a57ee/debugpy-1.8.22-cp313-cp313-win32.whl", hash = "sha256:ba810a66b437e3c43ca0ce0892404f010338286263ca8e5108442d76a9485337", size = 5359071, upload-time = "2026-09-15T21:44:29.064Z" }, - { url = "https://files.pythonhosted.org/packages/d0/f5/c2b3e8260388d7b76bc4e5a3a29648a0ca47dca6a2721d6549ff2d944eee/debugpy-1.8.22-cp313-cp313-win_amd64.whl", hash = "sha256:f49b1d6cecf326b63c06d7f517e4a6642777f758c58cb799e7bc5a6dd74721c2", size = 5400362, upload-time = "2026-09-15T21:44:30.857Z" }, - { url = "https://files.pythonhosted.org/packages/ea/95/d97240bb68b86db8d0a07602be068aae9e50f17063d696a4c6f619975311/debugpy-1.8.22-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:e6744ac1850c73c2ba7b29a126cf9ab74efd1831190720e8fee17ef5389c8f5d", size = 2493148, upload-time = "2026-09-15T21:44:32.518Z" }, - { url = "https://files.pythonhosted.org/packages/70/9e/994fe1fb23dd4a5a8d059a0dc95da4b0821907200a61c2d5a4fdc21b6cf8/debugpy-1.8.22-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0c1104233340196e5cbf5514e7dfdbb98968e730cfd7fdd6f3d72a082be838a9", size = 3961025, upload-time = "2026-09-15T21:44:34.387Z" }, - { url = "https://files.pythonhosted.org/packages/37/e0/66319918883cf6dd6dea3805210c3cafd9e65dfaa6f8b852482cff10a026/debugpy-1.8.22-cp314-cp314-win32.whl", hash = "sha256:feea785c7bbeb8cfd5b01a63f9061c899dc468c8be81671141a29305774fa294", size = 5359408, upload-time = "2026-09-15T21:44:36.128Z" }, - { url = "https://files.pythonhosted.org/packages/94/53/5b4d91f7fffb9393ac907133201d1da0c070d57e463ee8e2ec1db3cb63b6/debugpy-1.8.22-cp314-cp314-win_amd64.whl", hash = "sha256:d593a330297332ec435f3965c448b6d500e03f33675cf2063178bc76377a788e", size = 5397429, upload-time = "2026-09-15T21:44:38.429Z" }, - { url = "https://files.pythonhosted.org/packages/24/71/580df4aed96c9b23c9cbb049907d8ac1347050afdb8f450df20649d51578/debugpy-1.8.22-cp315-cp315-macosx_15_0_universal2.whl", hash = "sha256:a9eca6ab09a61534e923064400081e016f7e1d8430cc6e8b7a9cecd2f59e2b07", size = 2493380, upload-time = "2026-09-15T21:44:40.074Z" }, - { url = "https://files.pythonhosted.org/packages/b0/10/a323945b0966e560c40fe48470d2847bfc7692a8f550d7086e9a28f5b623/debugpy-1.8.22-cp315-cp315-manylinux_2_34_x86_64.whl", hash = "sha256:b7dbde1fb822d100802d505b2aed6d0813b1a0a2015d495cb8798b2215d5d1e5", size = 3975456, upload-time = "2026-09-15T21:44:41.623Z" }, - { url = "https://files.pythonhosted.org/packages/ef/f9/4383635281999c3eaa44bf327de523ba42240b35eb87c1130adac4a829c6/debugpy-1.8.22-cp315-cp315-win32.whl", hash = "sha256:225d063f81708c2546999e7edfac0198b2d5c2f144797dc64858f867948633e0", size = 5359469, upload-time = "2026-09-15T21:44:43.234Z" }, - { url = "https://files.pythonhosted.org/packages/7b/14/4fe9f46a9c825c8cbc6dc4aebc2a885b0bceb37ce2a37699c170482e7a40/debugpy-1.8.22-cp315-cp315-win_amd64.whl", hash = "sha256:b17a4896520f1c6da09ce76ec8215df0b85b0b6f3617526a4c65c2315340875a", size = 5397869, upload-time = "2026-09-15T21:44:44.783Z" }, - { url = "https://files.pythonhosted.org/packages/56/3d/c7dc9f35bc9e22cdb53679f844bee40fd5cff871862f60595869a433400d/debugpy-1.8.22-py2.py3-none-any.whl", hash = "sha256:a9e9d3550e15ca479c59333e90845029190531f0cacfedab3b815a57bd913947", size = 5374857, upload-time = "2026-09-15T21:44:54.454Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/44/9d/3cb6693342acf96802dba89934a5b8da43c201d764ebd1f2c0514a3d42bd/debugpy-1.8.22.tar.gz", hash = "sha256:e489c7268e1c7b41e13b438d9c533d2a7af73fb59bf8cd30fead8286c1c39c4e", size = 1710877 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/e9/ced401ba1088e1d2b3228dbbcae33e15aa8c08a1368433bedebfa8efcb02/debugpy-1.8.22-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:fa47099176d1d612bef69e9f74658a5687a9f05bd80fb5710f99959bace85735", size = 2128369 }, + { url = "https://files.pythonhosted.org/packages/4f/97/5c8e839a7f73e92cd4a733408bb85bb764e829e52a9fb097f99d6e2cbbc0/debugpy-1.8.22-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:7bf29e0d8ce80b100d37fb333e1b193b790d962739f3067796c2b03ffdc9afee", size = 3022081 }, + { url = "https://files.pythonhosted.org/packages/11/ed/fbb045d18a8b733a798bb834a03a20cd64f1446206d844b674bb638da6c3/debugpy-1.8.22-cp310-cp310-win32.whl", hash = "sha256:56b877b37ed73f0bf53ba7afc394816ff1eb5d701bae24a24ed9e1ea6f7ce34f", size = 5325298 }, + { url = "https://files.pythonhosted.org/packages/82/7f/633160132b80b424c6816391f239682435c6b77cdd58288d04b3443b9f80/debugpy-1.8.22-cp310-cp310-win_amd64.whl", hash = "sha256:1bd0c6df3c68c0a3f71db8baa3780a953abb65537ec4b3bc6b935ad5b3b3d45c", size = 5353598 }, + { url = "https://files.pythonhosted.org/packages/26/4a/8c24c588088c622df6dacdc0110e9cc81b44781fe7a408113441e76aa955/debugpy-1.8.22-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:66e4ac3d6e7026e83e7d93d7ee2f51dd4a4e8dff673578d424e60796893e5b2c", size = 2217799 }, + { url = "https://files.pythonhosted.org/packages/c7/14/1c9ff33eba51da70a8dfe2277cc13153bb3ffec79282b9acfcdbe1641d30/debugpy-1.8.22-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:192b73e8d53bbd60225220c0943627bf249ac93d0ea3090e23c45f3e0ceb6a35", size = 3074375 }, + { url = "https://files.pythonhosted.org/packages/10/e8/1fa16a94af7d8b86c3ca336d7d32b78cb2c475266d0bb889732d2bc50310/debugpy-1.8.22-cp311-cp311-win32.whl", hash = "sha256:745e1800ec2961e5660c1a317c0a20e28c0fef4de36c04f17a21c33f2b37a92a", size = 5258392 }, + { url = "https://files.pythonhosted.org/packages/32/1a/c086b883a4561017bfdd83c1bc70bf2b05ab6540ff767dedf207cadab938/debugpy-1.8.22-cp311-cp311-win_amd64.whl", hash = "sha256:1e76339d5510bc17e9181dba9577508afcb21aad5728f1a55ef74d7d97d255f3", size = 5277955 }, + { url = "https://files.pythonhosted.org/packages/73/21/5c7f5c66eaeee57c9dadb0bbff22efddc8d99cdbd3d036c806175f1828cc/debugpy-1.8.22-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ad076f4f66cb8acb79e4384d48b1ea60a0b1957aa0b1112d4887ea3a4df60e0", size = 2499375 }, + { url = "https://files.pythonhosted.org/packages/7e/ec/7389798843643ea5bc6a08480ba1b855377c70e2ed90189c0a03594e401e/debugpy-1.8.22-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:8a697acec45dbc70d17fb5d9f4f61989fc294d273c69de487fd10cb35fdd75eb", size = 4000941 }, + { url = "https://files.pythonhosted.org/packages/20/f4/64ce7a2499550929e08654360b270409eeda3e321b7c27027d9c919aa36b/debugpy-1.8.22-cp312-cp312-win32.whl", hash = "sha256:12bc7f368182b517cf26c76a2393fff65354e365fa1552b6241e66edff17997b", size = 5359040 }, + { url = "https://files.pythonhosted.org/packages/4b/fe/e7d3b72874cdd327bda295d1995a01e426857ae9cbaaba74fd997bc3afbc/debugpy-1.8.22-cp312-cp312-win_amd64.whl", hash = "sha256:371a4ba4a5975eb958393903f3254cf983da7b1c7c178b3f987ee427f42515e3", size = 5398304 }, + { url = "https://files.pythonhosted.org/packages/6b/f4/fee6d40f5865aa84e1100c9df1a0f2cf8991776b61f11f433ff277df4415/debugpy-1.8.22-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:c21dd7e1ec22556bb41dc3bf6b86c603e440ec889c4acb27d7206354b2106c54", size = 2493531 }, + { url = "https://files.pythonhosted.org/packages/bc/4f/8df91270afbf61fc5ab202951f28451194a7eb093704b26da11cb0ecd308/debugpy-1.8.22-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:c1ffb9953708b648ecf6acd2e5ad2c002bf68785ed618197dd4463a2a7226f39", size = 3994351 }, + { url = "https://files.pythonhosted.org/packages/4f/11/2e34a4f21d6155b0c58003fd6fca9819df7d6fe7d7acd83b146b648a57ee/debugpy-1.8.22-cp313-cp313-win32.whl", hash = "sha256:ba810a66b437e3c43ca0ce0892404f010338286263ca8e5108442d76a9485337", size = 5359071 }, + { url = "https://files.pythonhosted.org/packages/d0/f5/c2b3e8260388d7b76bc4e5a3a29648a0ca47dca6a2721d6549ff2d944eee/debugpy-1.8.22-cp313-cp313-win_amd64.whl", hash = "sha256:f49b1d6cecf326b63c06d7f517e4a6642777f758c58cb799e7bc5a6dd74721c2", size = 5400362 }, + { url = "https://files.pythonhosted.org/packages/ea/95/d97240bb68b86db8d0a07602be068aae9e50f17063d696a4c6f619975311/debugpy-1.8.22-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:e6744ac1850c73c2ba7b29a126cf9ab74efd1831190720e8fee17ef5389c8f5d", size = 2493148 }, + { url = "https://files.pythonhosted.org/packages/70/9e/994fe1fb23dd4a5a8d059a0dc95da4b0821907200a61c2d5a4fdc21b6cf8/debugpy-1.8.22-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0c1104233340196e5cbf5514e7dfdbb98968e730cfd7fdd6f3d72a082be838a9", size = 3961025 }, + { url = "https://files.pythonhosted.org/packages/37/e0/66319918883cf6dd6dea3805210c3cafd9e65dfaa6f8b852482cff10a026/debugpy-1.8.22-cp314-cp314-win32.whl", hash = "sha256:feea785c7bbeb8cfd5b01a63f9061c899dc468c8be81671141a29305774fa294", size = 5359408 }, + { url = "https://files.pythonhosted.org/packages/94/53/5b4d91f7fffb9393ac907133201d1da0c070d57e463ee8e2ec1db3cb63b6/debugpy-1.8.22-cp314-cp314-win_amd64.whl", hash = "sha256:d593a330297332ec435f3965c448b6d500e03f33675cf2063178bc76377a788e", size = 5397429 }, + { url = "https://files.pythonhosted.org/packages/24/71/580df4aed96c9b23c9cbb049907d8ac1347050afdb8f450df20649d51578/debugpy-1.8.22-cp315-cp315-macosx_15_0_universal2.whl", hash = "sha256:a9eca6ab09a61534e923064400081e016f7e1d8430cc6e8b7a9cecd2f59e2b07", size = 2493380 }, + { url = "https://files.pythonhosted.org/packages/b0/10/a323945b0966e560c40fe48470d2847bfc7692a8f550d7086e9a28f5b623/debugpy-1.8.22-cp315-cp315-manylinux_2_34_x86_64.whl", hash = "sha256:b7dbde1fb822d100802d505b2aed6d0813b1a0a2015d495cb8798b2215d5d1e5", size = 3975456 }, + { url = "https://files.pythonhosted.org/packages/ef/f9/4383635281999c3eaa44bf327de523ba42240b35eb87c1130adac4a829c6/debugpy-1.8.22-cp315-cp315-win32.whl", hash = "sha256:225d063f81708c2546999e7edfac0198b2d5c2f144797dc64858f867948633e0", size = 5359469 }, + { url = "https://files.pythonhosted.org/packages/7b/14/4fe9f46a9c825c8cbc6dc4aebc2a885b0bceb37ce2a37699c170482e7a40/debugpy-1.8.22-cp315-cp315-win_amd64.whl", hash = "sha256:b17a4896520f1c6da09ce76ec8215df0b85b0b6f3617526a4c65c2315340875a", size = 5397869 }, + { url = "https://files.pythonhosted.org/packages/56/3d/c7dc9f35bc9e22cdb53679f844bee40fd5cff871862f60595869a433400d/debugpy-1.8.22-py2.py3-none-any.whl", hash = "sha256:a9e9d3550e15ca479c59333e90845029190531f0cacfedab3b815a57bd913947", size = 5374857 }, ] [[package]] name = "decorator" version = "5.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084 } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, + { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365 }, ] [[package]] name = "defusedxml" version = "0.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520 } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604 }, ] [[package]] @@ -679,18 +685,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload-time = "2025-01-27T10:46:25.7Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998 }, ] [[package]] name = "docutils" version = "0.23" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/39/a4/5180d9afc57e8fca05601dd652bdff19604c218814037fe90ffc7625a50a/docutils-0.23.tar.gz", hash = "sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e", size = 2303823, upload-time = "2026-05-27T17:41:06.934Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/a4/5180d9afc57e8fca05601dd652bdff19604c218814037fe90ffc7625a50a/docutils-0.23.tar.gz", hash = "sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e", size = 2303823 } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/91/30151a39f7570f448ed84529390628a651d7f27c87d73c9b887f8189695e/docutils-0.23-py3-none-any.whl", hash = "sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea", size = 634701, upload-time = "2026-05-27T17:40:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/30151a39f7570f448ed84529390628a651d7f27c87d73c9b887f8189695e/docutils-0.23-py3-none-any.whl", hash = "sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea", size = 634701 }, ] [[package]] @@ -698,47 +704,47 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740 }, ] [[package]] name = "executing" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317 }, ] [[package]] name = "fastjsonschema" version = "2.22.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/a4/9473c7c3b87009d9c1d74034e4a0f6a35ff0d42dd0f9866d0c3ec4e9217b/fastjsonschema-2.22.2.tar.gz", hash = "sha256:72064e12356a7d6ef02165be2946b9abadbdf238536e07eb587e3dbaa33099cf", size = 385171, upload-time = "2026-08-15T19:47:08.853Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/a4/9473c7c3b87009d9c1d74034e4a0f6a35ff0d42dd0f9866d0c3ec4e9217b/fastjsonschema-2.22.2.tar.gz", hash = "sha256:72064e12356a7d6ef02165be2946b9abadbdf238536e07eb587e3dbaa33099cf", size = 385171 } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/82/2755c7c982086f00d4dab85bc120ec35045a9fc2191893a6ce79afe94443/fastjsonschema-2.22.2-py3-none-any.whl", hash = "sha256:0fb3915616adac85ccfdd737d26be1089845d2019819505b42d39888458f74d4", size = 27413, upload-time = "2026-08-15T19:47:04.406Z" }, + { url = "https://files.pythonhosted.org/packages/49/82/2755c7c982086f00d4dab85bc120ec35045a9fc2191893a6ce79afe94443/fastjsonschema-2.22.2-py3-none-any.whl", hash = "sha256:0fb3915616adac85ccfdd737d26be1089845d2019819505b42d39888458f74d4", size = 27413 }, ] [[package]] name = "fqdn" version = "1.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015 } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, + { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121 }, ] [[package]] name = "h11" version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, ] [[package]] @@ -749,9 +755,9 @@ dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, ] [[package]] @@ -764,9 +770,9 @@ dependencies = [ { name = "httpcore" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, ] [[package]] @@ -776,18 +782,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069", size = 18088, upload-time = "2026-02-04T16:19:41.26Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069", size = 18088 } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca", size = 14689, upload-time = "2026-02-04T16:19:40.051Z" }, + { url = "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca", size = 14689 }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] [[package]] @@ -795,20 +801,20 @@ name = "importlib-metadata" version = "6.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/82/f6e29c8d5c098b6be61460371c2c5591f4a335923639edec43b3830650a4/importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4", size = 53569, upload-time = "2023-06-18T21:44:35.024Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/82/f6e29c8d5c098b6be61460371c2c5591f4a335923639edec43b3830650a4/importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4", size = 53569 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/94/64287b38c7de4c90683630338cf28f129decbba0a44f0c6db35a873c73c4/importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5", size = 22934, upload-time = "2023-06-18T21:44:33.441Z" }, + { url = "https://files.pythonhosted.org/packages/ff/94/64287b38c7de4c90683630338cf28f129decbba0a44f0c6db35a873c73c4/importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5", size = 22934 }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, ] [[package]] @@ -831,9 +837,9 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/c4/e4a38f579de4225a561305666f7541cdabb30075def2aa1ac17bd73c1fb5/ipykernel-7.3.0.tar.gz", hash = "sha256:9acaaaf97d16355166e4085afe9d225bfbdf2b7ef520f9df3be8f2b248275e09", size = 184899, upload-time = "2026-06-10T08:41:25.481Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/c4/e4a38f579de4225a561305666f7541cdabb30075def2aa1ac17bd73c1fb5/ipykernel-7.3.0.tar.gz", hash = "sha256:9acaaaf97d16355166e4085afe9d225bfbdf2b7ef520f9df3be8f2b248275e09", size = 184899 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl", hash = "sha256:897eb64da762549ef610698fca5e9675195ec6ac8ec7f19d81ce1ca20c876057", size = 120583, upload-time = "2026-06-10T08:41:23.648Z" }, + { url = "https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl", hash = "sha256:897eb64da762549ef610698fca5e9675195ec6ac8ec7f19d81ce1ca20c876057", size = 120583 }, ] [[package]] @@ -844,21 +850,21 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "decorator" }, - { name = "exceptiongroup" }, - { name = "jedi" }, - { name = "matplotlib-inline" }, - { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit" }, - { name = "pygments" }, - { name = "stack-data" }, - { name = "traitlets" }, - { name = "typing-extensions" }, + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "jedi", marker = "python_full_version < '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, + { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "stack-data", marker = "python_full_version < '3.11'" }, + { name = "traitlets", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/56/4cc7fc9e9e3f38fd324f24f8afe0ad8bb5fa41283f37f1aaf9de0612c968/ipython-8.39.0-py3-none-any.whl", hash = "sha256:bb3c51c4fa8148ab1dea07a79584d1c854e234ea44aa1283bcb37bc75054651f", size = 831849, upload-time = "2026-03-27T10:02:07.846Z" }, + { url = "https://files.pythonhosted.org/packages/c0/56/4cc7fc9e9e3f38fd324f24f8afe0ad8bb5fa41283f37f1aaf9de0612c968/ipython-8.39.0-py3-none-any.whl", hash = "sha256:bb3c51c4fa8148ab1dea07a79584d1c854e234ea44aa1283bcb37bc75054651f", size = 831849 }, ] [[package]] @@ -871,21 +877,21 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "ipython-pygments-lexers" }, - { name = "jedi" }, - { name = "matplotlib-inline" }, - { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit" }, - { name = "psutil", marker = "sys_platform != 'cygwin' and sys_platform != 'emscripten'" }, - { name = "pygments" }, - { name = "stack-data" }, - { name = "traitlets" }, - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, + { name = "jedi", marker = "python_full_version >= '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, + { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, + { name = "psutil", marker = "python_full_version >= '3.11' and sys_platform != 'cygwin' and sys_platform != 'emscripten'" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "stack-data", marker = "python_full_version >= '3.11'" }, + { name = "traitlets", marker = "python_full_version >= '3.11'" }, + { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/32/99451b1283ec5d92ad77073f12e1c667dc10775384d8f15c2914207149dd/ipython-9.17.1.tar.gz", hash = "sha256:8919be8c27f20a6f4423145028063f6637b42a03ce57665bb12015ee1f073529", size = 4539289, upload-time = "2026-09-01T08:29:32.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/32/99451b1283ec5d92ad77073f12e1c667dc10775384d8f15c2914207149dd/ipython-9.17.1.tar.gz", hash = "sha256:8919be8c27f20a6f4423145028063f6637b42a03ce57665bb12015ee1f073529", size = 4539289 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/1e/65b59cf518c106aa755e7f7da3099027738687a862ec785060702a481320/ipython-9.17.1-py3-none-any.whl", hash = "sha256:6d1645743cfd1a07eb695d85aa2b5fa66721f8cbae9431d4049f7084bbf06509", size = 639038, upload-time = "2026-09-01T08:29:30.673Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1e/65b59cf518c106aa755e7f7da3099027738687a862ec785060702a481320/ipython-9.17.1-py3-none-any.whl", hash = "sha256:6d1645743cfd1a07eb695d85aa2b5fa66721f8cbae9431d4049f7084bbf06509", size = 639038 }, ] [[package]] @@ -893,20 +899,20 @@ name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pygments" }, + { name = "pygments", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074 }, ] [[package]] name = "isodate" version = "0.7.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705 } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 }, ] [[package]] @@ -916,9 +922,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "arrow" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, + { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321 }, ] [[package]] @@ -928,9 +934,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 }, ] [[package]] @@ -940,9 +946,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, + { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871 }, ] [[package]] @@ -952,9 +958,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280", size = 20837, upload-time = "2026-07-14T01:28:02.544Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280", size = 20837 } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30", size = 11677, upload-time = "2026-07-14T01:28:01.59Z" }, + { url = "https://files.pythonhosted.org/packages/02/36/ecc85bc96c273dc8a11273ed4782272975e6338d4a3e9228621175edf0e3/jaraco_functools-4.6.0-py3-none-any.whl", hash = "sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30", size = 11677 }, ] [[package]] @@ -964,18 +970,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "parso" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" }, + { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812 }, ] [[package]] name = "jeepney" version = "0.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010 }, ] [[package]] @@ -985,27 +991,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, ] [[package]] name = "json5" version = "0.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/7d/05c46a96a78147ae3bf99c2f4169ce144a70220b8d6fcd56f6ec368b8ce9/json5-0.15.0.tar.gz", hash = "sha256:7424d1f1eb1d56da6e3d70643f53619862b4ce81440bdb8ecfd6f875e5ba4a71", size = 53278, upload-time = "2026-06-19T20:08:27.716Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/7d/05c46a96a78147ae3bf99c2f4169ce144a70220b8d6fcd56f6ec368b8ce9/json5-0.15.0.tar.gz", hash = "sha256:7424d1f1eb1d56da6e3d70643f53619862b4ce81440bdb8ecfd6f875e5ba4a71", size = 53278 } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/be/59527c99478aade6bb33a68d72e6e18dd4e6ff6eacfc7d01bdb15bc76912/json5-0.15.0-py3-none-any.whl", hash = "sha256:56636a30c0e8a4665fe2179c0212f32eae3796dea89ea6f649b9436ecdb39618", size = 36570, upload-time = "2026-06-19T20:08:26.748Z" }, + { url = "https://files.pythonhosted.org/packages/eb/be/59527c99478aade6bb33a68d72e6e18dd4e6ff6eacfc7d01bdb15bc76912/json5-0.15.0-py3-none-any.whl", hash = "sha256:56636a30c0e8a4665fe2179c0212f32eae3796dea89ea6f649b9436ecdb39618", size = 36570 }, ] [[package]] name = "jsonpointer" version = "3.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659 }, ] [[package]] @@ -1016,9 +1022,9 @@ dependencies = [ { name = "attrs" }, { name = "pyrsistent" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/3d/ca032d5ac064dff543aa13c984737795ac81abc9fb130cd2fcff17cfabc7/jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", size = 297785, upload-time = "2022-11-29T20:37:47.453Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/3d/ca032d5ac064dff543aa13c984737795ac81abc9fb130cd2fcff17cfabc7/jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d", size = 297785 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/97/c698bd9350f307daad79dd740806e1a59becd693bd11443a0f531e3229b3/jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6", size = 90379, upload-time = "2022-11-29T20:37:45.842Z" }, + { url = "https://files.pythonhosted.org/packages/c1/97/c698bd9350f307daad79dd740806e1a59becd693bd11443a0f531e3229b3/jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6", size = 90379 }, ] [package.optional-dependencies] @@ -1045,9 +1051,9 @@ dependencies = [ { name = "traitlets" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/2a/906772148a06e48885039e0250c340b770a55bbf37b08d6ee0449df369c3/jupyter_client-8.10.0.tar.gz", hash = "sha256:9f7116294dca55f1785be880057d44544db9b1567718d92cb33c58886afb9497", size = 360653, upload-time = "2026-08-28T12:17:10.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/2a/906772148a06e48885039e0250c340b770a55bbf37b08d6ee0449df369c3/jupyter_client-8.10.0.tar.gz", hash = "sha256:9f7116294dca55f1785be880057d44544db9b1567718d92cb33c58886afb9497", size = 360653 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/88/7c548de1f6c2ade7c931a3282da73f9274fa6a1531091be682f89c85efb9/jupyter_client-8.10.0-py3-none-any.whl", hash = "sha256:5f73f24f22fa25192cfff6b23c051932a2473a797b05734aff495b392103e14e", size = 110184, upload-time = "2026-08-28T12:17:09.028Z" }, + { url = "https://files.pythonhosted.org/packages/0f/88/7c548de1f6c2ade7c931a3282da73f9274fa6a1531091be682f89c85efb9/jupyter_client-8.10.0-py3-none-any.whl", hash = "sha256:5f73f24f22fa25192cfff6b23c051932a2473a797b05734aff495b392103e14e", size = 110184 }, ] [[package]] @@ -1058,9 +1064,9 @@ dependencies = [ { name = "platformdirs" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032 }, ] [[package]] @@ -1075,9 +1081,9 @@ dependencies = [ { name = "rfc3986-validator" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d0/b0/7afcd1d66834f43d08ec47c861a5540d7ad57eab47605ccd83429c147755/jupyter_events-0.6.3.tar.gz", hash = "sha256:9a6e9995f75d1b7146b436ea24d696ce3a35bfa8bfe45e0c33c334c79464d0b3", size = 59393, upload-time = "2023-01-12T17:50:44.942Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/b0/7afcd1d66834f43d08ec47c861a5540d7ad57eab47605ccd83429c147755/jupyter_events-0.6.3.tar.gz", hash = "sha256:9a6e9995f75d1b7146b436ea24d696ce3a35bfa8bfe45e0c33c334c79464d0b3", size = 59393 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/14/e11a93c1b47a69432ee7898f1b55f1da27f2f93b009a34dbdafb9b903f81/jupyter_events-0.6.3-py3-none-any.whl", hash = "sha256:57a2749f87ba387cd1bfd9b22a0875b889237dbf2edc2121ebb22bde47036c17", size = 18773, upload-time = "2023-01-12T17:50:42.605Z" }, + { url = "https://files.pythonhosted.org/packages/ee/14/e11a93c1b47a69432ee7898f1b55f1da27f2f93b009a34dbdafb9b903f81/jupyter_events-0.6.3-py3-none-any.whl", hash = "sha256:57a2749f87ba387cd1bfd9b22a0875b889237dbf2edc2121ebb22bde47036c17", size = 18773 }, ] [[package]] @@ -1087,9 +1093,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-server" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/ff/1e4a61f5170a9a1d978f3ac3872449de6c01fc71eaf89657824c878b1549/jupyter_lsp-2.3.1.tar.gz", hash = "sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6", size = 55677, upload-time = "2026-04-02T08:10:06.749Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/ff/1e4a61f5170a9a1d978f3ac3872449de6c01fc71eaf89657824c878b1549/jupyter_lsp-2.3.1.tar.gz", hash = "sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6", size = 55677 } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/e8/9d61dcbd1dce8ef418f06befd4ac084b4720429c26b0b1222bc218685eff/jupyter_lsp-2.3.1-py3-none-any.whl", hash = "sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81", size = 77513, upload-time = "2026-04-02T08:10:01.753Z" }, + { url = "https://files.pythonhosted.org/packages/23/e8/9d61dcbd1dce8ef418f06befd4ac084b4720429c26b0b1222bc218685eff/jupyter_lsp-2.3.1-py3-none-any.whl", hash = "sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81", size = 77513 }, ] [[package]] @@ -1117,9 +1123,9 @@ dependencies = [ { name = "traitlets" }, { name = "websocket-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/c8/156bef918468b3a96c6c29440769d5c88355ad2ab3633446401601b07621/jupyter_server-2.10.0.tar.gz", hash = "sha256:47b8f5e63440125cb1bb8957bf12b18453ee5ed9efe42d2f7b2ca66a7019a278", size = 709151, upload-time = "2023-11-06T13:46:21.003Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/c8/156bef918468b3a96c6c29440769d5c88355ad2ab3633446401601b07621/jupyter_server-2.10.0.tar.gz", hash = "sha256:47b8f5e63440125cb1bb8957bf12b18453ee5ed9efe42d2f7b2ca66a7019a278", size = 709151 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/42/d200e2aaed1ce8f755b499aea7f83632760654ab75dae07f9038594c7418/jupyter_server-2.10.0-py3-none-any.whl", hash = "sha256:dde56c9bc3cb52d7b72cc0f696d15d7163603526f1a758eb4a27405b73eab2a5", size = 377881, upload-time = "2023-11-06T13:46:17.881Z" }, + { url = "https://files.pythonhosted.org/packages/e6/42/d200e2aaed1ce8f755b499aea7f83632760654ab75dae07f9038594c7418/jupyter_server-2.10.0-py3-none-any.whl", hash = "sha256:dde56c9bc3cb52d7b72cc0f696d15d7163603526f1a758eb4a27405b73eab2a5", size = 377881 }, ] [[package]] @@ -1130,9 +1136,9 @@ dependencies = [ { name = "pywinpty", marker = "os_name == 'nt'" }, { name = "terminado" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl", hash = "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14", size = 13704, upload-time = "2026-01-14T16:53:18.738Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl", hash = "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14", size = 13704 }, ] [[package]] @@ -1154,18 +1160,18 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/c2/decb25538588ed104ada22c31d1845b1500f98806e3261b94f0333e8b099/jupyterlab-4.1.6.tar.gz", hash = "sha256:7935f36ba26eb615183a4f5c2bbca5791b5108ce2a00b5505f8cfd100d53648e", size = 21773628, upload-time = "2024-04-08T14:00:54.61Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/c2/decb25538588ed104ada22c31d1845b1500f98806e3261b94f0333e8b099/jupyterlab-4.1.6.tar.gz", hash = "sha256:7935f36ba26eb615183a4f5c2bbca5791b5108ce2a00b5505f8cfd100d53648e", size = 21773628 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/5a/e084a40e593329859926c5824cd47b32b2700732a179ca94b2c533c4dddf/jupyterlab-4.1.6-py3-none-any.whl", hash = "sha256:cf3e862bc10dbf4331e4eb37438634f813c238cfc62c71c640b3b3b2caa089a8", size = 11416049, upload-time = "2024-04-08T14:00:46.698Z" }, + { url = "https://files.pythonhosted.org/packages/4c/5a/e084a40e593329859926c5824cd47b32b2700732a179ca94b2c533c4dddf/jupyterlab-4.1.6-py3-none-any.whl", hash = "sha256:cf3e862bc10dbf4331e4eb37438634f813c238cfc62c71c640b3b3b2caa089a8", size = 11416049 }, ] [[package]] name = "jupyterlab-pygments" version = "0.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884 }, ] [[package]] @@ -1181,9 +1187,9 @@ dependencies = [ { name = "packaging" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/29/d867a88db915b54ff1025a9c0576b051687ac5baa5f7cd723e22069606de/jupyterlab_server-2.24.0.tar.gz", hash = "sha256:4e6f99e0a5579bbbc32e449c4dbb039561d4f1a7827d5733273ed56738f21f07", size = 72145, upload-time = "2023-07-24T08:07:24.605Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/29/d867a88db915b54ff1025a9c0576b051687ac5baa5f7cd723e22069606de/jupyterlab_server-2.24.0.tar.gz", hash = "sha256:4e6f99e0a5579bbbc32e449c4dbb039561d4f1a7827d5733273ed56738f21f07", size = 72145 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/0d/6d4eab0391bd4df1c43f308368d5e177b0fa8ac632267222a23b71317091/jupyterlab_server-2.24.0-py3-none-any.whl", hash = "sha256:5f077e142bb8dc9b843d960f940c513581bceca3793a0d80f9c67d9522c4e876", size = 57329, upload-time = "2023-07-24T08:07:22.09Z" }, + { url = "https://files.pythonhosted.org/packages/a7/0d/6d4eab0391bd4df1c43f308368d5e177b0fa8ac632267222a23b71317091/jupyterlab_server-2.24.0-py3-none-any.whl", hash = "sha256:5f077e142bb8dc9b843d960f940c513581bceca3793a0d80f9c67d9522c4e876", size = 57329 }, ] [[package]] @@ -1199,133 +1205,133 @@ dependencies = [ { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, { name = "secretstorage", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516 } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160 }, ] [[package]] name = "lxml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/8a/f8192a08237ef2fb1b19733f709db88a4c43bc8ab8357f01cb41a27e7f6a/lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388", size = 8590589, upload-time = "2025-09-22T04:00:10.51Z" }, - { url = "https://files.pythonhosted.org/packages/12/64/27bcd07ae17ff5e5536e8d88f4c7d581b48963817a13de11f3ac3329bfa2/lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153", size = 4629671, upload-time = "2025-09-22T04:00:15.411Z" }, - { url = "https://files.pythonhosted.org/packages/02/5a/a7d53b3291c324e0b6e48f3c797be63836cc52156ddf8f33cd72aac78866/lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31", size = 4999961, upload-time = "2025-09-22T04:00:17.619Z" }, - { url = "https://files.pythonhosted.org/packages/f5/55/d465e9b89df1761674d8672bb3e4ae2c47033b01ec243964b6e334c6743f/lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9", size = 5157087, upload-time = "2025-09-22T04:00:19.868Z" }, - { url = "https://files.pythonhosted.org/packages/62/38/3073cd7e3e8dfc3ba3c3a139e33bee3a82de2bfb0925714351ad3d255c13/lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8", size = 5067620, upload-time = "2025-09-22T04:00:21.877Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d3/1e001588c5e2205637b08985597827d3827dbaaece16348c8822bfe61c29/lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba", size = 5406664, upload-time = "2025-09-22T04:00:23.714Z" }, - { url = "https://files.pythonhosted.org/packages/20/cf/cab09478699b003857ed6ebfe95e9fb9fa3d3c25f1353b905c9b73cfb624/lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c", size = 5289397, upload-time = "2025-09-22T04:00:25.544Z" }, - { url = "https://files.pythonhosted.org/packages/a3/84/02a2d0c38ac9a8b9f9e5e1bbd3f24b3f426044ad618b552e9549ee91bd63/lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c", size = 4772178, upload-time = "2025-09-22T04:00:27.602Z" }, - { url = "https://files.pythonhosted.org/packages/56/87/e1ceadcc031ec4aa605fe95476892d0b0ba3b7f8c7dcdf88fdeff59a9c86/lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321", size = 5358148, upload-time = "2025-09-22T04:00:29.323Z" }, - { url = "https://files.pythonhosted.org/packages/fe/13/5bb6cf42bb228353fd4ac5f162c6a84fd68a4d6f67c1031c8cf97e131fc6/lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1", size = 5112035, upload-time = "2025-09-22T04:00:31.061Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e2/ea0498552102e59834e297c5c6dff8d8ded3db72ed5e8aad77871476f073/lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34", size = 4799111, upload-time = "2025-09-22T04:00:33.11Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9e/8de42b52a73abb8af86c66c969b3b4c2a96567b6ac74637c037d2e3baa60/lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a", size = 5351662, upload-time = "2025-09-22T04:00:35.237Z" }, - { url = "https://files.pythonhosted.org/packages/28/a2/de776a573dfb15114509a37351937c367530865edb10a90189d0b4b9b70a/lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c", size = 5314973, upload-time = "2025-09-22T04:00:37.086Z" }, - { url = "https://files.pythonhosted.org/packages/50/a0/3ae1b1f8964c271b5eec91db2043cf8c6c0bce101ebb2a633b51b044db6c/lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b", size = 3611953, upload-time = "2025-09-22T04:00:39.224Z" }, - { url = "https://files.pythonhosted.org/packages/d1/70/bd42491f0634aad41bdfc1e46f5cff98825fb6185688dc82baa35d509f1a/lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0", size = 4032695, upload-time = "2025-09-22T04:00:41.402Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d0/05c6a72299f54c2c561a6c6cbb2f512e047fca20ea97a05e57931f194ac4/lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5", size = 3680051, upload-time = "2025-09-22T04:00:43.525Z" }, - { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, - { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, - { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, - { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, - { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, - { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, - { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, - { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, - { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, - { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, - { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, - { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, - { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, - { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, - { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, - { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, - { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, - { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, - { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, - { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, - { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, - { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, - { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, - { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, - { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, - { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, - { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, - { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, - { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, - { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, - { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, - { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, - { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, - { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, - { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, - { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, - { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, - { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, - { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, - { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, - { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, - { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, - { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, - { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, - { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, - { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, - { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, - { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, - { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, - { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, - { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, - { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, - { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, - { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, - { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, - { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, - { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, - { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, - { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, - { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, - { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, - { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, - { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, - { url = "https://files.pythonhosted.org/packages/e7/9c/780c9a8fce3f04690b374f72f41306866b0400b9d0fdf3e17aaa37887eed/lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6", size = 3939264, upload-time = "2025-09-22T04:04:32.892Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5a/1ab260c00adf645d8bf7dec7f920f744b032f69130c681302821d5debea6/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba", size = 4216435, upload-time = "2025-09-22T04:04:34.907Z" }, - { url = "https://files.pythonhosted.org/packages/f2/37/565f3b3d7ffede22874b6d86be1a1763d00f4ea9fc5b9b6ccb11e4ec8612/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5", size = 4325913, upload-time = "2025-09-22T04:04:37.205Z" }, - { url = "https://files.pythonhosted.org/packages/22/ec/f3a1b169b2fb9d03467e2e3c0c752ea30e993be440a068b125fc7dd248b0/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4", size = 4269357, upload-time = "2025-09-22T04:04:39.322Z" }, - { url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d", size = 4412295, upload-time = "2025-09-22T04:04:41.647Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d", size = 3516913, upload-time = "2025-09-22T04:04:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, - { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, - { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, - { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, - { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/8a/f8192a08237ef2fb1b19733f709db88a4c43bc8ab8357f01cb41a27e7f6a/lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388", size = 8590589 }, + { url = "https://files.pythonhosted.org/packages/12/64/27bcd07ae17ff5e5536e8d88f4c7d581b48963817a13de11f3ac3329bfa2/lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153", size = 4629671 }, + { url = "https://files.pythonhosted.org/packages/02/5a/a7d53b3291c324e0b6e48f3c797be63836cc52156ddf8f33cd72aac78866/lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31", size = 4999961 }, + { url = "https://files.pythonhosted.org/packages/f5/55/d465e9b89df1761674d8672bb3e4ae2c47033b01ec243964b6e334c6743f/lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9", size = 5157087 }, + { url = "https://files.pythonhosted.org/packages/62/38/3073cd7e3e8dfc3ba3c3a139e33bee3a82de2bfb0925714351ad3d255c13/lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8", size = 5067620 }, + { url = "https://files.pythonhosted.org/packages/4a/d3/1e001588c5e2205637b08985597827d3827dbaaece16348c8822bfe61c29/lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba", size = 5406664 }, + { url = "https://files.pythonhosted.org/packages/20/cf/cab09478699b003857ed6ebfe95e9fb9fa3d3c25f1353b905c9b73cfb624/lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c", size = 5289397 }, + { url = "https://files.pythonhosted.org/packages/a3/84/02a2d0c38ac9a8b9f9e5e1bbd3f24b3f426044ad618b552e9549ee91bd63/lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c", size = 4772178 }, + { url = "https://files.pythonhosted.org/packages/56/87/e1ceadcc031ec4aa605fe95476892d0b0ba3b7f8c7dcdf88fdeff59a9c86/lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321", size = 5358148 }, + { url = "https://files.pythonhosted.org/packages/fe/13/5bb6cf42bb228353fd4ac5f162c6a84fd68a4d6f67c1031c8cf97e131fc6/lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1", size = 5112035 }, + { url = "https://files.pythonhosted.org/packages/e4/e2/ea0498552102e59834e297c5c6dff8d8ded3db72ed5e8aad77871476f073/lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34", size = 4799111 }, + { url = "https://files.pythonhosted.org/packages/6a/9e/8de42b52a73abb8af86c66c969b3b4c2a96567b6ac74637c037d2e3baa60/lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a", size = 5351662 }, + { url = "https://files.pythonhosted.org/packages/28/a2/de776a573dfb15114509a37351937c367530865edb10a90189d0b4b9b70a/lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c", size = 5314973 }, + { url = "https://files.pythonhosted.org/packages/50/a0/3ae1b1f8964c271b5eec91db2043cf8c6c0bce101ebb2a633b51b044db6c/lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b", size = 3611953 }, + { url = "https://files.pythonhosted.org/packages/d1/70/bd42491f0634aad41bdfc1e46f5cff98825fb6185688dc82baa35d509f1a/lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0", size = 4032695 }, + { url = "https://files.pythonhosted.org/packages/d2/d0/05c6a72299f54c2c561a6c6cbb2f512e047fca20ea97a05e57931f194ac4/lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5", size = 3680051 }, + { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365 }, + { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793 }, + { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362 }, + { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152 }, + { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539 }, + { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853 }, + { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133 }, + { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944 }, + { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535 }, + { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343 }, + { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419 }, + { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008 }, + { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906 }, + { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357 }, + { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583 }, + { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591 }, + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887 }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818 }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807 }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179 }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044 }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685 }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127 }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958 }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541 }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426 }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917 }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795 }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759 }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666 }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989 }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456 }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793 }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836 }, + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494 }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146 }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932 }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060 }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000 }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496 }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779 }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072 }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675 }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171 }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175 }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688 }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655 }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695 }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841 }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700 }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347 }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248 }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801 }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403 }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974 }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953 }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054 }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421 }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684 }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463 }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437 }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890 }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185 }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895 }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246 }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797 }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404 }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072 }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617 }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930 }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380 }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632 }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171 }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109 }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061 }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233 }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739 }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119 }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665 }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997 }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957 }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372 }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653 }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795 }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023 }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420 }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837 }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205 }, + { url = "https://files.pythonhosted.org/packages/e7/9c/780c9a8fce3f04690b374f72f41306866b0400b9d0fdf3e17aaa37887eed/lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6", size = 3939264 }, + { url = "https://files.pythonhosted.org/packages/f5/5a/1ab260c00adf645d8bf7dec7f920f744b032f69130c681302821d5debea6/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba", size = 4216435 }, + { url = "https://files.pythonhosted.org/packages/f2/37/565f3b3d7ffede22874b6d86be1a1763d00f4ea9fc5b9b6ccb11e4ec8612/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5", size = 4325913 }, + { url = "https://files.pythonhosted.org/packages/22/ec/f3a1b169b2fb9d03467e2e3c0c752ea30e993be440a068b125fc7dd248b0/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4", size = 4269357 }, + { url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d", size = 4412295 }, + { url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d", size = 3516913 }, + { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829 }, + { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277 }, + { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433 }, + { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119 }, + { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314 }, + { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768 }, ] [[package]] @@ -1335,94 +1341,94 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687 }, ] [[package]] name = "markupsafe" version = "3.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, - { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631 }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057 }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050 }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681 }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705 }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524 }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282 }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745 }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571 }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056 }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932 }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631 }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058 }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287 }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940 }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887 }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692 }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471 }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923 }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572 }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077 }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876 }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615 }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020 }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332 }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947 }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962 }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760 }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529 }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015 }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540 }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105 }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906 }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622 }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029 }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374 }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980 }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990 }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784 }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588 }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041 }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543 }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113 }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911 }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658 }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066 }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639 }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569 }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284 }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801 }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769 }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642 }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612 }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200 }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973 }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619 }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029 }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408 }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005 }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048 }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821 }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606 }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043 }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747 }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341 }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073 }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661 }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069 }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670 }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598 }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261 }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835 }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733 }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672 }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819 }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426 }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146 }, ] [[package]] @@ -1432,18 +1438,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150 } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534 }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, ] [[package]] @@ -1453,18 +1459,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/92/328a294a6de83bacb95bed01f04e0eaff4e3616ee359fc821a5dfc539b02/mistune-3.3.4.tar.gz", hash = "sha256:58b5c96d6fcb61190dfe5fae498d2b2065f99cf61e9649418fd54cf1ada86dfe", size = 121426, upload-time = "2026-07-22T05:22:30.89Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/92/328a294a6de83bacb95bed01f04e0eaff4e3616ee359fc821a5dfc539b02/mistune-3.3.4.tar.gz", hash = "sha256:58b5c96d6fcb61190dfe5fae498d2b2065f99cf61e9649418fd54cf1ada86dfe", size = 121426 } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl", hash = "sha256:ee015381e955e370962968befe1d729ab60fafb6a715ac6751763fbce38c8d4a", size = 66862, upload-time = "2026-07-22T05:22:29.419Z" }, + { url = "https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl", hash = "sha256:ee015381e955e370962968befe1d729ab60fafb6a715ac6751763fbce38c8d4a", size = 66862 }, ] [[package]] name = "more-itertools" version = "10.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667 }, ] [[package]] @@ -1477,9 +1483,9 @@ dependencies = [ { name = "nbformat" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/a5/b3bae4b590c0cbcada2c63a34f7580024e834a8ba213e949a2f906705787/nbclient-0.11.0.tar.gz", hash = "sha256:04a134a5b087f2c5887f228aca155db50169b8cd9334dee6942c8e927e56081a", size = 62535, upload-time = "2026-06-05T07:52:41.746Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/a5/b3bae4b590c0cbcada2c63a34f7580024e834a8ba213e949a2f906705787/nbclient-0.11.0.tar.gz", hash = "sha256:04a134a5b087f2c5887f228aca155db50169b8cd9334dee6942c8e927e56081a", size = 62535 } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl", hash = "sha256:ef7fa0d59d6e1d41103933d8a445a18d5de860ca6b613b87b8574accdb3c2895", size = 25288, upload-time = "2026-06-05T07:52:40.115Z" }, + { url = "https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl", hash = "sha256:ef7fa0d59d6e1d41103933d8a445a18d5de860ca6b613b87b8574accdb3c2895", size = 25288 }, ] [[package]] @@ -1502,9 +1508,9 @@ dependencies = [ { name = "pygments" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311 } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8", size = 261927, upload-time = "2026-04-08T00:44:12.845Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8", size = 261927 }, ] [[package]] @@ -1517,18 +1523,18 @@ dependencies = [ { name = "jupyter-core" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/72/b3446efab8756e7df4b8ec587f8e611cb5a7249e4323db480802f1d3be04/nbformat-5.11.1.tar.gz", hash = "sha256:32d4521c68c6e7d5b29c76defaeed9f42ea733142b9b19f88277ce10390b9c4d", size = 147775, upload-time = "2026-08-17T08:10:51.942Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/72/b3446efab8756e7df4b8ec587f8e611cb5a7249e4323db480802f1d3be04/nbformat-5.11.1.tar.gz", hash = "sha256:32d4521c68c6e7d5b29c76defaeed9f42ea733142b9b19f88277ce10390b9c4d", size = 147775 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/69/ee613f74085ca7103f79cd08d579c4f3177d1d26e5d3d9528d2d6536a707/nbformat-5.11.1-py3-none-any.whl", hash = "sha256:cc6698fa75f4fab8755ead786317815f13a6fee3b53311c0abb1a8b51d52f7ec", size = 79849, upload-time = "2026-08-17T08:10:50.18Z" }, + { url = "https://files.pythonhosted.org/packages/1a/69/ee613f74085ca7103f79cd08d579c4f3177d1d26e5d3d9528d2d6536a707/nbformat-5.11.1-py3-none-any.whl", hash = "sha256:cc6698fa75f4fab8755ead786317815f13a6fee3b53311c0abb1a8b51d52f7ec", size = 79849 }, ] [[package]] name = "nest-asyncio2" version = "1.7.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/a6/a2775b388a14b5ea0c894bbe513c0d3e04e65e8ae583e60f5357722b8e03/nest_asyncio2-1.7.3.tar.gz", hash = "sha256:2e9a84d5d1efe6d020c72988d21aec569bac42d98af2ff6b9de24640c5d22a34", size = 15642, upload-time = "2026-09-22T17:15:06.709Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a6/a2775b388a14b5ea0c894bbe513c0d3e04e65e8ae583e60f5357722b8e03/nest_asyncio2-1.7.3.tar.gz", hash = "sha256:2e9a84d5d1efe6d020c72988d21aec569bac42d98af2ff6b9de24640c5d22a34", size = 15642 } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/dd/a8bebc78975a7466932077a0973cbab4361a32368b1d28d40683ec19333c/nest_asyncio2-1.7.3-py3-none-any.whl", hash = "sha256:2bc87bdca654e719425145f5e84eaeb0080b013fdd47d65729a7d66243f4987c", size = 8364, upload-time = "2026-09-22T17:15:05.492Z" }, + { url = "https://files.pythonhosted.org/packages/29/dd/a8bebc78975a7466932077a0973cbab4361a32368b1d28d40683ec19333c/nest_asyncio2-1.7.3-py3-none-any.whl", hash = "sha256:2bc87bdca654e719425145f5e84eaeb0080b013fdd47d65729a7d66243f4987c", size = 8364 }, ] [[package]] @@ -1538,9 +1544,9 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11'", ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263 }, ] [[package]] @@ -1552,43 +1558,43 @@ resolution-markers = [ "python_full_version >= '3.12' and python_full_version < '3.14'", "python_full_version == '3.11.*'", ] -sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065, upload-time = "2025-05-29T11:35:07.804Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065 } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406 }, ] [[package]] name = "nh3" version = "0.3.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/2f/022b27146d52d24b1b353b003359134788ecbcd6fcdf6283adbd57c0fbc8/nh3-0.3.7.tar.gz", hash = "sha256:71860d01c16f4d8c72e334e0674beb2b0899dbd0bf760de18932ef4390303848", size = 25662, upload-time = "2026-08-23T14:26:30.728Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/88/b594f0e86856b37e182fb663283da419eea6424972506e640e890885467f/nh3-0.3.7-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:91a4dab4e94d9fc54b9f67b1adfb23e81fab7ab43f33c3b8c97be9aa38f789ba", size = 1471147, upload-time = "2026-08-23T14:25:55.259Z" }, - { url = "https://files.pythonhosted.org/packages/1e/60/847a21339f095c4d4c655af31fa2d18b174585bcc210709facacc7ce205c/nh3-0.3.7-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eae64328e46a25785535afcb6885b6f182ecaf5ee8c88f8c075422db8aacc65b", size = 820463, upload-time = "2026-08-23T14:25:56.803Z" }, - { url = "https://files.pythonhosted.org/packages/7b/7f/1a103e00aaf5e59f2dee4c2709aac609bb2d4bb74fddaf0dcfade11ed87b/nh3-0.3.7-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4968fe8d2db97c6f047659bf46a449fd8ec377f44ebf3e0a1b96c0d3a333ae32", size = 861456, upload-time = "2026-08-23T14:25:58.087Z" }, - { url = "https://files.pythonhosted.org/packages/d8/4a/e9c436089a0c80b928011ead0efd156aa7639a19b6064ef58dcedcab8369/nh3-0.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:be53a4825585f701955cb9baf49f478f56eb81e20294329fe4bc689dd5dd81fa", size = 1023930, upload-time = "2026-08-23T14:25:59.465Z" }, - { url = "https://files.pythonhosted.org/packages/04/5c/aa1468e3e281e78d2b3b7d762ccba59f681af355e971dbd255d5903f7b86/nh3-0.3.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:94fd6e59553fbb9ffd8ba71bbd5a54e3126ba01799a097ae30d5341d750bc6ac", size = 1102614, upload-time = "2026-08-23T14:26:00.869Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9f/57d186d9d3dd38905dc12dddb3484406cdf6aa0b1ce33639a2d277d4ee1c/nh3-0.3.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:18f4278ecd157d43cb35acd5aae9f35cfa79f546b4922bd86536adc0f6312102", size = 1059915, upload-time = "2026-08-23T14:26:02.388Z" }, - { url = "https://files.pythonhosted.org/packages/6b/53/097a5ad0b34b15d67a472ef849165a54209fa5fbd3e639801c6fe439ba28/nh3-0.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:808def0c8c07843e6e50dc84f532457bfa2cfd17417b219a5d9e7c773709331a", size = 1047402, upload-time = "2026-08-23T14:26:03.897Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/c57a2c70534418310889a65ccfac3525e62f0bc0a8613225903403755ce7/nh3-0.3.7-cp314-cp314t-win32.whl", hash = "sha256:874b7d67a067bd29a59223f6270fc30da4edd8e6d87fd219fc93bcbaa662c946", size = 619895, upload-time = "2026-08-23T14:26:05.105Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b7/efda1d0a611d940bdfde6893bde1ea6b7b7d48c31273aea48e35b822fd58/nh3-0.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:614dac4a4c36ad084e78447d16fe898dedd762e354a7ab9cda2984e82f67883d", size = 633456, upload-time = "2026-08-23T14:26:06.661Z" }, - { url = "https://files.pythonhosted.org/packages/1d/18/3ab564595cb88196f50d26e163ed0fd2acc731ab26ac615df91981885887/nh3-0.3.7-cp314-cp314t-win_arm64.whl", hash = "sha256:157ec1eb7a62f3d9a7badb8d82d89aa810e3e24e097eedfa481a25d0c8a99877", size = 611003, upload-time = "2026-08-23T14:26:07.813Z" }, - { url = "https://files.pythonhosted.org/packages/94/0d/c257754bf57f829f307aa226bbe136d3a1356b5a0d08324c7b6bd2a8aacd/nh3-0.3.7-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6c3aa50eb26e9228238271db9f983cbc3b006dfbfeca2d4dc34c33ddc6ac5ea5", size = 1493959, upload-time = "2026-08-23T14:26:09.025Z" }, - { url = "https://files.pythonhosted.org/packages/07/42/a687e7091928806e514f89fa2666f25ec9bfe0a902fc4402b25e51ce408b/nh3-0.3.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f266d3f1b3647449923a8e406524632220dd5d8b647078dfe45b885d33d10479", size = 859615, upload-time = "2026-08-23T14:26:10.606Z" }, - { url = "https://files.pythonhosted.org/packages/85/05/b0e6bef633549a23347d5462aa288fcc42381e7918482062ca3cb456242a/nh3-0.3.7-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8fd1ab205258b29254f72db377d99e2c96aa7653ef3b015ccab0420b094b506", size = 839872, upload-time = "2026-08-23T14:26:12.037Z" }, - { url = "https://files.pythonhosted.org/packages/17/40/2a0921d45b20828708bcb56887e47dcf8cae13818de5bf9a01308d348712/nh3-0.3.7-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:19f288c938ec6eef1f5d2c6cab47838e71fef8097e1c1233802be5a6230ba086", size = 1091325, upload-time = "2026-08-23T14:26:13.34Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d1/9d70e0e418a48280ec0ddc6c1b08b4b1136ebcc31a1625e57ff5c665fa51/nh3-0.3.7-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de2b2aab32ea303405debefdcfc58043d3e635fa3f67b9eb140d2b0e0c0d2563", size = 1042482, upload-time = "2026-08-23T14:26:14.667Z" }, - { url = "https://files.pythonhosted.org/packages/93/a7/02dd159d4e71f98607d8d4249cddb7561e77be1a8e4dec77d76e1b68fc99/nh3-0.3.7-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b7279d43323a25225df23576af6594a16693f61431170848b8b2ac21ad4f174", size = 946868, upload-time = "2026-08-23T14:26:16.094Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ed/c5510c615dce55b6fcc364aa1838142f938beed64f5e4927490dfcaf4405/nh3-0.3.7-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70f5ac8626e899a4bab0ef74ca2f5bd602f49c7b739e6e5026b4afc6d63dac42", size = 832161, upload-time = "2026-08-23T14:26:17.272Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e3/3212c1a5b5745245d7f18885207bbddb34c56075f34dd682bd539aad55cc/nh3-0.3.7-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:5ffdfcb9a686ffb12765376bcfb6b5b55728516d3c0ee317d29982381ded3df8", size = 849791, upload-time = "2026-08-23T14:26:18.498Z" }, - { url = "https://files.pythonhosted.org/packages/20/64/9e36594efad6c290de4240d02cb2bd80c339a4ab1c4de66e599ffa6d9d81/nh3-0.3.7-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bc42bb1193c1e28a1e74c2cabaca178e118a7103e8832699fef8a2b3e2496493", size = 875473, upload-time = "2026-08-23T14:26:19.908Z" }, - { url = "https://files.pythonhosted.org/packages/00/0c/1a8985fd43fea5530c0ac890b6f0b423770ee72f111b70b7a77f2dec243a/nh3-0.3.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d56e76bd3cadb09b6b0cef364850811663734b348a25f5f587a2819c495367bd", size = 1036463, upload-time = "2026-08-23T14:26:21.536Z" }, - { url = "https://files.pythonhosted.org/packages/b2/5d/891e533b716cf00df76ad0ba6485dcfd14d59a6430a3cc99057c4c04004e/nh3-0.3.7-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:fd4a70efb45d5372174f718878eb7a35c12677626a63b2f103b23b833457dcac", size = 1116029, upload-time = "2026-08-23T14:26:22.907Z" }, - { url = "https://files.pythonhosted.org/packages/42/e5/ae8c0782fce74fb6fcf7234bb3d4017f37ce181b4f9d29369eab21c50a04/nh3-0.3.7-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:15f5fbf090f5c88d61c820e1fc1fceecb6520cca9fe85649c06b57ef9dc9ff62", size = 1076589, upload-time = "2026-08-23T14:26:24.302Z" }, - { url = "https://files.pythonhosted.org/packages/26/a4/c3423351e8d864ad756e85e15f0c01433361f14d34e4ed156482c0518f2a/nh3-0.3.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6698a822132beedab80f131c08d8d0ac5a178ddeb488d02ca4b67716ecfac7af", size = 1058871, upload-time = "2026-08-23T14:26:25.674Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6a/478f153f1d7c0baaa3d1e8bb5fdcee3a6235f90fe44ea969a9d4e2b8c47a/nh3-0.3.7-cp38-abi3-win32.whl", hash = "sha256:6e4280115d44c3b278eef712a86748c1a723105cd79feec46952383117ab4e59", size = 630729, upload-time = "2026-08-23T14:26:26.932Z" }, - { url = "https://files.pythonhosted.org/packages/b4/b9/34433ccb1f0fe6968dabbb7d4bf5721c6221878ef07832748c06655a6a80/nh3-0.3.7-cp38-abi3-win_amd64.whl", hash = "sha256:618e3059caf41ccdf5dcccb3fa9df4cf6e4efe23d1382a8bbfca272a8a4f8bfc", size = 644462, upload-time = "2026-08-23T14:26:28.294Z" }, - { url = "https://files.pythonhosted.org/packages/f9/70/e140dffff6e808dc6343598df76e7e2407fd0f581de3524c75fba2e0cf24/nh3-0.3.7-cp38-abi3-win_arm64.whl", hash = "sha256:f04b7d333b27f13ca439da3cf1c75c2fba34f104969f6ce4ac8e7079699c2f4a", size = 621867, upload-time = "2026-08-23T14:26:29.547Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/18/2f/022b27146d52d24b1b353b003359134788ecbcd6fcdf6283adbd57c0fbc8/nh3-0.3.7.tar.gz", hash = "sha256:71860d01c16f4d8c72e334e0674beb2b0899dbd0bf760de18932ef4390303848", size = 25662 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/88/b594f0e86856b37e182fb663283da419eea6424972506e640e890885467f/nh3-0.3.7-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:91a4dab4e94d9fc54b9f67b1adfb23e81fab7ab43f33c3b8c97be9aa38f789ba", size = 1471147 }, + { url = "https://files.pythonhosted.org/packages/1e/60/847a21339f095c4d4c655af31fa2d18b174585bcc210709facacc7ce205c/nh3-0.3.7-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eae64328e46a25785535afcb6885b6f182ecaf5ee8c88f8c075422db8aacc65b", size = 820463 }, + { url = "https://files.pythonhosted.org/packages/7b/7f/1a103e00aaf5e59f2dee4c2709aac609bb2d4bb74fddaf0dcfade11ed87b/nh3-0.3.7-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4968fe8d2db97c6f047659bf46a449fd8ec377f44ebf3e0a1b96c0d3a333ae32", size = 861456 }, + { url = "https://files.pythonhosted.org/packages/d8/4a/e9c436089a0c80b928011ead0efd156aa7639a19b6064ef58dcedcab8369/nh3-0.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:be53a4825585f701955cb9baf49f478f56eb81e20294329fe4bc689dd5dd81fa", size = 1023930 }, + { url = "https://files.pythonhosted.org/packages/04/5c/aa1468e3e281e78d2b3b7d762ccba59f681af355e971dbd255d5903f7b86/nh3-0.3.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:94fd6e59553fbb9ffd8ba71bbd5a54e3126ba01799a097ae30d5341d750bc6ac", size = 1102614 }, + { url = "https://files.pythonhosted.org/packages/6a/9f/57d186d9d3dd38905dc12dddb3484406cdf6aa0b1ce33639a2d277d4ee1c/nh3-0.3.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:18f4278ecd157d43cb35acd5aae9f35cfa79f546b4922bd86536adc0f6312102", size = 1059915 }, + { url = "https://files.pythonhosted.org/packages/6b/53/097a5ad0b34b15d67a472ef849165a54209fa5fbd3e639801c6fe439ba28/nh3-0.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:808def0c8c07843e6e50dc84f532457bfa2cfd17417b219a5d9e7c773709331a", size = 1047402 }, + { url = "https://files.pythonhosted.org/packages/9a/a7/c57a2c70534418310889a65ccfac3525e62f0bc0a8613225903403755ce7/nh3-0.3.7-cp314-cp314t-win32.whl", hash = "sha256:874b7d67a067bd29a59223f6270fc30da4edd8e6d87fd219fc93bcbaa662c946", size = 619895 }, + { url = "https://files.pythonhosted.org/packages/e6/b7/efda1d0a611d940bdfde6893bde1ea6b7b7d48c31273aea48e35b822fd58/nh3-0.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:614dac4a4c36ad084e78447d16fe898dedd762e354a7ab9cda2984e82f67883d", size = 633456 }, + { url = "https://files.pythonhosted.org/packages/1d/18/3ab564595cb88196f50d26e163ed0fd2acc731ab26ac615df91981885887/nh3-0.3.7-cp314-cp314t-win_arm64.whl", hash = "sha256:157ec1eb7a62f3d9a7badb8d82d89aa810e3e24e097eedfa481a25d0c8a99877", size = 611003 }, + { url = "https://files.pythonhosted.org/packages/94/0d/c257754bf57f829f307aa226bbe136d3a1356b5a0d08324c7b6bd2a8aacd/nh3-0.3.7-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6c3aa50eb26e9228238271db9f983cbc3b006dfbfeca2d4dc34c33ddc6ac5ea5", size = 1493959 }, + { url = "https://files.pythonhosted.org/packages/07/42/a687e7091928806e514f89fa2666f25ec9bfe0a902fc4402b25e51ce408b/nh3-0.3.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f266d3f1b3647449923a8e406524632220dd5d8b647078dfe45b885d33d10479", size = 859615 }, + { url = "https://files.pythonhosted.org/packages/85/05/b0e6bef633549a23347d5462aa288fcc42381e7918482062ca3cb456242a/nh3-0.3.7-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8fd1ab205258b29254f72db377d99e2c96aa7653ef3b015ccab0420b094b506", size = 839872 }, + { url = "https://files.pythonhosted.org/packages/17/40/2a0921d45b20828708bcb56887e47dcf8cae13818de5bf9a01308d348712/nh3-0.3.7-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:19f288c938ec6eef1f5d2c6cab47838e71fef8097e1c1233802be5a6230ba086", size = 1091325 }, + { url = "https://files.pythonhosted.org/packages/e4/d1/9d70e0e418a48280ec0ddc6c1b08b4b1136ebcc31a1625e57ff5c665fa51/nh3-0.3.7-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de2b2aab32ea303405debefdcfc58043d3e635fa3f67b9eb140d2b0e0c0d2563", size = 1042482 }, + { url = "https://files.pythonhosted.org/packages/93/a7/02dd159d4e71f98607d8d4249cddb7561e77be1a8e4dec77d76e1b68fc99/nh3-0.3.7-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b7279d43323a25225df23576af6594a16693f61431170848b8b2ac21ad4f174", size = 946868 }, + { url = "https://files.pythonhosted.org/packages/a6/ed/c5510c615dce55b6fcc364aa1838142f938beed64f5e4927490dfcaf4405/nh3-0.3.7-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70f5ac8626e899a4bab0ef74ca2f5bd602f49c7b739e6e5026b4afc6d63dac42", size = 832161 }, + { url = "https://files.pythonhosted.org/packages/7b/e3/3212c1a5b5745245d7f18885207bbddb34c56075f34dd682bd539aad55cc/nh3-0.3.7-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:5ffdfcb9a686ffb12765376bcfb6b5b55728516d3c0ee317d29982381ded3df8", size = 849791 }, + { url = "https://files.pythonhosted.org/packages/20/64/9e36594efad6c290de4240d02cb2bd80c339a4ab1c4de66e599ffa6d9d81/nh3-0.3.7-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bc42bb1193c1e28a1e74c2cabaca178e118a7103e8832699fef8a2b3e2496493", size = 875473 }, + { url = "https://files.pythonhosted.org/packages/00/0c/1a8985fd43fea5530c0ac890b6f0b423770ee72f111b70b7a77f2dec243a/nh3-0.3.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d56e76bd3cadb09b6b0cef364850811663734b348a25f5f587a2819c495367bd", size = 1036463 }, + { url = "https://files.pythonhosted.org/packages/b2/5d/891e533b716cf00df76ad0ba6485dcfd14d59a6430a3cc99057c4c04004e/nh3-0.3.7-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:fd4a70efb45d5372174f718878eb7a35c12677626a63b2f103b23b833457dcac", size = 1116029 }, + { url = "https://files.pythonhosted.org/packages/42/e5/ae8c0782fce74fb6fcf7234bb3d4017f37ce181b4f9d29369eab21c50a04/nh3-0.3.7-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:15f5fbf090f5c88d61c820e1fc1fceecb6520cca9fe85649c06b57ef9dc9ff62", size = 1076589 }, + { url = "https://files.pythonhosted.org/packages/26/a4/c3423351e8d864ad756e85e15f0c01433361f14d34e4ed156482c0518f2a/nh3-0.3.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6698a822132beedab80f131c08d8d0ac5a178ddeb488d02ca4b67716ecfac7af", size = 1058871 }, + { url = "https://files.pythonhosted.org/packages/4b/6a/478f153f1d7c0baaa3d1e8bb5fdcee3a6235f90fe44ea969a9d4e2b8c47a/nh3-0.3.7-cp38-abi3-win32.whl", hash = "sha256:6e4280115d44c3b278eef712a86748c1a723105cd79feec46952383117ab4e59", size = 630729 }, + { url = "https://files.pythonhosted.org/packages/b4/b9/34433ccb1f0fe6968dabbb7d4bf5721c6221878ef07832748c06655a6a80/nh3-0.3.7-cp38-abi3-win_amd64.whl", hash = "sha256:618e3059caf41ccdf5dcccb3fa9df4cf6e4efe23d1382a8bbfca272a8a4f8bfc", size = 644462 }, + { url = "https://files.pythonhosted.org/packages/f9/70/e140dffff6e808dc6343598df76e7e2407fd0f581de3524c75fba2e0cf24/nh3-0.3.7-cp38-abi3-win_arm64.whl", hash = "sha256:f04b7d333b27f13ca439da3cf1c75c2fba34f104969f6ce4ac8e7079699c2f4a", size = 621867 }, ] [[package]] @@ -1598,116 +1604,114 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-server" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, + { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307 }, ] [[package]] name = "numpy" version = "1.26.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload-time = "2024-02-05T23:48:01.194Z" }, - { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411, upload-time = "2024-02-05T23:48:29.038Z" }, - { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016, upload-time = "2024-02-05T23:48:54.098Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889, upload-time = "2024-02-05T23:49:25.361Z" }, - { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746, upload-time = "2024-02-05T23:49:51.983Z" }, - { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620, upload-time = "2024-02-05T23:50:22.515Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659, upload-time = "2024-02-05T23:50:35.834Z" }, - { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905, upload-time = "2024-02-05T23:51:03.701Z" }, - { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, - { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, - { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, - { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, - { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, - { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, - { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, - { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, - { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, - { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, - { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, - { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, - { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, - { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468 }, + { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411 }, + { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016 }, + { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889 }, + { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746 }, + { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620 }, + { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659 }, + { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905 }, + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554 }, + { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127 }, + { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994 }, + { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005 }, + { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297 }, + { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567 }, + { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812 }, + { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913 }, + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901 }, + { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868 }, + { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109 }, + { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613 }, + { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172 }, + { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643 }, + { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803 }, + { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754 }, ] [[package]] name = "opentrons" -version = "9.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aionotify" }, - { name = "anyio" }, - { name = "click" }, - { name = "jsonschema" }, - { name = "numpy" }, - { name = "opentrons-shared-data" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyro5" }, - { name = "pyserial" }, - { name = "pyusb" }, - { name = "typing-extensions" }, +version = "8.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aionotify", marker = "python_full_version < '3.11'" }, + { name = "anyio", marker = "python_full_version < '3.11'" }, + { name = "click", marker = "python_full_version < '3.11'" }, + { name = "jsonschema", marker = "python_full_version < '3.11'" }, + { name = "numpy", marker = "python_full_version < '3.11'" }, + { name = "opentrons-shared-data", marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pydantic", marker = "python_full_version < '3.11'" }, + { name = "pydantic-settings", marker = "python_full_version < '3.11'" }, + { name = "pyserial", marker = "python_full_version < '3.11'" }, + { name = "pyusb", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/6b/59576b5140bdf1373f9457fc0340640a46d56295a8fd30ebf2abde908587/opentrons-9.1.2.tar.gz", hash = "sha256:93e53312909b3e32e3838877336d225558ad34c4f6b22875ea910b3fd2c56abf", size = 2346130, upload-time = "2026-08-28T15:46:40.757Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/f9/8e9fb367276a6fa56268f3ca40202994529f691ce4a10536bcacd782aff9/opentrons-8.8.2.tar.gz", hash = "sha256:a77a32d86a7e92a6ee7277d9d44518084c3ec5de6e5e23542f1515b4335de9d3", size = 2154281 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/6d/c7f0305b9d753d60831ae77d2d7f56fa904949f2c4eb931b2708b278c8be/opentrons-9.1.2-py3-none-any.whl", hash = "sha256:ec3fd3f208e8a7adf2f51ed435ee0d9d7e251cf2843a416bf2fc3eb3b9475d21", size = 1973473, upload-time = "2026-08-28T15:46:38.949Z" }, + { url = "https://files.pythonhosted.org/packages/52/f6/7329e1e0aca3e4dfd27719dc7ab72ad9764541cb937b0c7321ad43f4191e/opentrons-8.8.2-py3-none-any.whl", hash = "sha256:e6f455a1aa807c7787f3d469757d3c7b6ada0fa0390a8319106bdbe2dd85e89d", size = 1870227 }, ] [[package]] name = "opentrons-shared-data" -version = "9.1.2" +version = "8.8.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jsonschema" }, - { name = "numpy" }, - { name = "pydantic" }, - { name = "typing-extensions" }, + { name = "jsonschema", marker = "python_full_version < '3.11'" }, + { name = "pydantic", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/6f/985cba00630a6b323bcfcd0defa7d1108a0e12783705d7d5f668c187586c/opentrons_shared_data-9.1.2.tar.gz", hash = "sha256:c42f5bbbd532e216b4ebc7ee7cdfaacb5f04d735a7121ad6a4f87463b9e3183a", size = 771543, upload-time = "2026-08-10T19:10:23.096Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/b4/6d66d9b106a2c81f4c91e18e224bc55908df1f10711cb03e9934e65cde36/opentrons_shared_data-8.8.2.tar.gz", hash = "sha256:b07a3343d5986cb1d49bb0005642425067955b0b8282d208a5f42ab69f1bb9a4", size = 658731 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/59/8ca320aba8df6be0d11ec6ed9149fca1051bbf0736dc881f08f372396cf3/opentrons_shared_data-9.1.2-py3-none-any.whl", hash = "sha256:f48131173e6ff6424e5c846b4cc8ba5cfc3b6809a792efa2b6008e132e7b1365", size = 1291704, upload-time = "2026-08-10T19:10:21.201Z" }, + { url = "https://files.pythonhosted.org/packages/98/40/a6620ced846254d020e350cccdf19cd505628e8dca1b82b93cfcddb242f8/opentrons_shared_data-8.8.2-py3-none-any.whl", hash = "sha256:12d77eab3a41bfb4e5a947f7ca5062d40eb1fcffcf2a6d12a317af7ac249b6f3", size = 1169425 }, ] [[package]] name = "overrides" version = "7.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832 }, ] [[package]] name = "packaging" version = "26.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412 } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956 }, ] [[package]] name = "pandocfilters" version = "1.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663 }, ] [[package]] name = "parso" version = "0.8.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824 } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025 }, ] [[package]] @@ -1717,27 +1721,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, ] [[package]] name = "platformdirs" version = "4.11.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/dd/65804b0c2925a1c821a05502ea57517b69a073ff400d25ab9faa3a2cf012/platformdirs-4.11.12.tar.gz", hash = "sha256:e8dc1cb58f1153fd7f61db1374317770baababec2480b37b8f01c6cc25b45267", size = 40016, upload-time = "2026-09-22T01:02:36.973Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/dd/65804b0c2925a1c821a05502ea57517b69a073ff400d25ab9faa3a2cf012/platformdirs-4.11.12.tar.gz", hash = "sha256:e8dc1cb58f1153fd7f61db1374317770baababec2480b37b8f01c6cc25b45267", size = 40016 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/2b/69de6317fbf48865bb12aa56c9cfcfff46b3add0cd45250725d96146dce0/platformdirs-4.11.12-py3-none-any.whl", hash = "sha256:b1ba966ac76153d9985a0787ffab951e7c18416189873d4e4ddb2c8a04a0a254", size = 24976, upload-time = "2026-09-22T01:02:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2b/69de6317fbf48865bb12aa56c9cfcfff46b3add0cd45250725d96146dce0/platformdirs-4.11.12-py3-none-any.whl", hash = "sha256:b1ba966ac76153d9985a0787ffab951e7c18416189873d4e4ddb2c8a04a0a254", size = 24976 }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, ] [[package]] @@ -1747,18 +1751,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/b1/85e18ac92afd08c533603e3393977b6bc1443043115a47bb094f3b98f94f/prettytable-3.16.0.tar.gz", hash = "sha256:3c64b31719d961bf69c9a7e03d0c1e477320906a98da63952bc6698d6164ff57", size = 66276, upload-time = "2025-03-24T19:39:04.008Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/b1/85e18ac92afd08c533603e3393977b6bc1443043115a47bb094f3b98f94f/prettytable-3.16.0.tar.gz", hash = "sha256:3c64b31719d961bf69c9a7e03d0c1e477320906a98da63952bc6698d6164ff57", size = 66276 } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/c7/5613524e606ea1688b3bdbf48aa64bafb6d0a4ac3750274c43b6158a390f/prettytable-3.16.0-py3-none-any.whl", hash = "sha256:b5eccfabb82222f5aa46b798ff02a8452cf530a352c31bddfa29be41242863aa", size = 33863, upload-time = "2025-03-24T19:39:02.359Z" }, + { url = "https://files.pythonhosted.org/packages/02/c7/5613524e606ea1688b3bdbf48aa64bafb6d0a4ac3750274c43b6158a390f/prettytable-3.16.0-py3-none-any.whl", hash = "sha256:b5eccfabb82222f5aa46b798ff02a8452cf530a352c31bddfa29be41242863aa", size = 33863 }, ] [[package]] name = "prometheus-client" version = "0.26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/73/f1334c29c2af4cd9dba6c7817e61b611bd0215e2eb5565c6064a4de18802/prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b", size = 92910, upload-time = "2026-07-24T19:36:41.893Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/73/f1334c29c2af4cd9dba6c7817e61b611bd0215e2eb5565c6064a4de18802/prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b", size = 92910 } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/a3/b69efbf4143b5b9859b977770bbbabcc2796b702fa69dc40271e45cd5a56/prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6", size = 64494, upload-time = "2026-07-24T19:36:40.854Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a3/b69efbf4143b5b9859b977770bbbabcc2796b702fa69dc40271e45cd5a56/prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6", size = 64494 }, ] [[package]] @@ -1768,77 +1772,64 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/ea/39b988c938f75cb75d7045b5c69f8bfed47ee2152c8837fb403de29d6fb8/prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6", size = 435492, upload-time = "2026-07-26T20:56:14.758Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ea/39b988c938f75cb75d7045b5c69f8bfed47ee2152c8837fb403de29d6fb8/prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6", size = 435492 } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288, upload-time = "2026-07-26T20:56:12.512Z" }, + { url = "https://files.pythonhosted.org/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288 }, ] [[package]] name = "psutil" version = "7.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, - { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, - { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, - { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, - { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, - { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, - { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, - { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, - { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, - { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, - { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, - { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, - { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, - { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, - { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, - { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, - { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, - { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, - { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595 }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082 }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476 }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062 }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893 }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589 }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664 }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087 }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383 }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210 }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228 }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284 }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090 }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859 }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560 }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997 }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972 }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266 }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737 }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617 }, ] [[package]] name = "ptyprocess" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, -] - -[[package]] -name = "pudupy" -version = "1.0.1a1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentrons" }, - { name = "xlsxwriter" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/25/09/255b4415d1cc2cf5ce18b3447c7fe6ecc1b32b24689d9e1a054885179eac/pudupy-1.0.1a1.tar.gz", hash = "sha256:abc169524574bc63c7611fc7bea33202e8f2c85b925256e8c8c7b7866c08c11e", size = 71533, upload-time = "2026-05-28T21:02:02.276Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/42/3e8cd81db9b28fcce1ef13e73364c7a1de187758608c841df0401588250e/pudupy-1.0.1a1-py3-none-any.whl", hash = "sha256:32cec1dfcaea9696574ecfbab04a454e2228e85c5935c906cd32b91a88719b93", size = 64131, upload-time = "2026-05-28T21:02:01.253Z" }, + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, ] [[package]] name = "pure-eval" version = "0.2.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/9f/abfd2959e9261dd5217ca8551d4de211ca6ab26fe9b72cf44731ff6c4442/pure_eval-0.2.4.tar.gz", hash = "sha256:260c2774686e651b79f8b8e7fc9d80b3599ea6a66334b47d5f4abb69fc2c0ea1", size = 20563, upload-time = "2026-09-10T21:41:22.836Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/9f/abfd2959e9261dd5217ca8551d4de211ca6ab26fe9b72cf44731ff6c4442/pure_eval-0.2.4.tar.gz", hash = "sha256:260c2774686e651b79f8b8e7fc9d80b3599ea6a66334b47d5f4abb69fc2c0ea1", size = 20563 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/18/83376915176eb058cb86470eb7396388a13350b09a8f233a79303bbcbc5b/pure_eval-0.2.4-py3-none-any.whl", hash = "sha256:96cae060a313cfaad51bb761278bfb0e62dc0248d9315a81173752dc546cd37a", size = 11893, upload-time = "2026-09-10T21:41:21.532Z" }, + { url = "https://files.pythonhosted.org/packages/6d/18/83376915176eb058cb86470eb7396388a13350b09a8f233a79303bbcbc5b/pure_eval-0.2.4-py3-none-any.whl", hash = "sha256:96cae060a313cfaad51bb761278bfb0e62dc0248d9315a81173752dc546cd37a", size = 11893 }, ] [[package]] name = "pycparser" version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172 }, ] [[package]] @@ -1846,14 +1837,14 @@ name = "pydantic" version = "2.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, + { name = "annotated-types", marker = "python_full_version < '3.11'" }, + { name = "pydantic-core", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-inspection", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750 } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, + { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589 }, ] [[package]] @@ -1861,115 +1852,115 @@ name = "pydantic-core" version = "2.46.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/6b/8f79692844269427abb3e4dd9e68edfcbe65ae25527d99183214de716c59/pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6", size = 2076533, upload-time = "2026-08-28T09:57:35.421Z" }, - { url = "https://files.pythonhosted.org/packages/bd/d0/c787604c71c2bdcda1a5656942fc822cd0f9cd879b9484bb84fc42172703/pydantic_core-2.46.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615", size = 1924650, upload-time = "2026-08-28T09:57:37.944Z" }, - { url = "https://files.pythonhosted.org/packages/4a/77/ca2f8e997d9bfdb32205297aff38f210f398822d895b1af1b59fd9df9c13/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb", size = 1951261, upload-time = "2026-08-28T09:57:39.339Z" }, - { url = "https://files.pythonhosted.org/packages/a0/53/bd12e1a9255df4edee00353778e2614b5346265d51e1567ab72153e803a2/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b", size = 2021808, upload-time = "2026-08-28T09:57:40.69Z" }, - { url = "https://files.pythonhosted.org/packages/d7/41/f7f312751ebc6d6767da91964a9c7954c18e226a1720ab234e3dfb9d6c17/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6", size = 2196184, upload-time = "2026-08-28T09:57:42.275Z" }, - { url = "https://files.pythonhosted.org/packages/3d/93/ce93aa030ab6bac4683ba8861e7baad89dd24b02e66b8801a0e4f6a00311/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793", size = 2238212, upload-time = "2026-08-28T09:57:44.122Z" }, - { url = "https://files.pythonhosted.org/packages/34/a1/c8e6b66f499f510752c07a092dfe27621f9c255635e59d38704b5681c35a/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b", size = 2064073, upload-time = "2026-08-28T09:57:45.613Z" }, - { url = "https://files.pythonhosted.org/packages/5c/fa/605e2b127ee30dbf4b1da9da4843587cf2b2d16486c241cc7a5be2d2c1bd/pydantic_core-2.46.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461", size = 2093102, upload-time = "2026-08-28T09:57:46.953Z" }, - { url = "https://files.pythonhosted.org/packages/4a/f7/1ab28093c09032ddce7c92c7a55d503b6ecd70f42c32492946c1cb5477b1/pydantic_core-2.46.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736", size = 2133452, upload-time = "2026-08-28T09:57:48.362Z" }, - { url = "https://files.pythonhosted.org/packages/30/c8/47c79b756f12f85e8b0fbdb2b495f6b6eb32e6c98a4beae7a570a0b7c63c/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3", size = 2146477, upload-time = "2026-08-28T09:57:49.74Z" }, - { url = "https://files.pythonhosted.org/packages/13/5c/79fc00cb8f651d6061991de8d7cedf1c78c73cbd4862c42ef418f03b8bfa/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f", size = 2300832, upload-time = "2026-08-28T09:57:51.639Z" }, - { url = "https://files.pythonhosted.org/packages/b4/72/dd1a29853cf6d22a1ebd9e3baf0239cbc57d2d16caff36a89e38eb9b1db3/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1", size = 2320505, upload-time = "2026-08-28T09:57:53.236Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d1/ba4a8e06a9ddad0b4caf69cfaeecc0fbfcec20473bd808f5127fd16491c4/pydantic_core-2.46.5-cp310-cp310-win32.whl", hash = "sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069", size = 1956853, upload-time = "2026-08-28T09:57:54.592Z" }, - { url = "https://files.pythonhosted.org/packages/f2/94/205ed9d7ddaf44acd489889708ea124a3f41bdb42c141c8684d528ad0e7a/pydantic_core-2.46.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d", size = 2042551, upload-time = "2026-08-28T09:57:56.017Z" }, - { url = "https://files.pythonhosted.org/packages/a2/b6/81d2d19ea0be2c03664381b59f65fa72fc7969decedae00bc2c4ad835708/pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f", size = 2074737, upload-time = "2026-08-28T09:57:57.711Z" }, - { url = "https://files.pythonhosted.org/packages/0c/18/b70da8300e292df4099684ea11b1958043580d2f50d2dc8bf7e542bdd84a/pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f", size = 1921751, upload-time = "2026-08-28T09:57:59.265Z" }, - { url = "https://files.pythonhosted.org/packages/e7/1a/0d590341b6ffa4b4aca83508e6b8db4761aaeacfc15a25ca3815876d4797/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061", size = 1948231, upload-time = "2026-08-28T09:58:00.678Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1d/02eb35761c51f2f7b1b042d6ab4cda6600f0c8c88a2243b3f734376201e5/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be", size = 2020708, upload-time = "2026-08-28T09:58:02.267Z" }, - { url = "https://files.pythonhosted.org/packages/4a/ea/f86073830e35d508cc8ddf9c3d9e6e6840fcb88d34bf726b0b4710186f27/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a", size = 2194914, upload-time = "2026-08-28T09:58:03.934Z" }, - { url = "https://files.pythonhosted.org/packages/bb/d7/fc36240d7791ce90939e51608568c33bfdae26202016f9770c229a487d86/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b", size = 2235622, upload-time = "2026-08-28T09:58:05.516Z" }, - { url = "https://files.pythonhosted.org/packages/cf/bc/3fa2d76b83162820a17da7f645b28d1cba99fc8e1e5fc6517067ec450fa1/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c", size = 2062091, upload-time = "2026-08-28T09:58:07.135Z" }, - { url = "https://files.pythonhosted.org/packages/ab/9a/095d557bb492c90cd8a70a6dd048bf793d433d03d86c81c11e912e4cd049/pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee", size = 2089904, upload-time = "2026-08-28T09:58:08.814Z" }, - { url = "https://files.pythonhosted.org/packages/24/98/7b76b1ad10a19a617a52aaa1d80e159115af939b095e86f8e756fd52e0df/pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e", size = 2132244, upload-time = "2026-08-28T09:58:10.435Z" }, - { url = "https://files.pythonhosted.org/packages/20/32/7d6ca365fadba186a0c8f85de1a701663bce81efd309d9479be58687622f/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2", size = 2143901, upload-time = "2026-08-28T09:58:12.033Z" }, - { url = "https://files.pythonhosted.org/packages/f8/09/eb9a6aa57f22fd1541a9c0aa2a1f3aeef3ec65347d33e10a6da2f43e0ee9/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689", size = 2299425, upload-time = "2026-08-28T09:58:13.614Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f9/548a5bb9d4ba8cd26e26daf48052236f6b38bb61e7b7241fbc3c995719eb/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec", size = 2318566, upload-time = "2026-08-28T09:58:15.199Z" }, - { url = "https://files.pythonhosted.org/packages/4a/20/06454d18834c02c406c9133f1a3b485305fd9ee984f9636c2f730bef6a9d/pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129", size = 1954258, upload-time = "2026-08-28T09:58:16.813Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c2/718b9deb4b72453b5d8c7447a3b14cb77bef36917ef5f514e0948a4096a0/pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c", size = 2041030, upload-time = "2026-08-28T09:58:18.288Z" }, - { url = "https://files.pythonhosted.org/packages/67/ea/c1d1a5b72d6e1ff7f377a4d9199f6591f095beb5b409a8a5d89f7238d939/pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8", size = 2009234, upload-time = "2026-08-28T09:58:19.929Z" }, - { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" }, - { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" }, - { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, - { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, - { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, - { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, - { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, - { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, - { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, - { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" }, - { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" }, - { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" }, - { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, - { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, - { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, - { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, - { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, - { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, - { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, - { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, - { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, - { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, - { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, - { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, - { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, - { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, - { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, - { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, - { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, - { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, - { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, - { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, - { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, - { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, - { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, - { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, - { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, - { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, - { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, - { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, - { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, - { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, - { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, - { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, - { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, - { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, - { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, - { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, - { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, - { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, - { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, - { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, - { url = "https://files.pythonhosted.org/packages/af/1e/ecca01fce348f7e8afa9572441ff6f7d1cc70d21e4859f33944d10877e1e/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2", size = 2075342, upload-time = "2026-08-28T10:00:51.353Z" }, - { url = "https://files.pythonhosted.org/packages/1f/4c/af80c7a8032dfc897040ad5cb772bebde529a381186499e6e29987f23f8c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c", size = 1907219, upload-time = "2026-08-28T10:00:53.438Z" }, - { url = "https://files.pythonhosted.org/packages/be/3e/54d89e2b092e778716bf6153634ef479e955f48c261090be23aa1e0fb0b5/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47", size = 1953393, upload-time = "2026-08-28T10:00:55.58Z" }, - { url = "https://files.pythonhosted.org/packages/ea/89/828ee90cda28ce17bdefaa3a6eaf74fe430e113295a10e6126beca559d6c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a", size = 2099024, upload-time = "2026-08-28T10:00:57.794Z" }, - { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, - { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, - { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, - { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, - { url = "https://files.pythonhosted.org/packages/20/21/22102e9950b3049526d20e811b95396508377d87651edd2b80d2b3d28659/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f", size = 2071333, upload-time = "2026-08-28T10:01:09.636Z" }, - { url = "https://files.pythonhosted.org/packages/d8/18/87aefa427d191e6d3ab1447f1efc1cdcac86af1069239b133e8a0fd7f7c9/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0", size = 1912713, upload-time = "2026-08-28T10:01:12.285Z" }, - { url = "https://files.pythonhosted.org/packages/1f/93/fd89e9ad49b1805ca94d24ce1088b7d305f05c35ffafcedb9819d03588a0/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4", size = 2090926, upload-time = "2026-08-28T10:01:15.19Z" }, - { url = "https://files.pythonhosted.org/packages/6f/45/8e59dab6acf8d35f02f0a958980074f31038968bdb2c983fcae9d1efee03/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25", size = 2131303, upload-time = "2026-08-28T10:01:17.937Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a5/e1d4dc5180dd887a9522efc1f8716b8692b7606b1d3273d7862eaf66be44/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6", size = 2145128, upload-time = "2026-08-28T10:01:20.694Z" }, - { url = "https://files.pythonhosted.org/packages/c2/d7/ad493864a7fb21c0c4df98f965e2db430cb25a9d7369b5778d5016c09fd9/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e", size = 2294560, upload-time = "2026-08-28T10:01:23.495Z" }, - { url = "https://files.pythonhosted.org/packages/02/8e/b41c84c913f29973a268e6c2b5bbf13c95adb9956c126d10da11ba3b2bef/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda", size = 2317531, upload-time = "2026-08-28T10:01:26.334Z" }, - { url = "https://files.pythonhosted.org/packages/db/1d/068464f23075f66a8f1b806935e9cd9363ee446636ea70d2c22ee8659dbf/pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266", size = 2140686, upload-time = "2026-08-28T10:01:28.947Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/6b/8f79692844269427abb3e4dd9e68edfcbe65ae25527d99183214de716c59/pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6", size = 2076533 }, + { url = "https://files.pythonhosted.org/packages/bd/d0/c787604c71c2bdcda1a5656942fc822cd0f9cd879b9484bb84fc42172703/pydantic_core-2.46.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615", size = 1924650 }, + { url = "https://files.pythonhosted.org/packages/4a/77/ca2f8e997d9bfdb32205297aff38f210f398822d895b1af1b59fd9df9c13/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb", size = 1951261 }, + { url = "https://files.pythonhosted.org/packages/a0/53/bd12e1a9255df4edee00353778e2614b5346265d51e1567ab72153e803a2/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b", size = 2021808 }, + { url = "https://files.pythonhosted.org/packages/d7/41/f7f312751ebc6d6767da91964a9c7954c18e226a1720ab234e3dfb9d6c17/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6", size = 2196184 }, + { url = "https://files.pythonhosted.org/packages/3d/93/ce93aa030ab6bac4683ba8861e7baad89dd24b02e66b8801a0e4f6a00311/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793", size = 2238212 }, + { url = "https://files.pythonhosted.org/packages/34/a1/c8e6b66f499f510752c07a092dfe27621f9c255635e59d38704b5681c35a/pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b", size = 2064073 }, + { url = "https://files.pythonhosted.org/packages/5c/fa/605e2b127ee30dbf4b1da9da4843587cf2b2d16486c241cc7a5be2d2c1bd/pydantic_core-2.46.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461", size = 2093102 }, + { url = "https://files.pythonhosted.org/packages/4a/f7/1ab28093c09032ddce7c92c7a55d503b6ecd70f42c32492946c1cb5477b1/pydantic_core-2.46.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736", size = 2133452 }, + { url = "https://files.pythonhosted.org/packages/30/c8/47c79b756f12f85e8b0fbdb2b495f6b6eb32e6c98a4beae7a570a0b7c63c/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3", size = 2146477 }, + { url = "https://files.pythonhosted.org/packages/13/5c/79fc00cb8f651d6061991de8d7cedf1c78c73cbd4862c42ef418f03b8bfa/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f", size = 2300832 }, + { url = "https://files.pythonhosted.org/packages/b4/72/dd1a29853cf6d22a1ebd9e3baf0239cbc57d2d16caff36a89e38eb9b1db3/pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1", size = 2320505 }, + { url = "https://files.pythonhosted.org/packages/ec/d1/ba4a8e06a9ddad0b4caf69cfaeecc0fbfcec20473bd808f5127fd16491c4/pydantic_core-2.46.5-cp310-cp310-win32.whl", hash = "sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069", size = 1956853 }, + { url = "https://files.pythonhosted.org/packages/f2/94/205ed9d7ddaf44acd489889708ea124a3f41bdb42c141c8684d528ad0e7a/pydantic_core-2.46.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d", size = 2042551 }, + { url = "https://files.pythonhosted.org/packages/a2/b6/81d2d19ea0be2c03664381b59f65fa72fc7969decedae00bc2c4ad835708/pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f", size = 2074737 }, + { url = "https://files.pythonhosted.org/packages/0c/18/b70da8300e292df4099684ea11b1958043580d2f50d2dc8bf7e542bdd84a/pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f", size = 1921751 }, + { url = "https://files.pythonhosted.org/packages/e7/1a/0d590341b6ffa4b4aca83508e6b8db4761aaeacfc15a25ca3815876d4797/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061", size = 1948231 }, + { url = "https://files.pythonhosted.org/packages/7d/1d/02eb35761c51f2f7b1b042d6ab4cda6600f0c8c88a2243b3f734376201e5/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be", size = 2020708 }, + { url = "https://files.pythonhosted.org/packages/4a/ea/f86073830e35d508cc8ddf9c3d9e6e6840fcb88d34bf726b0b4710186f27/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a", size = 2194914 }, + { url = "https://files.pythonhosted.org/packages/bb/d7/fc36240d7791ce90939e51608568c33bfdae26202016f9770c229a487d86/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b", size = 2235622 }, + { url = "https://files.pythonhosted.org/packages/cf/bc/3fa2d76b83162820a17da7f645b28d1cba99fc8e1e5fc6517067ec450fa1/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c", size = 2062091 }, + { url = "https://files.pythonhosted.org/packages/ab/9a/095d557bb492c90cd8a70a6dd048bf793d433d03d86c81c11e912e4cd049/pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee", size = 2089904 }, + { url = "https://files.pythonhosted.org/packages/24/98/7b76b1ad10a19a617a52aaa1d80e159115af939b095e86f8e756fd52e0df/pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e", size = 2132244 }, + { url = "https://files.pythonhosted.org/packages/20/32/7d6ca365fadba186a0c8f85de1a701663bce81efd309d9479be58687622f/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2", size = 2143901 }, + { url = "https://files.pythonhosted.org/packages/f8/09/eb9a6aa57f22fd1541a9c0aa2a1f3aeef3ec65347d33e10a6da2f43e0ee9/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689", size = 2299425 }, + { url = "https://files.pythonhosted.org/packages/8a/f9/548a5bb9d4ba8cd26e26daf48052236f6b38bb61e7b7241fbc3c995719eb/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec", size = 2318566 }, + { url = "https://files.pythonhosted.org/packages/4a/20/06454d18834c02c406c9133f1a3b485305fd9ee984f9636c2f730bef6a9d/pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129", size = 1954258 }, + { url = "https://files.pythonhosted.org/packages/9e/c2/718b9deb4b72453b5d8c7447a3b14cb77bef36917ef5f514e0948a4096a0/pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c", size = 2041030 }, + { url = "https://files.pythonhosted.org/packages/67/ea/c1d1a5b72d6e1ff7f377a4d9199f6591f095beb5b409a8a5d89f7238d939/pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8", size = 2009234 }, + { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516 }, + { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874 }, + { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772 }, + { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832 }, + { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645 }, + { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935 }, + { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284 }, + { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889 }, + { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006 }, + { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408 }, + { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609 }, + { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618 }, + { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475 }, + { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140 }, + { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729 }, + { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885 }, + { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768 }, + { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241 }, + { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975 }, + { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542 }, + { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692 }, + { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633 }, + { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235 }, + { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367 }, + { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420 }, + { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588 }, + { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866 }, + { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580 }, + { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980 }, + { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213 }, + { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081 }, + { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497 }, + { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130 }, + { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371 }, + { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822 }, + { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756 }, + { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352 }, + { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777 }, + { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312 }, + { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067 }, + { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516 }, + { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223 }, + { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827 }, + { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648 }, + { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652 }, + { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829 }, + { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716 }, + { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216 }, + { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635 }, + { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369 }, + { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238 }, + { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740 }, + { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425 }, + { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306 }, + { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589 }, + { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882 }, + { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210 }, + { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180 }, + { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515 }, + { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276 }, + { url = "https://files.pythonhosted.org/packages/af/1e/ecca01fce348f7e8afa9572441ff6f7d1cc70d21e4859f33944d10877e1e/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2", size = 2075342 }, + { url = "https://files.pythonhosted.org/packages/1f/4c/af80c7a8032dfc897040ad5cb772bebde529a381186499e6e29987f23f8c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c", size = 1907219 }, + { url = "https://files.pythonhosted.org/packages/be/3e/54d89e2b092e778716bf6153634ef479e955f48c261090be23aa1e0fb0b5/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47", size = 1953393 }, + { url = "https://files.pythonhosted.org/packages/ea/89/828ee90cda28ce17bdefaa3a6eaf74fe430e113295a10e6126beca559d6c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a", size = 2099024 }, + { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540 }, + { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040 }, + { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479 }, + { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034 }, + { url = "https://files.pythonhosted.org/packages/20/21/22102e9950b3049526d20e811b95396508377d87651edd2b80d2b3d28659/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f", size = 2071333 }, + { url = "https://files.pythonhosted.org/packages/d8/18/87aefa427d191e6d3ab1447f1efc1cdcac86af1069239b133e8a0fd7f7c9/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0", size = 1912713 }, + { url = "https://files.pythonhosted.org/packages/1f/93/fd89e9ad49b1805ca94d24ce1088b7d305f05c35ffafcedb9819d03588a0/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4", size = 2090926 }, + { url = "https://files.pythonhosted.org/packages/6f/45/8e59dab6acf8d35f02f0a958980074f31038968bdb2c983fcae9d1efee03/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25", size = 2131303 }, + { url = "https://files.pythonhosted.org/packages/d5/a5/e1d4dc5180dd887a9522efc1f8716b8692b7606b1d3273d7862eaf66be44/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6", size = 2145128 }, + { url = "https://files.pythonhosted.org/packages/c2/d7/ad493864a7fb21c0c4df98f965e2db430cb25a9d7369b5778d5016c09fd9/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e", size = 2294560 }, + { url = "https://files.pythonhosted.org/packages/02/8e/b41c84c913f29973a268e6c2b5bbf13c95adb9956c126d10da11ba3b2bef/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda", size = 2317531 }, + { url = "https://files.pythonhosted.org/packages/db/1d/068464f23075f66a8f1b806935e9cd9363ee446636ea70d2c22ee8659dbf/pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266", size = 2140686 }, ] [[package]] @@ -1977,13 +1968,13 @@ name = "pydantic-settings" version = "2.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, + { name = "pydantic", marker = "python_full_version < '3.11'" }, + { name = "python-dotenv", marker = "python_full_version < '3.11'" }, + { name = "typing-inspection", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253 } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" }, + { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413 }, ] [[package]] @@ -1991,35 +1982,35 @@ name = "pydivsufsort" version = "0.0.18" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, - { name = "wheel" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ce/aa/a4168a2077288071dc9183768e14fb35893606231eebbf1b156663c95df2/pydivsufsort-0.0.18.tar.gz", hash = "sha256:e23ff06abbd8e34e812c66a38cb6c853f55a3f15fccaa001e339752f34865e77", size = 360071, upload-time = "2024-09-26T01:04:37.053Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/74/ffb311f8d5f157d5b2048d84ac3a3a5127001a7bb94f759b6f69520ab819/pydivsufsort-0.0.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:40474ee2f071fb16b33082596b35638c2c17eb37d9c0efced8b31394d6e0cacd", size = 397218, upload-time = "2024-09-26T00:19:44.161Z" }, - { url = "https://files.pythonhosted.org/packages/0a/26/0daff9c93f1543e1ebfbeeb10ee6b3bd21694af8fa72b060789b303c18de/pydivsufsort-0.0.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37c660abe0f126111e8e251391a8efc88dcbb726342e18ac230c0ba79f81c1c2", size = 355600, upload-time = "2024-09-26T00:19:45.845Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9c/2113375bef83d3b30f631fa7d5feea45fb6d22440a99e0e0f37492ecb30b/pydivsufsort-0.0.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bb76f97f1a6143b0c54b1fcf771977b0a0c8f16b85dcde295ddce20a0f7535a", size = 2822811, upload-time = "2024-09-26T01:04:39.469Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ea/13f8177a13bccd61b9305b13462a0163d43d10d40d93fec2e526d648f94c/pydivsufsort-0.0.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a370a2807833f8dc53f3a457ba5ad247ae948a4aa629ec6d3edc44a62e8546bd", size = 2866051, upload-time = "2024-09-26T01:04:41.25Z" }, - { url = "https://files.pythonhosted.org/packages/c1/14/ef9e23be0e3b6b8babb67161e4bd13bd6eb39a1154b01a12a159af3bff27/pydivsufsort-0.0.18-cp310-cp310-win32.whl", hash = "sha256:19a6c06e73f89402e3cf3c882c19b847536be9168e2bab102053be8e74439e5e", size = 293799, upload-time = "2024-09-26T00:26:21.18Z" }, - { url = "https://files.pythonhosted.org/packages/ce/7c/4aded88173aa7026d02b24253394a3722aa38f7c7b40d0e6fa0a6de10df6/pydivsufsort-0.0.18-cp310-cp310-win_amd64.whl", hash = "sha256:2f7f013edaa94fa07891619977dcf0a363fa4b7b0d00ed953c4bb58d8c5232ab", size = 351862, upload-time = "2024-09-26T00:21:45.916Z" }, - { url = "https://files.pythonhosted.org/packages/f2/f1/34a9648719ea1a5697ad45731bad4f3f7e06872695b4bb8966271455bde8/pydivsufsort-0.0.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2cd51b1c5cfe2ab88a7628d4f26f5a5d843bf563aa9865cdaca5ad364fbd59f3", size = 397797, upload-time = "2024-09-26T00:19:47.423Z" }, - { url = "https://files.pythonhosted.org/packages/72/f1/2c40c95bbe78059d632fd59cc75e40b82f03d33c7868fc27972ec2e5d1c3/pydivsufsort-0.0.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c301ba012b185a0ad0dcbafa6d405b94b3b3d2c9b04d686633bb8d436ef08a2", size = 355392, upload-time = "2024-09-26T00:19:48.754Z" }, - { url = "https://files.pythonhosted.org/packages/8a/23/99f62a0f90653d70ad122bcbe304c8b7362587f1f2c59745230d0853d0f8/pydivsufsort-0.0.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a862ae27f527c5fadcf181bc486901d080a28af21e764d36519ade205c2b4af7", size = 2832814, upload-time = "2024-09-26T01:04:43.026Z" }, - { url = "https://files.pythonhosted.org/packages/ed/e0/6b3b61780fccb02a5d8a74a37768f44c3fab9a3dd866928ebfb165327435/pydivsufsort-0.0.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce5731dfea69dadc17b91420251a5a060c6ab21e166ec2bb2d3fe3ad9cb80a3f", size = 2848992, upload-time = "2024-09-26T01:04:44.757Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ee/9630fd2b36e0c966ab4e528d8c9b5ef5c9c8f9ff492b1b17e43d3198f1ed/pydivsufsort-0.0.18-cp311-cp311-win32.whl", hash = "sha256:63b01c2cf6c8d91f0820f153332fa911be564bd2e54cff510cd5a7d3d4a124e8", size = 293765, upload-time = "2024-09-26T00:26:22.559Z" }, - { url = "https://files.pythonhosted.org/packages/9a/3e/9d49200f8ddaaebd5e126cd923af516de6c000b18c57962bd3afb1c4626b/pydivsufsort-0.0.18-cp311-cp311-win_amd64.whl", hash = "sha256:acd3bfe375706f4cba391340e6ad947fc470447de777acd34b1d3a448eb07977", size = 354257, upload-time = "2024-09-26T00:21:47.037Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ba/765da9cf9d053a1c815dca6ce34f9d47f61725264fe7689d32208e7106a5/pydivsufsort-0.0.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31ec5e6f8aa64500c81b27e6c750d06412a1fc7c506413923f67e8d9241492d0", size = 399274, upload-time = "2024-09-26T00:19:49.872Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b1/4a626bb8aced9aee973eaebb1bac814c53d81983cee54eaa1c549af18b21/pydivsufsort-0.0.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:754c665aab78793b329b7d94ed82e03b50d0166f733e26236eeb0410dd328520", size = 359151, upload-time = "2024-09-26T00:19:50.932Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ae/6acb3a8a4c239e2cf5029631a597920217cf1f279fe1066003dfb8cd90c2/pydivsufsort-0.0.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a5e7437408ab94c4829a39a3d1dd826de4ebfde4ddc5a9accf4718519d1e453", size = 2831689, upload-time = "2024-09-26T01:04:46.274Z" }, - { url = "https://files.pythonhosted.org/packages/b0/dc/8afbc81320a5185cc76e07fc4102f171854db44fc499c39751dbb36fabb1/pydivsufsort-0.0.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87364bf7d70876dc7f8d92c61c2c3fe025be520a666b9714d808ecd9eb42ec99", size = 2909957, upload-time = "2024-09-26T01:04:48.19Z" }, - { url = "https://files.pythonhosted.org/packages/2f/70/e921d50c766356a929b1c663d608916fc4c015698c03c81e90db51242f84/pydivsufsort-0.0.18-cp312-cp312-win32.whl", hash = "sha256:c0d8ccab13628e2d09e1fe314bc95746817de78b902d5a5fc0da445d5427c1d2", size = 284952, upload-time = "2024-09-26T00:26:25.109Z" }, - { url = "https://files.pythonhosted.org/packages/01/99/b93c3318c9a08888c81e427e05e9b3a31010299988e15999b07d76465189/pydivsufsort-0.0.18-cp312-cp312-win_amd64.whl", hash = "sha256:5f7678c020b24eda861ec9771b7239b24551c17bb21a55a350ecafc36b733780", size = 344554, upload-time = "2024-09-26T00:21:48.056Z" }, - { url = "https://files.pythonhosted.org/packages/34/e4/e8348caf813eaffc0619356453290c8b3f27298b16d59f0bd2b2b62b1de6/pydivsufsort-0.0.18-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8951fa16d4ac757409ec69d7edca867719b1b25462602f351c95e67f218a5cb5", size = 395851, upload-time = "2024-09-26T00:19:52.068Z" }, - { url = "https://files.pythonhosted.org/packages/91/e1/2673fa17b7e20f32207ea492f433363760a1d4c960cf241857a17beb4b88/pydivsufsort-0.0.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:702970e4b8601d7a2f0e46deaea4313913ec0aa145c949aca35cfe3d6b7727cd", size = 356797, upload-time = "2024-09-26T00:19:53.196Z" }, - { url = "https://files.pythonhosted.org/packages/f3/2a/d19203b926d3b1a13ce9dd580a1708abaa82ff0a3b3ead697c37389534fe/pydivsufsort-0.0.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f05a12a412030de8a6912966a0e492eae90e8cfec79060e7211ef39fd1813f8", size = 2816932, upload-time = "2024-09-26T01:04:50.002Z" }, - { url = "https://files.pythonhosted.org/packages/19/34/e733fbf731ac384116a5cfa6c7370aa86ab5cade43ff0ea335caabd13980/pydivsufsort-0.0.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:907383c28615d2d563f76fd2f75dc978fccc1f9150fd82fb2d9f0e44ff8dae59", size = 2881565, upload-time = "2024-09-26T01:04:51.508Z" }, - { url = "https://files.pythonhosted.org/packages/57/21/d4b721f3219f12e518d0e5b665f0fee69cd2161b474535babb6464783eb9/pydivsufsort-0.0.18-cp313-cp313-win32.whl", hash = "sha256:3d0db2bdca91c8f4645d2e6a9e5515001dea19223afef15adde5f752b74e95ff", size = 282957, upload-time = "2024-09-26T00:26:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/9e/79/65908503eeb34c44cc0dd70f14b6c59809af9f9696b397e8a6a76b785a91/pydivsufsort-0.0.18-cp313-cp313-win_amd64.whl", hash = "sha256:3341a023a6a0d27b0c944f48ee9e1f980e3bbfdca7f1cd9494cfd400ff8e631a", size = 346477, upload-time = "2024-09-26T00:21:49.549Z" }, + { name = "numpy", marker = "python_full_version < '3.12'" }, + { name = "wheel", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/aa/a4168a2077288071dc9183768e14fb35893606231eebbf1b156663c95df2/pydivsufsort-0.0.18.tar.gz", hash = "sha256:e23ff06abbd8e34e812c66a38cb6c853f55a3f15fccaa001e339752f34865e77", size = 360071 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/74/ffb311f8d5f157d5b2048d84ac3a3a5127001a7bb94f759b6f69520ab819/pydivsufsort-0.0.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:40474ee2f071fb16b33082596b35638c2c17eb37d9c0efced8b31394d6e0cacd", size = 397218 }, + { url = "https://files.pythonhosted.org/packages/0a/26/0daff9c93f1543e1ebfbeeb10ee6b3bd21694af8fa72b060789b303c18de/pydivsufsort-0.0.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37c660abe0f126111e8e251391a8efc88dcbb726342e18ac230c0ba79f81c1c2", size = 355600 }, + { url = "https://files.pythonhosted.org/packages/ff/9c/2113375bef83d3b30f631fa7d5feea45fb6d22440a99e0e0f37492ecb30b/pydivsufsort-0.0.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bb76f97f1a6143b0c54b1fcf771977b0a0c8f16b85dcde295ddce20a0f7535a", size = 2822811 }, + { url = "https://files.pythonhosted.org/packages/ab/ea/13f8177a13bccd61b9305b13462a0163d43d10d40d93fec2e526d648f94c/pydivsufsort-0.0.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a370a2807833f8dc53f3a457ba5ad247ae948a4aa629ec6d3edc44a62e8546bd", size = 2866051 }, + { url = "https://files.pythonhosted.org/packages/c1/14/ef9e23be0e3b6b8babb67161e4bd13bd6eb39a1154b01a12a159af3bff27/pydivsufsort-0.0.18-cp310-cp310-win32.whl", hash = "sha256:19a6c06e73f89402e3cf3c882c19b847536be9168e2bab102053be8e74439e5e", size = 293799 }, + { url = "https://files.pythonhosted.org/packages/ce/7c/4aded88173aa7026d02b24253394a3722aa38f7c7b40d0e6fa0a6de10df6/pydivsufsort-0.0.18-cp310-cp310-win_amd64.whl", hash = "sha256:2f7f013edaa94fa07891619977dcf0a363fa4b7b0d00ed953c4bb58d8c5232ab", size = 351862 }, + { url = "https://files.pythonhosted.org/packages/f2/f1/34a9648719ea1a5697ad45731bad4f3f7e06872695b4bb8966271455bde8/pydivsufsort-0.0.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2cd51b1c5cfe2ab88a7628d4f26f5a5d843bf563aa9865cdaca5ad364fbd59f3", size = 397797 }, + { url = "https://files.pythonhosted.org/packages/72/f1/2c40c95bbe78059d632fd59cc75e40b82f03d33c7868fc27972ec2e5d1c3/pydivsufsort-0.0.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c301ba012b185a0ad0dcbafa6d405b94b3b3d2c9b04d686633bb8d436ef08a2", size = 355392 }, + { url = "https://files.pythonhosted.org/packages/8a/23/99f62a0f90653d70ad122bcbe304c8b7362587f1f2c59745230d0853d0f8/pydivsufsort-0.0.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a862ae27f527c5fadcf181bc486901d080a28af21e764d36519ade205c2b4af7", size = 2832814 }, + { url = "https://files.pythonhosted.org/packages/ed/e0/6b3b61780fccb02a5d8a74a37768f44c3fab9a3dd866928ebfb165327435/pydivsufsort-0.0.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce5731dfea69dadc17b91420251a5a060c6ab21e166ec2bb2d3fe3ad9cb80a3f", size = 2848992 }, + { url = "https://files.pythonhosted.org/packages/ed/ee/9630fd2b36e0c966ab4e528d8c9b5ef5c9c8f9ff492b1b17e43d3198f1ed/pydivsufsort-0.0.18-cp311-cp311-win32.whl", hash = "sha256:63b01c2cf6c8d91f0820f153332fa911be564bd2e54cff510cd5a7d3d4a124e8", size = 293765 }, + { url = "https://files.pythonhosted.org/packages/9a/3e/9d49200f8ddaaebd5e126cd923af516de6c000b18c57962bd3afb1c4626b/pydivsufsort-0.0.18-cp311-cp311-win_amd64.whl", hash = "sha256:acd3bfe375706f4cba391340e6ad947fc470447de777acd34b1d3a448eb07977", size = 354257 }, + { url = "https://files.pythonhosted.org/packages/5e/ba/765da9cf9d053a1c815dca6ce34f9d47f61725264fe7689d32208e7106a5/pydivsufsort-0.0.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31ec5e6f8aa64500c81b27e6c750d06412a1fc7c506413923f67e8d9241492d0", size = 399274 }, + { url = "https://files.pythonhosted.org/packages/9b/b1/4a626bb8aced9aee973eaebb1bac814c53d81983cee54eaa1c549af18b21/pydivsufsort-0.0.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:754c665aab78793b329b7d94ed82e03b50d0166f733e26236eeb0410dd328520", size = 359151 }, + { url = "https://files.pythonhosted.org/packages/a4/ae/6acb3a8a4c239e2cf5029631a597920217cf1f279fe1066003dfb8cd90c2/pydivsufsort-0.0.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a5e7437408ab94c4829a39a3d1dd826de4ebfde4ddc5a9accf4718519d1e453", size = 2831689 }, + { url = "https://files.pythonhosted.org/packages/b0/dc/8afbc81320a5185cc76e07fc4102f171854db44fc499c39751dbb36fabb1/pydivsufsort-0.0.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87364bf7d70876dc7f8d92c61c2c3fe025be520a666b9714d808ecd9eb42ec99", size = 2909957 }, + { url = "https://files.pythonhosted.org/packages/2f/70/e921d50c766356a929b1c663d608916fc4c015698c03c81e90db51242f84/pydivsufsort-0.0.18-cp312-cp312-win32.whl", hash = "sha256:c0d8ccab13628e2d09e1fe314bc95746817de78b902d5a5fc0da445d5427c1d2", size = 284952 }, + { url = "https://files.pythonhosted.org/packages/01/99/b93c3318c9a08888c81e427e05e9b3a31010299988e15999b07d76465189/pydivsufsort-0.0.18-cp312-cp312-win_amd64.whl", hash = "sha256:5f7678c020b24eda861ec9771b7239b24551c17bb21a55a350ecafc36b733780", size = 344554 }, + { url = "https://files.pythonhosted.org/packages/34/e4/e8348caf813eaffc0619356453290c8b3f27298b16d59f0bd2b2b62b1de6/pydivsufsort-0.0.18-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8951fa16d4ac757409ec69d7edca867719b1b25462602f351c95e67f218a5cb5", size = 395851 }, + { url = "https://files.pythonhosted.org/packages/91/e1/2673fa17b7e20f32207ea492f433363760a1d4c960cf241857a17beb4b88/pydivsufsort-0.0.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:702970e4b8601d7a2f0e46deaea4313913ec0aa145c949aca35cfe3d6b7727cd", size = 356797 }, + { url = "https://files.pythonhosted.org/packages/f3/2a/d19203b926d3b1a13ce9dd580a1708abaa82ff0a3b3ead697c37389534fe/pydivsufsort-0.0.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f05a12a412030de8a6912966a0e492eae90e8cfec79060e7211ef39fd1813f8", size = 2816932 }, + { url = "https://files.pythonhosted.org/packages/19/34/e733fbf731ac384116a5cfa6c7370aa86ab5cade43ff0ea335caabd13980/pydivsufsort-0.0.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:907383c28615d2d563f76fd2f75dc978fccc1f9150fd82fb2d9f0e44ff8dae59", size = 2881565 }, + { url = "https://files.pythonhosted.org/packages/57/21/d4b721f3219f12e518d0e5b665f0fee69cd2161b474535babb6464783eb9/pydivsufsort-0.0.18-cp313-cp313-win32.whl", hash = "sha256:3d0db2bdca91c8f4645d2e6a9e5515001dea19223afef15adde5f752b74e95ff", size = 282957 }, + { url = "https://files.pythonhosted.org/packages/9e/79/65908503eeb34c44cc0dd70f14b6c59809af9f9696b397e8a6a76b785a91/pydivsufsort-0.0.18-cp313-cp313-win_amd64.whl", hash = "sha256:3341a023a6a0d27b0c944f48ee9e1f980e3bbfdca7f1cd9494cfd400ff8e631a", size = 346477 }, ] [[package]] @@ -2031,14 +2022,14 @@ resolution-markers = [ "python_full_version >= '3.12' and python_full_version < '3.14'", ] dependencies = [ - { name = "appdirs" }, - { name = "biopython" }, - { name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" } }, - { name = "prettytable" }, + { name = "appdirs", marker = "python_full_version >= '3.12'" }, + { name = "biopython", marker = "python_full_version >= '3.12'" }, + { name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "prettytable", marker = "python_full_version >= '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/52/5f83c479af9468dde6bb78a18b9f0d74eeef2b539a088dcacca978719051/pydna-5.0.0.tar.gz", hash = "sha256:685fb6321f51f0503d7732b11649bbcb6bf3a254b396fc8abc1f1b8d0b65f664", size = 19322625, upload-time = "2022-12-01T13:13:09.407Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/52/5f83c479af9468dde6bb78a18b9f0d74eeef2b539a088dcacca978719051/pydna-5.0.0.tar.gz", hash = "sha256:685fb6321f51f0503d7732b11649bbcb6bf3a254b396fc8abc1f1b8d0b65f664", size = 19322625 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/6d/909f612ecc76befabcb711e49dcceae8d01c1cbec78e621ea6515fcb3607/pydna-5.0.0-py3-none-any.whl", hash = "sha256:ccf48529824a075143b8c4a2b10af64f4fe3041956aae5f5d292fb165324ac8f", size = 115065, upload-time = "2022-12-01T13:13:05.449Z" }, + { url = "https://files.pythonhosted.org/packages/2d/6d/909f612ecc76befabcb711e49dcceae8d01c1cbec78e621ea6515fcb3607/pydna-5.0.0-py3-none-any.whl", hash = "sha256:ccf48529824a075143b8c4a2b10af64f4fe3041956aae5f5d292fb165324ac8f", size = 115065 }, ] [[package]] @@ -2050,104 +2041,92 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "appdirs" }, - { name = "biopython" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" }, + { name = "appdirs", marker = "python_full_version < '3.12'" }, + { name = "biopython", marker = "python_full_version < '3.12'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "networkx", version = "3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "numpy" }, - { name = "prettytable" }, - { name = "pydivsufsort" }, - { name = "pyfiglet" }, - { name = "regex" }, - { name = "seguid" }, + { name = "numpy", marker = "python_full_version < '3.12'" }, + { name = "prettytable", marker = "python_full_version < '3.12'" }, + { name = "pydivsufsort", marker = "python_full_version < '3.12'" }, + { name = "pyfiglet", marker = "python_full_version < '3.12'" }, + { name = "regex", marker = "python_full_version < '3.12'" }, + { name = "seguid", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/52/8165f6493e214549369c42af0b075787e0a89bd6940e0093ac15b4385749/pydna-5.5.3.tar.gz", hash = "sha256:20c420e316e6a24023ff28ca49b52500d7c258ce56db609e510b54c315e2d7d4", size = 136734, upload-time = "2025-09-11T05:37:19.032Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/52/8165f6493e214549369c42af0b075787e0a89bd6940e0093ac15b4385749/pydna-5.5.3.tar.gz", hash = "sha256:20c420e316e6a24023ff28ca49b52500d7c258ce56db609e510b54c315e2d7d4", size = 136734 } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/d5/fd51af04d301974d8c06120a68e203ff2537eb05d424ca2458181dbab9c4/pydna-5.5.3-py3-none-any.whl", hash = "sha256:d4bef7c345938c474512a5466c895e7a226efd9da7090f9320f1f0de967ae291", size = 148138, upload-time = "2025-09-11T05:37:17.172Z" }, + { url = "https://files.pythonhosted.org/packages/28/d5/fd51af04d301974d8c06120a68e203ff2537eb05d424ca2458181dbab9c4/pydna-5.5.3-py3-none-any.whl", hash = "sha256:d4bef7c345938c474512a5466c895e7a226efd9da7090f9320f1f0de967ae291", size = 148138 }, ] [[package]] name = "pyfiglet" version = "0.8.post1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/02/48293654fb2e4fdeb4d927f00a380230a832744b6c9af757373a72d018d1/pyfiglet-0.8.post1.tar.gz", hash = "sha256:c6c2321755d09267b438ec7b936825a4910fec696292139e664ca8670e103639", size = 634618, upload-time = "2019-02-05T13:50:20.006Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/02/48293654fb2e4fdeb4d927f00a380230a832744b6c9af757373a72d018d1/pyfiglet-0.8.post1.tar.gz", hash = "sha256:c6c2321755d09267b438ec7b936825a4910fec696292139e664ca8670e103639", size = 634618 } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/07/fcfdd7a2872f5b348953de35acce1544dab0c1e8368dca54279b1cde5c15/pyfiglet-0.8.post1-py2.py3-none-any.whl", hash = "sha256:d555bcea17fbeaf70eaefa48bb119352487e629c9b56f30f383e2c62dd67a01c", size = 865831, upload-time = "2019-02-05T13:50:22.689Z" }, + { url = "https://files.pythonhosted.org/packages/33/07/fcfdd7a2872f5b348953de35acce1544dab0c1e8368dca54279b1cde5c15/pyfiglet-0.8.post1-py2.py3-none-any.whl", hash = "sha256:d555bcea17fbeaf70eaefa48bb119352487e629c9b56f30f383e2c62dd67a01c", size = 865831 }, ] [[package]] name = "pygments" version = "2.21.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329 } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147 }, ] [[package]] name = "pyparsing" version = "3.2.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, + { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890 }, ] [[package]] name = "pyproject-hooks" version = "1.3.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/5d/f2ddeef4a855a102aaae5e97826a0260007522ab504421b75addfdb1517c/pyproject_hooks-1.3.3.tar.gz", hash = "sha256:defda19b854fa0d3bd4f76ea4ddcba8abd7dcfcdd585a6690ade050744fc5f43", size = 21013, upload-time = "2026-09-16T08:58:03.999Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/5d/f2ddeef4a855a102aaae5e97826a0260007522ab504421b75addfdb1517c/pyproject_hooks-1.3.3.tar.gz", hash = "sha256:defda19b854fa0d3bd4f76ea4ddcba8abd7dcfcdd585a6690ade050744fc5f43", size = 21013 } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/11/044d1ae1b4ec0d7af88ee5bc91e081be1022533032b906a7bdabdbb60977/pyproject_hooks-1.3.3-py3-none-any.whl", hash = "sha256:5fc53fdac9f7bd63fbcdc868fb5f90b4784d78a53a3d3388cd738b807441a20b", size = 10724, upload-time = "2026-09-16T08:58:02.96Z" }, -] - -[[package]] -name = "pyro5" -version = "5.17" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "serpent" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b9/312055148dbd9d0c829d30e0fbc0cff9bcc6c6311bada80737879c7f5cc6/pyro5-5.17.tar.gz", hash = "sha256:cfac69638d80943aff9cc5f1466755dd0fef8aed0bb4bda41b5eb045818ce6fc", size = 268643, upload-time = "2026-06-19T23:40:52.703Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/62/878386c290088c6f614bf50e59c362e23c8aaa86de9a1d74d4ee67108736/pyro5-5.17-py3-none-any.whl", hash = "sha256:0e08866d46fe4e315fd1a6403c66e354a6bc75c5cc3d19cd3495bb292ece4eaa", size = 79715, upload-time = "2026-06-19T23:40:51.28Z" }, + { url = "https://files.pythonhosted.org/packages/85/11/044d1ae1b4ec0d7af88ee5bc91e081be1022533032b906a7bdabdbb60977/pyproject_hooks-1.3.3-py3-none-any.whl", hash = "sha256:5fc53fdac9f7bd63fbcdc868fb5f90b4784d78a53a3d3388cd738b807441a20b", size = 10724 }, ] [[package]] name = "pyrsistent" version = "0.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/3a/5031723c09068e9c8c2f0bc25c3a9245f2b1d1aea8396c787a408f2b95ca/pyrsistent-0.20.0.tar.gz", hash = "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", size = 103642, upload-time = "2023-10-25T21:06:56.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/19/c343b14061907b629b765444b6436b160e2bd4184d17d4804bbe6381f6be/pyrsistent-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce", size = 83416, upload-time = "2023-10-25T21:06:04.579Z" }, - { url = "https://files.pythonhosted.org/packages/9f/4f/8342079ea331031ef9ed57edd312a9ad283bcc8adfaf268931ae356a09a6/pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f", size = 118021, upload-time = "2023-10-25T21:06:06.953Z" }, - { url = "https://files.pythonhosted.org/packages/d7/b7/64a125c488243965b7c5118352e47c6f89df95b4ac306d31cee409153d57/pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34", size = 117747, upload-time = "2023-10-25T21:06:08.5Z" }, - { url = "https://files.pythonhosted.org/packages/fe/a5/43c67bd5f80df9e7583042398d12113263ec57f27c0607abe9d78395d18f/pyrsistent-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b", size = 114524, upload-time = "2023-10-25T21:06:10.728Z" }, - { url = "https://files.pythonhosted.org/packages/8a/98/b382a87e89ca839106d874f7bf78d226b3eedb26735eb6f751f1a3375f21/pyrsistent-0.20.0-cp310-cp310-win32.whl", hash = "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f", size = 60780, upload-time = "2023-10-25T21:06:12.14Z" }, - { url = "https://files.pythonhosted.org/packages/37/8a/23e2193f7adea6901262e3cf39c7fe18ac0c446176c0ff0e19aeb2e9681e/pyrsistent-0.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7", size = 63310, upload-time = "2023-10-25T21:06:13.598Z" }, - { url = "https://files.pythonhosted.org/packages/df/63/7544dc7d0953294882a5c587fb1b10a26e0c23d9b92281a14c2514bac1f7/pyrsistent-0.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958", size = 83481, upload-time = "2023-10-25T21:06:15.238Z" }, - { url = "https://files.pythonhosted.org/packages/ae/a0/49249bc14d71b1bf2ffe89703acfa86f2017c25cfdabcaea532b8c8a5810/pyrsistent-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8", size = 120222, upload-time = "2023-10-25T21:06:17.144Z" }, - { url = "https://files.pythonhosted.org/packages/a1/94/9808e8c9271424120289b9028a657da336ad7e43da0647f62e4f6011d19b/pyrsistent-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a", size = 120002, upload-time = "2023-10-25T21:06:18.727Z" }, - { url = "https://files.pythonhosted.org/packages/3f/f6/9ecfb78b2fc8e2540546db0fe19df1fae0f56664a5958c21ff8861b0f8da/pyrsistent-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224", size = 116850, upload-time = "2023-10-25T21:06:20.424Z" }, - { url = "https://files.pythonhosted.org/packages/83/c8/e6d28bc27a0719f8eaae660357df9757d6e9ca9be2691595721de9e8adfc/pyrsistent-0.20.0-cp311-cp311-win32.whl", hash = "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656", size = 60775, upload-time = "2023-10-25T21:06:21.815Z" }, - { url = "https://files.pythonhosted.org/packages/98/87/c6ef52ff30388f357922d08de012abdd3dc61e09311d88967bdae23ab657/pyrsistent-0.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee", size = 63306, upload-time = "2023-10-25T21:06:22.874Z" }, - { url = "https://files.pythonhosted.org/packages/15/ee/ff2ed52032ac1ce2e7ba19e79bd5b05d152ebfb77956cf08fcd6e8d760ea/pyrsistent-0.20.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e", size = 83537, upload-time = "2023-10-25T21:06:24.17Z" }, - { url = "https://files.pythonhosted.org/packages/80/f1/338d0050b24c3132bcfc79b68c3a5f54bce3d213ecef74d37e988b971d8a/pyrsistent-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e", size = 122615, upload-time = "2023-10-25T21:06:25.815Z" }, - { url = "https://files.pythonhosted.org/packages/07/3a/e56d6431b713518094fae6ff833a04a6f49ad0fbe25fb7c0dc7408e19d20/pyrsistent-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3", size = 122335, upload-time = "2023-10-25T21:06:28.631Z" }, - { url = "https://files.pythonhosted.org/packages/4a/bb/5f40a4d5e985a43b43f607250e766cdec28904682c3505eb0bd343a4b7db/pyrsistent-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", size = 118510, upload-time = "2023-10-25T21:06:30.718Z" }, - { url = "https://files.pythonhosted.org/packages/1c/13/e6a22f40f5800af116c02c28e29f15c06aa41cb2036f6a64ab124647f28b/pyrsistent-0.20.0-cp312-cp312-win32.whl", hash = "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", size = 60865, upload-time = "2023-10-25T21:06:32.742Z" }, - { url = "https://files.pythonhosted.org/packages/75/ef/2fa3b55023ec07c22682c957808f9a41836da4cd006b5f55ec76bf0fbfa6/pyrsistent-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", size = 63239, upload-time = "2023-10-25T21:06:34.035Z" }, - { url = "https://files.pythonhosted.org/packages/23/88/0acd180010aaed4987c85700b7cc17f9505f3edb4e5873e4dc67f613e338/pyrsistent-0.20.0-py3-none-any.whl", hash = "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", size = 58106, upload-time = "2023-10-25T21:06:54.387Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ce/3a/5031723c09068e9c8c2f0bc25c3a9245f2b1d1aea8396c787a408f2b95ca/pyrsistent-0.20.0.tar.gz", hash = "sha256:4c48f78f62ab596c679086084d0dd13254ae4f3d6c72a83ffdf5ebdef8f265a4", size = 103642 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/19/c343b14061907b629b765444b6436b160e2bd4184d17d4804bbe6381f6be/pyrsistent-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c3aba3e01235221e5b229a6c05f585f344734bd1ad42a8ac51493d74722bbce", size = 83416 }, + { url = "https://files.pythonhosted.org/packages/9f/4f/8342079ea331031ef9ed57edd312a9ad283bcc8adfaf268931ae356a09a6/pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1beb78af5423b879edaf23c5591ff292cf7c33979734c99aa66d5914ead880f", size = 118021 }, + { url = "https://files.pythonhosted.org/packages/d7/b7/64a125c488243965b7c5118352e47c6f89df95b4ac306d31cee409153d57/pyrsistent-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21cc459636983764e692b9eba7144cdd54fdec23ccdb1e8ba392a63666c60c34", size = 117747 }, + { url = "https://files.pythonhosted.org/packages/fe/a5/43c67bd5f80df9e7583042398d12113263ec57f27c0607abe9d78395d18f/pyrsistent-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5ac696f02b3fc01a710427585c855f65cd9c640e14f52abe52020722bb4906b", size = 114524 }, + { url = "https://files.pythonhosted.org/packages/8a/98/b382a87e89ca839106d874f7bf78d226b3eedb26735eb6f751f1a3375f21/pyrsistent-0.20.0-cp310-cp310-win32.whl", hash = "sha256:0724c506cd8b63c69c7f883cc233aac948c1ea946ea95996ad8b1380c25e1d3f", size = 60780 }, + { url = "https://files.pythonhosted.org/packages/37/8a/23e2193f7adea6901262e3cf39c7fe18ac0c446176c0ff0e19aeb2e9681e/pyrsistent-0.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:8441cf9616d642c475684d6cf2520dd24812e996ba9af15e606df5f6fd9d04a7", size = 63310 }, + { url = "https://files.pythonhosted.org/packages/df/63/7544dc7d0953294882a5c587fb1b10a26e0c23d9b92281a14c2514bac1f7/pyrsistent-0.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0f3b1bcaa1f0629c978b355a7c37acd58907390149b7311b5db1b37648eb6958", size = 83481 }, + { url = "https://files.pythonhosted.org/packages/ae/a0/49249bc14d71b1bf2ffe89703acfa86f2017c25cfdabcaea532b8c8a5810/pyrsistent-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdd7ef1ea7a491ae70d826b6cc64868de09a1d5ff9ef8d574250d0940e275b8", size = 120222 }, + { url = "https://files.pythonhosted.org/packages/a1/94/9808e8c9271424120289b9028a657da336ad7e43da0647f62e4f6011d19b/pyrsistent-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cae40a9e3ce178415040a0383f00e8d68b569e97f31928a3a8ad37e3fde6df6a", size = 120002 }, + { url = "https://files.pythonhosted.org/packages/3f/f6/9ecfb78b2fc8e2540546db0fe19df1fae0f56664a5958c21ff8861b0f8da/pyrsistent-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6288b3fa6622ad8a91e6eb759cfc48ff3089e7c17fb1d4c59a919769314af224", size = 116850 }, + { url = "https://files.pythonhosted.org/packages/83/c8/e6d28bc27a0719f8eaae660357df9757d6e9ca9be2691595721de9e8adfc/pyrsistent-0.20.0-cp311-cp311-win32.whl", hash = "sha256:7d29c23bdf6e5438c755b941cef867ec2a4a172ceb9f50553b6ed70d50dfd656", size = 60775 }, + { url = "https://files.pythonhosted.org/packages/98/87/c6ef52ff30388f357922d08de012abdd3dc61e09311d88967bdae23ab657/pyrsistent-0.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:59a89bccd615551391f3237e00006a26bcf98a4d18623a19909a2c48b8e986ee", size = 63306 }, + { url = "https://files.pythonhosted.org/packages/15/ee/ff2ed52032ac1ce2e7ba19e79bd5b05d152ebfb77956cf08fcd6e8d760ea/pyrsistent-0.20.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:09848306523a3aba463c4b49493a760e7a6ca52e4826aa100ee99d8d39b7ad1e", size = 83537 }, + { url = "https://files.pythonhosted.org/packages/80/f1/338d0050b24c3132bcfc79b68c3a5f54bce3d213ecef74d37e988b971d8a/pyrsistent-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a14798c3005ec892bbada26485c2eea3b54109cb2533713e355c806891f63c5e", size = 122615 }, + { url = "https://files.pythonhosted.org/packages/07/3a/e56d6431b713518094fae6ff833a04a6f49ad0fbe25fb7c0dc7408e19d20/pyrsistent-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b14decb628fac50db5e02ee5a35a9c0772d20277824cfe845c8a8b717c15daa3", size = 122335 }, + { url = "https://files.pythonhosted.org/packages/4a/bb/5f40a4d5e985a43b43f607250e766cdec28904682c3505eb0bd343a4b7db/pyrsistent-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e2c116cc804d9b09ce9814d17df5edf1df0c624aba3b43bc1ad90411487036d", size = 118510 }, + { url = "https://files.pythonhosted.org/packages/1c/13/e6a22f40f5800af116c02c28e29f15c06aa41cb2036f6a64ab124647f28b/pyrsistent-0.20.0-cp312-cp312-win32.whl", hash = "sha256:e78d0c7c1e99a4a45c99143900ea0546025e41bb59ebc10182e947cf1ece9174", size = 60865 }, + { url = "https://files.pythonhosted.org/packages/75/ef/2fa3b55023ec07c22682c957808f9a41836da4cd006b5f55ec76bf0fbfa6/pyrsistent-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:4021a7f963d88ccd15b523787d18ed5e5269ce57aa4037146a2377ff607ae87d", size = 63239 }, + { url = "https://files.pythonhosted.org/packages/23/88/0acd180010aaed4987c85700b7cc17f9505f3edb4e5873e4dc67f613e338/pyrsistent-0.20.0-py3-none-any.whl", hash = "sha256:c55acc4733aad6560a7f5f818466631f07efc001fd023f34a6c203f8b6df0f0b", size = 58106 }, ] [[package]] name = "pyserial" version = "3.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125, upload-time = "2020-11-23T03:59:15.045Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125 } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" }, + { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585 }, ] [[package]] @@ -2163,9 +2142,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750 }, ] [[package]] @@ -2176,9 +2155,9 @@ dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/17/139b134cb36e496a62780b2ff19ea47fd834f2d180a32e6dd9210f4a8a77/pytest_cov-6.2.0.tar.gz", hash = "sha256:9a4331e087a0f5074dc1e19fe0485a07a462b346cbb91e2ac903ec5504abce10", size = 68872, upload-time = "2025-06-11T21:55:02.68Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/17/139b134cb36e496a62780b2ff19ea47fd834f2d180a32e6dd9210f4a8a77/pytest_cov-6.2.0.tar.gz", hash = "sha256:9a4331e087a0f5074dc1e19fe0485a07a462b346cbb91e2ac903ec5504abce10", size = 68872 } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/66/a38138fbf711b2b93592dfd7303bba561f6bc05f85361a0388c105ceb727/pytest_cov-6.2.0-py3-none-any.whl", hash = "sha256:bd19301caf600ead1169db089ed0ad7b8f2b962214330a696b8c85a0b497b2ff", size = 24448, upload-time = "2025-06-11T21:55:00.938Z" }, + { url = "https://files.pythonhosted.org/packages/aa/66/a38138fbf711b2b93592dfd7303bba561f6bc05f85361a0388c105ceb727/pytest_cov-6.2.0-py3-none-any.whl", hash = "sha256:bd19301caf600ead1169db089ed0ad7b8f2b962214330a696b8c85a0b497b2ff", size = 24448 }, ] [[package]] @@ -2188,131 +2167,131 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, ] [[package]] name = "python-dotenv" version = "1.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" }, + { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780 }, ] [[package]] name = "python-json-logger" version = "4.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/25/5473e46b179f8e8b4ad3aeeb36773d1701b7770eaf5e5bc2025c7303b598/python_json_logger-4.2.0.tar.gz", hash = "sha256:e371ebe22ec01e289850102091a2b1f6fc9e655c7f1f5f29073936756c290afa", size = 18211, upload-time = "2026-08-15T11:36:38.232Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/25/5473e46b179f8e8b4ad3aeeb36773d1701b7770eaf5e5bc2025c7303b598/python_json_logger-4.2.0.tar.gz", hash = "sha256:e371ebe22ec01e289850102091a2b1f6fc9e655c7f1f5f29073936756c290afa", size = 18211 } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/55/6467fde553886cb293e41538f3a8b4e4fd4688c6df242cf982162d8367fb/python_json_logger-4.2.0-py3-none-any.whl", hash = "sha256:158a52126fcd6869e09574d2b66272666f3dc8f468c62637ef9a1fa883719cb9", size = 14988, upload-time = "2026-08-15T11:36:36.821Z" }, + { url = "https://files.pythonhosted.org/packages/dc/55/6467fde553886cb293e41538f3a8b4e4fd4688c6df242cf982162d8367fb/python_json_logger-4.2.0-py3-none-any.whl", hash = "sha256:158a52126fcd6869e09574d2b66272666f3dc8f468c62637ef9a1fa883719cb9", size = 14988 }, ] [[package]] name = "pyusb" version = "1.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d9/6e/433a5614132576289b8643fe598dd5d51b16e130fd591564be952e15bb45/pyusb-1.2.1.tar.gz", hash = "sha256:a4cc7404a203144754164b8b40994e2849fde1cfff06b08492f12fff9d9de7b9", size = 75292, upload-time = "2021-07-09T02:58:46.962Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/6e/433a5614132576289b8643fe598dd5d51b16e130fd591564be952e15bb45/pyusb-1.2.1.tar.gz", hash = "sha256:a4cc7404a203144754164b8b40994e2849fde1cfff06b08492f12fff9d9de7b9", size = 75292 } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/a8/4982498b2ab44d1fcd5c49f07ea3795eab01601dc143b009d333fcace3b9/pyusb-1.2.1-py3-none-any.whl", hash = "sha256:2b4c7cb86dbadf044dfb9d3a4ff69fd217013dbe78a792177a3feb172449ea36", size = 58439, upload-time = "2021-07-09T02:58:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/15/a8/4982498b2ab44d1fcd5c49f07ea3795eab01601dc143b009d333fcace3b9/pyusb-1.2.1-py3-none-any.whl", hash = "sha256:2b4c7cb86dbadf044dfb9d3a4ff69fd217013dbe78a792177a3feb172449ea36", size = 58439 }, ] [[package]] name = "pywin32-ctypes" version = "0.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, ] [[package]] name = "pywinpty" version = "3.0.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/ef/2d27f30c59a67be7025b2d7858c8c2d282b74d66544b2384730b82de74fd/pywinpty-3.0.5.tar.gz", hash = "sha256:61db0db063de9865adbea66db294628f8577f608d9764a4c7d3384eeacc4e81b", size = 16223484, upload-time = "2026-06-11T00:11:58.93Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/ef/2d27f30c59a67be7025b2d7858c8c2d282b74d66544b2384730b82de74fd/pywinpty-3.0.5.tar.gz", hash = "sha256:61db0db063de9865adbea66db294628f8577f608d9764a4c7d3384eeacc4e81b", size = 16223484 } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/ff/8bd91d4502b48bee0459bc912806fa512111905ab97d317e7cb1201b3542/pywinpty-3.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:b467dcad72365bc2205ed8b6e694e817d71de269d46e56cfe267dfa9d3d30e1b", size = 2092426, upload-time = "2026-06-10T23:40:23.292Z" }, - { url = "https://files.pythonhosted.org/packages/00/d2/dee13e67af2300a080dea30d4da8fdb05133a7eebb39171111f0530f7e88/pywinpty-3.0.5-cp310-cp310-win_arm64.whl", hash = "sha256:7dc4046ea8e4d7f0a16dae8dfcaeeda6df7ca3a9330444d2ba5bb96138fe0a91", size = 818083, upload-time = "2026-06-10T23:42:04.203Z" }, - { url = "https://files.pythonhosted.org/packages/b0/5c/31feb3dd82d1b33ae0bd09ca601edb993d9da1b7f0226b3336d4b4c39e1e/pywinpty-3.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:af7a8720c78776ddd6259b71dd567944f766a6cd67f8d2887fbc4973967bacda", size = 2092466, upload-time = "2026-06-10T23:44:24.453Z" }, - { url = "https://files.pythonhosted.org/packages/ee/fe/fe23e2229ffec0c10190cef5964f5c9b2dba179d23b69ae537b7ea90bcab/pywinpty-3.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:c2406f54f699eab75953fb75ce805f2ae55a33a957cd070890abd454fb4b7680", size = 818395, upload-time = "2026-06-10T23:41:56.93Z" }, - { url = "https://files.pythonhosted.org/packages/45/34/942cc95ca4e26489875aa8a95192766247a687379ec29543eebe73ec945f/pywinpty-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:d62946adf14b15b54c0b8d785f93fe18b04da23f4ad59e2e8c4612646e9abd23", size = 2090915, upload-time = "2026-06-10T23:43:14.98Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/5b9053004844139ea8bd86209c57ade12b134b2782f383a095784c8531ec/pywinpty-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:e9391c05fbfa7a992a97e831fc6849887b4014a614192e3d984a7ca59592b376", size = 815934, upload-time = "2026-06-10T23:41:42.384Z" }, - { url = "https://files.pythonhosted.org/packages/b9/f4/2a464b9893cceb3b3f416356e94fdc3e1bca9476993927e4e6d99fe95382/pywinpty-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:48db1b0ad9d0a1b81dcaaa7163a99a7808deaceb0c1b2344716dc1fc090c3c4c", size = 2090471, upload-time = "2026-06-10T23:42:11.071Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2c/a138491a0afbdb50eb79395577bd326d4b0fbde7209417d1a8087ff2493a/pywinpty-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:2c6008fb2d3774b48693b2fcb7f2cc317ade9dc581289a964ffeeaf81307c9b5", size = 815518, upload-time = "2026-06-10T23:42:02.363Z" }, - { url = "https://files.pythonhosted.org/packages/6f/15/54400049a380582acd1282665c70fcf11e0bd3713679aca78e24c3aae738/pywinpty-3.0.5-cp313-cp313t-win_amd64.whl", hash = "sha256:22ce1b780d89821cc52daf6eac0708af22d93d000ce9c7c07e37489db8594598", size = 2089920, upload-time = "2026-06-10T23:44:13.395Z" }, - { url = "https://files.pythonhosted.org/packages/94/0c/6f24f3c0799f502259b24bdf841a99ad2b0d59df5c2525b4e2a286d14be2/pywinpty-3.0.5-cp313-cp313t-win_arm64.whl", hash = "sha256:9c2919a81bc5cfb09b86fc5a002112b2de95ca4304a07413cbeeb746a1307a5c", size = 814520, upload-time = "2026-06-10T23:43:28.588Z" }, - { url = "https://files.pythonhosted.org/packages/e9/23/f3cd1b1e5fc56517f54452c49f92049e7dd9ffc8a63de22a495581f50d04/pywinpty-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:03bb3c16d691d9242267201830bcd0e64a9b663170e9042bc84b210da9de15ac", size = 2090663, upload-time = "2026-06-10T23:43:59.845Z" }, - { url = "https://files.pythonhosted.org/packages/9d/dd/96d6cbfc6d9ddab5c1c2f92c26545ae8997446a2ba7ee2024cd43c81f49b/pywinpty-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:89c5c6ef08997a3b4b277b214a35fe15cab4dd6d119f0140aa71df5b1168fdbc", size = 815700, upload-time = "2026-06-10T23:40:50.001Z" }, - { url = "https://files.pythonhosted.org/packages/30/36/d98087bce0acaa4cce7f196103cfa7be3f63ce65f52473bb3e38784ae5d9/pywinpty-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7b566165e0c5fdd6abe167a5ac8b954be6a843eb55a85946576d6bc1dea03d6d", size = 2090093, upload-time = "2026-06-10T23:40:58.933Z" }, - { url = "https://files.pythonhosted.org/packages/57/fd/fe2b0db922ba052ce3976a08f3fc05d0c05047c8b4ebb6102e832b8ef563/pywinpty-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:24366280a8aa677323da87bec729cb3ea3b35367386cece0978bdc6e4695c690", size = 814517, upload-time = "2026-06-10T23:42:34.946Z" }, + { url = "https://files.pythonhosted.org/packages/89/ff/8bd91d4502b48bee0459bc912806fa512111905ab97d317e7cb1201b3542/pywinpty-3.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:b467dcad72365bc2205ed8b6e694e817d71de269d46e56cfe267dfa9d3d30e1b", size = 2092426 }, + { url = "https://files.pythonhosted.org/packages/00/d2/dee13e67af2300a080dea30d4da8fdb05133a7eebb39171111f0530f7e88/pywinpty-3.0.5-cp310-cp310-win_arm64.whl", hash = "sha256:7dc4046ea8e4d7f0a16dae8dfcaeeda6df7ca3a9330444d2ba5bb96138fe0a91", size = 818083 }, + { url = "https://files.pythonhosted.org/packages/b0/5c/31feb3dd82d1b33ae0bd09ca601edb993d9da1b7f0226b3336d4b4c39e1e/pywinpty-3.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:af7a8720c78776ddd6259b71dd567944f766a6cd67f8d2887fbc4973967bacda", size = 2092466 }, + { url = "https://files.pythonhosted.org/packages/ee/fe/fe23e2229ffec0c10190cef5964f5c9b2dba179d23b69ae537b7ea90bcab/pywinpty-3.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:c2406f54f699eab75953fb75ce805f2ae55a33a957cd070890abd454fb4b7680", size = 818395 }, + { url = "https://files.pythonhosted.org/packages/45/34/942cc95ca4e26489875aa8a95192766247a687379ec29543eebe73ec945f/pywinpty-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:d62946adf14b15b54c0b8d785f93fe18b04da23f4ad59e2e8c4612646e9abd23", size = 2090915 }, + { url = "https://files.pythonhosted.org/packages/6a/70/5b9053004844139ea8bd86209c57ade12b134b2782f383a095784c8531ec/pywinpty-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:e9391c05fbfa7a992a97e831fc6849887b4014a614192e3d984a7ca59592b376", size = 815934 }, + { url = "https://files.pythonhosted.org/packages/b9/f4/2a464b9893cceb3b3f416356e94fdc3e1bca9476993927e4e6d99fe95382/pywinpty-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:48db1b0ad9d0a1b81dcaaa7163a99a7808deaceb0c1b2344716dc1fc090c3c4c", size = 2090471 }, + { url = "https://files.pythonhosted.org/packages/3c/2c/a138491a0afbdb50eb79395577bd326d4b0fbde7209417d1a8087ff2493a/pywinpty-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:2c6008fb2d3774b48693b2fcb7f2cc317ade9dc581289a964ffeeaf81307c9b5", size = 815518 }, + { url = "https://files.pythonhosted.org/packages/6f/15/54400049a380582acd1282665c70fcf11e0bd3713679aca78e24c3aae738/pywinpty-3.0.5-cp313-cp313t-win_amd64.whl", hash = "sha256:22ce1b780d89821cc52daf6eac0708af22d93d000ce9c7c07e37489db8594598", size = 2089920 }, + { url = "https://files.pythonhosted.org/packages/94/0c/6f24f3c0799f502259b24bdf841a99ad2b0d59df5c2525b4e2a286d14be2/pywinpty-3.0.5-cp313-cp313t-win_arm64.whl", hash = "sha256:9c2919a81bc5cfb09b86fc5a002112b2de95ca4304a07413cbeeb746a1307a5c", size = 814520 }, + { url = "https://files.pythonhosted.org/packages/e9/23/f3cd1b1e5fc56517f54452c49f92049e7dd9ffc8a63de22a495581f50d04/pywinpty-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:03bb3c16d691d9242267201830bcd0e64a9b663170e9042bc84b210da9de15ac", size = 2090663 }, + { url = "https://files.pythonhosted.org/packages/9d/dd/96d6cbfc6d9ddab5c1c2f92c26545ae8997446a2ba7ee2024cd43c81f49b/pywinpty-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:89c5c6ef08997a3b4b277b214a35fe15cab4dd6d119f0140aa71df5b1168fdbc", size = 815700 }, + { url = "https://files.pythonhosted.org/packages/30/36/d98087bce0acaa4cce7f196103cfa7be3f63ce65f52473bb3e38784ae5d9/pywinpty-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7b566165e0c5fdd6abe167a5ac8b954be6a843eb55a85946576d6bc1dea03d6d", size = 2090093 }, + { url = "https://files.pythonhosted.org/packages/57/fd/fe2b0db922ba052ce3976a08f3fc05d0c05047c8b4ebb6102e832b8ef563/pywinpty-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:24366280a8aa677323da87bec729cb3ea3b35367386cece0978bdc6e4695c690", size = 814517 }, ] [[package]] name = "pyyaml" version = "6.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227 }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019 }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646 }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793 }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293 }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872 }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828 }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415 }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561 }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826 }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577 }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556 }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114 }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638 }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463 }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986 }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543 }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763 }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063 }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973 }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011 }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870 }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089 }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181 }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658 }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003 }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344 }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, ] [[package]] @@ -2322,77 +2301,77 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "implementation_name == 'pypy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e7/8d/5b3d5631c2f4b4b8862f64cd0c9eb777b5710eeb5125b4be8dd0a200a4c0/pyzmq-27.2.0.tar.gz", hash = "sha256:54d4259d1bfae24ecdb5ca79f7acc2eac6c286a02d6a0ae617797cb45f0726d3", size = 292316, upload-time = "2026-08-20T19:08:21.19Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/de/c9d653d686ec686bac6ae5f953c784ffc4d2e4a33f9c20d0326aff549ca8/pyzmq-27.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:480dba27b145373b5e103890f17969d891bc9e86746d6b8b29dd70b0d4addc62", size = 1458528, upload-time = "2026-08-20T19:06:11.455Z" }, - { url = "https://files.pythonhosted.org/packages/13/3a/e3ae8e56fdb87cefbef655dc1d808d5ac76eacf40af850c5d78fd81a7faf/pyzmq-27.2.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:722f0a6940be1a483c81029a271d950e04dc2ff113a42e21b3d2b7a0d8e59638", size = 985810, upload-time = "2026-08-20T19:06:13.409Z" }, - { url = "https://files.pythonhosted.org/packages/03/ee/0ace0abf6315f3f481388826f1e9218802d9b5bb4a01bae5ce42018c3b4e/pyzmq-27.2.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ee556ed1cf836f96de9d5e545563116426d4a94f21b8041fdc79408eff18ebb", size = 713840, upload-time = "2026-08-20T19:06:14.85Z" }, - { url = "https://files.pythonhosted.org/packages/3b/fd/aee8c87f4854a012e232a6557b1167b6cd207506f59f378954b2d8bb6ed1/pyzmq-27.2.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d64da42cae09e6b0c61368b4cc8ca80f23ce3af17584d08053f3dc957433d5ed", size = 888214, upload-time = "2026-08-20T19:06:16.295Z" }, - { url = "https://files.pythonhosted.org/packages/94/2f/9be34eab874a26aa91315be0b27f94d43d35dad544cbdf28bcd41691afb2/pyzmq-27.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:376981d106598beb70be384f44d8f589832fd0051d184d38d10043da3cc3b080", size = 1705252, upload-time = "2026-08-20T19:06:17.89Z" }, - { url = "https://files.pythonhosted.org/packages/7f/26/362344e337d6b5905d65d0c205e91090b39b3b5a59ec9b84dbe88a5dd60c/pyzmq-27.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:40d96cb7a8f6a43aa9617c00215c2b73e1b5e4a1d6cbc9f5860ed7ac682599f0", size = 2075298, upload-time = "2026-08-20T19:06:19.27Z" }, - { url = "https://files.pythonhosted.org/packages/1d/ee/3b76b91e2bb8c5c12f8ae9ee553eb6254ba0bb048c2f0dea7d945420a0d3/pyzmq-27.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4ebc7889b31bc11c72e9f17ba3ebb0a8b0911cce413f41b498e55383a94819a3", size = 1926812, upload-time = "2026-08-20T19:06:20.621Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ae/83d0740f125a6ef91d5ed29e560679e4c5beeaa3ab1fc87e5fe1128c296d/pyzmq-27.2.0-cp310-cp310-win32.whl", hash = "sha256:650c6cd7cb39a069e7048261efe66fce8bf2e0052c831a7a099b7a0f2ea860d7", size = 571057, upload-time = "2026-08-20T19:06:22.078Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b3/3e99a7af1c25e84f68c6c7b16c51b664dd51e12d0bc3d252a48d298e9a08/pyzmq-27.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:82a09aa67871d4f2fcafd47bf670fb93210b232a7c2d4b8a54676314edf04033", size = 643232, upload-time = "2026-08-20T19:06:23.431Z" }, - { url = "https://files.pythonhosted.org/packages/06/cb/785d002a08e630807141b102fd2740cc11623c366bb4606465450bd12e03/pyzmq-27.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bad4813f270592cedf56977e31ac1fc374fb0f6f67ea5134a5e37c19cb429a8e", size = 567590, upload-time = "2026-08-20T19:06:24.664Z" }, - { url = "https://files.pythonhosted.org/packages/1d/2e/8897afa4538707d86645f51cc50e66b2b84900edb1be9dc9af2c2fc04e5d/pyzmq-27.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:9216132843d139a123f243c07fe70f7487dce5041093dd77040f9adb5dc91872", size = 1457109, upload-time = "2026-08-20T19:06:26.022Z" }, - { url = "https://files.pythonhosted.org/packages/d1/bc/dbce7bc1654fa25b1e68b9bad9e547906f581ce919c186a88ed951cb794c/pyzmq-27.2.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d41ebb260b69329b7d4a2936d44c872c86dd785355b51366c8b14e07ed7e9373", size = 985701, upload-time = "2026-08-20T19:06:27.481Z" }, - { url = "https://files.pythonhosted.org/packages/95/cf/6981738b57c83fef33f356141ad83bf51e92f2f70c9d5767affd1a699f07/pyzmq-27.2.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:468139ddb2e494d06e586bd3a6835077e8b3764560c8db552fe685c5867fc24e", size = 714285, upload-time = "2026-08-20T19:06:28.962Z" }, - { url = "https://files.pythonhosted.org/packages/50/b5/13657961a845e29c28a4e7ac4202999ec90b3bba1890a5469ce2ae90359d/pyzmq-27.2.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39755dc4a923021bd0677990ffdbc21cff0e1ee1cf07fe3817acea153ef4cb67", size = 888465, upload-time = "2026-08-20T19:06:30.4Z" }, - { url = "https://files.pythonhosted.org/packages/58/5a/ca7ee7a767413d4ba858e93748b95e30b35b8c139849fba94de4433ea2e5/pyzmq-27.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:714f8cbd66c7e405338d668f79d2fe83fe923defe348e843be998603cf92eeff", size = 1705731, upload-time = "2026-08-20T19:06:31.819Z" }, - { url = "https://files.pythonhosted.org/packages/0b/8b/083f6184e4eba566c9a3cc9974b1b0fe327b7093788135ba8133edaa67a6/pyzmq-27.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1132805970045adb9f5f05dd57040978286a8e21a5475f2c2ddf1bc983b9a2c7", size = 2074243, upload-time = "2026-08-20T19:06:33.36Z" }, - { url = "https://files.pythonhosted.org/packages/57/f5/249362b664ae725d534c8843214fa9fd7fccd74532a19e24603954a88a7d/pyzmq-27.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b26f2d0493b79ce3c3112c8a12649418915582ba4707b8ed9f44febf2be71f42", size = 1926859, upload-time = "2026-08-20T19:06:34.796Z" }, - { url = "https://files.pythonhosted.org/packages/bf/cc/23c613c15f06d879f13364d14c17e5e4e8304049411e96c1410e6e56c3ea/pyzmq-27.2.0-cp311-cp311-win32.whl", hash = "sha256:44f261eca7dfb9904ea2b56428f59ab693bbe2715c0413a701f17b067ebf877c", size = 570747, upload-time = "2026-08-20T19:06:36.337Z" }, - { url = "https://files.pythonhosted.org/packages/dc/bc/bbbcf89003c93f18e33665c26e3c48d75e3915c3dd22887f3a7aea2c5e26/pyzmq-27.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:8b86e04f55af0f4d8cd8ecf14c0b8b81ebc8fd66fa20126b753514628ecadc7e", size = 644845, upload-time = "2026-08-20T19:06:37.711Z" }, - { url = "https://files.pythonhosted.org/packages/14/c5/4635d0ba2b8493edf6d5541fff0b07fa1d986fdfc29c596a53a21e20f9af/pyzmq-27.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:917d601e9540098f580d2723d0ce6402cdb6f02bc8dc2de74e0dca6e13bffd1b", size = 567495, upload-time = "2026-08-20T19:06:39.246Z" }, - { url = "https://files.pythonhosted.org/packages/57/8a/153532fa53db30e116118164f3af269a1f3966b3e2ba32c89b12fe864bd8/pyzmq-27.2.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:591c8de5851c5ea372194469fe97587b97c3b641e9a70f31bb3474acbfde0241", size = 1431074, upload-time = "2026-08-20T19:06:40.601Z" }, - { url = "https://files.pythonhosted.org/packages/c8/ef/c08b91248bb90a9efa81fa00ba81b69c157c74d0c5efbb2c319d91babb62/pyzmq-27.2.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:00e73942ef12cecbc7951c4a9104bb8ffaed742abb13af2da6833d90dd368cef", size = 973915, upload-time = "2026-08-20T19:06:42.037Z" }, - { url = "https://files.pythonhosted.org/packages/b4/78/a3a3a86c2b00fadb92ece1ca4f8f028d62b2ce9ac3526097239ab2d6fba9/pyzmq-27.2.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f8079d0521fe94bbb401fe9407578b28f3701627c8be2c9f7e0c5b77dcb0109", size = 697722, upload-time = "2026-08-20T19:06:43.325Z" }, - { url = "https://files.pythonhosted.org/packages/62/2c/d5828306f795e8d34676d266823b74e2101e0ad3760d12083de3e02abbb2/pyzmq-27.2.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dea74fd65f1fc5f7fe167916a473ebe6ed6174e5e5d9de11ea6583661be6cf43", size = 872258, upload-time = "2026-08-20T19:06:44.627Z" }, - { url = "https://files.pythonhosted.org/packages/09/52/51253b78fd8739293e283407eeecb14215c02c71b6519af21f6eed8e69cd/pyzmq-27.2.0-cp312-abi3-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcc99ca132b667a4ed750afd42db4ea73288f18425a9b2e3c0af095665c491f5", size = 739591, upload-time = "2026-08-20T19:06:46.214Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3e/142c85b67a4c9678629b0cf6d5125b29663d75be69bfaa57a3cac344d780/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b8d5f66e4a8246cf77f7b8f7902af64f00553368fa0373c89d99b78f0ad79394", size = 1689031, upload-time = "2026-08-20T19:06:47.612Z" }, - { url = "https://files.pythonhosted.org/packages/0e/ee/0776fb0f98ed1eb74d77240087fef0ab045b6ad15cb09555c6c5134c98ad/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:d1526b42a2e725b84ed226f37becedc250c6347594e5ed304e4e9aff68c9aec3", size = 2059547, upload-time = "2026-08-20T19:06:49.064Z" }, - { url = "https://files.pythonhosted.org/packages/aa/0e/ec77f691a4aebe29ab6329f996fb0e0270c876a3016086e3ca6ef733bcae/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f707bcf2c1d007d14d70531d4dd7b41060881c73efa845580bf6faaf9ea24d42", size = 1910457, upload-time = "2026-08-20T19:06:50.783Z" }, - { url = "https://files.pythonhosted.org/packages/30/97/1f5530ff4fc271b4597048371d5af972c2baab51be132ba15874e0327a6a/pyzmq-27.2.0-cp312-abi3-win32.whl", hash = "sha256:fdaaa4ea3242f6ad298eb5177eb042aea5c73c30e76d20caee7b15af20d24ec2", size = 563450, upload-time = "2026-08-20T19:06:52.307Z" }, - { url = "https://files.pythonhosted.org/packages/02/8b/b83f7780dad22e0878e4c7bd9158ebd24ed12bc3d5e3a471cd0576f77ded/pyzmq-27.2.0-cp312-abi3-win_amd64.whl", hash = "sha256:2c218c6ab8bc447ba62054b581fd30209689d199c6ecb253f79615ca74a38e12", size = 628633, upload-time = "2026-08-20T19:06:53.809Z" }, - { url = "https://files.pythonhosted.org/packages/52/aa/3918b5ac7f9987bd9c421b065074fd7409ded88f856f2c704a24341877ec/pyzmq-27.2.0-cp312-abi3-win_arm64.whl", hash = "sha256:348d6fd3e4b81ae4580622ea8c2ea60224e84b2ac1b3be4482e6edc7de06e7a3", size = 556006, upload-time = "2026-08-20T19:06:55.242Z" }, - { url = "https://files.pythonhosted.org/packages/83/5e/d0541596b48c5a19f85dcbea83d6673d8e91681cdf853eb194c31fc9766e/pyzmq-27.2.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:c551b9e2f86dc625fcb1a032c0d68042678caf96a8dd7c28796766b673bd5b52", size = 1127193, upload-time = "2026-08-20T19:06:56.545Z" }, - { url = "https://files.pythonhosted.org/packages/50/9f/8c7411bb283982d46e6d56dca6a095678c87eb0398daead12776d9881ac2/pyzmq-27.2.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:288cc790da0e3064a14a38ddc56ba169dada8c8af4cb86518db2bcbd380eedbb", size = 1166833, upload-time = "2026-08-20T19:06:58.011Z" }, - { url = "https://files.pythonhosted.org/packages/f9/84/a849161ff88b2de9b991cc8ab332218824741122fdc4fdf222a5b822ac8c/pyzmq-27.2.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:3d45189c0c3c99f817b7fefff0d32eeef684cf33e1e3c0fc4281515357c54702", size = 1134452, upload-time = "2026-08-20T19:06:59.898Z" }, - { url = "https://files.pythonhosted.org/packages/3c/34/ff4aaff0cfba2a4d7ad1a16ffedc52c6deb89fcf673d455085446b23f215/pyzmq-27.2.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d61910b52be5b2cd8b248dbcbe3a1b0275556a7d99fb613fc43323b546e273b8", size = 1167520, upload-time = "2026-08-20T19:07:01.283Z" }, - { url = "https://files.pythonhosted.org/packages/b6/07/42111e9dc1041d78b4443d6eb1b82b027f1a58178dc8a38385effbc72ad5/pyzmq-27.2.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:3ab6eb88590e510ab16715c32dbba12000da9bee989fdadd9ee19a234c492eb7", size = 1466289, upload-time = "2026-08-20T19:07:02.738Z" }, - { url = "https://files.pythonhosted.org/packages/4b/b4/def7a478458da78665840564161772e7e938600c32a89f28e8b221b54d2d/pyzmq-27.2.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1ecbdd131b9669f62d3a45afee5527c7ae9f141e4301267f21714c90bd21725f", size = 975868, upload-time = "2026-08-20T19:07:04.155Z" }, - { url = "https://files.pythonhosted.org/packages/38/d5/e3e85f7fea37153097aaff49db9e33093909cc2a7b22c1ac4ebe546600fc/pyzmq-27.2.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3146385b94a760236c5eceff468a66a296a716ca98a2e0f9217b1518118466b1", size = 706054, upload-time = "2026-08-20T19:07:05.623Z" }, - { url = "https://files.pythonhosted.org/packages/1c/ef/3b7d9449b223183222bf517245e1e53d5f1ab8c10be8b45f6a301b2f994a/pyzmq-27.2.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9846e881620dd62566ca76a53e384c3f37490faf4b9240aebc7498810dfca853", size = 878984, upload-time = "2026-08-20T19:07:07.153Z" }, - { url = "https://files.pythonhosted.org/packages/be/a5/8b49dbd494f6dcfda69dc4cade322a4b02706ef4e3d30cc366d4e369899f/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d9527e3dbaef1edaeeb2446fa7379446814a43ade8adc7c4a5ebe69437815ddd", size = 1697489, upload-time = "2026-08-20T19:07:08.945Z" }, - { url = "https://files.pythonhosted.org/packages/da/5a/4bb8280901130c26ea25f0cbb4a6d39d94250860c6b3dbd912f1cf48fca7/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:56b48fa9d478a3af7254f397697a62f5ad3e1bb677e200b2701f0c290d97e5af", size = 2064236, upload-time = "2026-08-20T19:07:10.384Z" }, - { url = "https://files.pythonhosted.org/packages/de/38/f433af66922554adb2b5f79e897018c8e19a90b9eaeb49c4814f8355ebe4/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bf0b6e4ce1bb089751c504c5493d6b0557eabd02dd21b76e9086cf964234b103", size = 1917424, upload-time = "2026-08-20T19:07:11.909Z" }, - { url = "https://files.pythonhosted.org/packages/36/81/ea1c1ae3f801d96ba2c269e056761ebcfe023476e651d3af2a7817962051/pyzmq-27.2.0-cp314-cp314t-win32.whl", hash = "sha256:fba8afcf265c6e9fbe1594cb045d4765c6c9a7d607653a8196067ef23566b843", size = 591103, upload-time = "2026-08-20T19:07:13.451Z" }, - { url = "https://files.pythonhosted.org/packages/8a/04/149a627707e780fa9f2c1ede3590c14fa6b18b5576d15744342622299a50/pyzmq-27.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d1bc1d380a91d954ed5fc9f12915dba014eed0978d2de05ee7ca688bdaac144a", size = 670215, upload-time = "2026-08-20T19:07:15.069Z" }, - { url = "https://files.pythonhosted.org/packages/30/ba/f9c3c1536c41ef3dbf765ea04218990e2056e558f98184ecd883767fc501/pyzmq-27.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c7cfb75caa83f5153c687e9d2107f64b5ef0ef0d6edd260d3ff920baaaa69101", size = 582252, upload-time = "2026-08-20T19:07:16.582Z" }, - { url = "https://files.pythonhosted.org/packages/fa/00/78fe097a304a408275747ce43f20428789130b059c5649956277c20f30cf/pyzmq-27.2.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:c5129a8fe43ecc49b99eb75616603d483a3c2fcaef504988fafe8ea392aea98b", size = 1134295, upload-time = "2026-08-20T19:07:17.94Z" }, - { url = "https://files.pythonhosted.org/packages/f2/83/1c36270658d2ee56e23a3f9ef5fbcb94cbd2f9fe966a6641f2f38e697162/pyzmq-27.2.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:baa2ce3485145653194d6c8c5beedd1e9f0bf46a0919c9fa2fe2204fc35b74d9", size = 1167492, upload-time = "2026-08-20T19:07:19.476Z" }, - { url = "https://files.pythonhosted.org/packages/58/b2/f0ae223438d7faa991f6feefdc823815f11cc604f898738376b59fd96515/pyzmq-27.2.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:e1ed46048d1920cabc96d952a0d5cfe4127ad8db572c335aae4e3c57b9278d7f", size = 1465992, upload-time = "2026-08-20T19:07:20.941Z" }, - { url = "https://files.pythonhosted.org/packages/59/46/fb56f3f37a6a0937b0e1d2885e808b5eedc171320bac85573cfae78fa9bc/pyzmq-27.2.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e0fa0bc6b1a184aee59b32efcd1b7f0e6d5b8f9387799e4c16a4cb66a86747d6", size = 976118, upload-time = "2026-08-20T19:07:22.577Z" }, - { url = "https://files.pythonhosted.org/packages/21/82/a2c9bfd7c4d34eea1278493cd041bc000d41acb4463c89ceaad29dc813b6/pyzmq-27.2.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4bd6743e8bf854c3bfce892dd6578a514aabf128e37a4b2eafcf01856f7e44", size = 705968, upload-time = "2026-08-20T19:07:24.019Z" }, - { url = "https://files.pythonhosted.org/packages/d6/12/b906b269116b6591dc15c0acc5d04c043957c8a531d336999731f4b1d899/pyzmq-27.2.0-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95369ed6626afcfe2ac89832fb1b917c077fbeb905fbbe5d918349ce0222b89b", size = 879011, upload-time = "2026-08-20T19:07:25.428Z" }, - { url = "https://files.pythonhosted.org/packages/12/13/f96359534bfb77651c15f1fbfc4bfdd7ec3489d23f434706d39598dd0dcd/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:40124779c3a56ad5d91902df1ff89159cb414b6c1a0ee697abcc66cf5e6db62d", size = 1697496, upload-time = "2026-08-20T19:07:26.821Z" }, - { url = "https://files.pythonhosted.org/packages/21/b4/2c007ae5f2fe5eca86cbfbc874ed86b5135f2f7812615dfd78606d3c93f6/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:ec8a318dfc27c7d946651b3d9e8025d5734f30c168a822195601827207bac09b", size = 2064347, upload-time = "2026-08-20T19:07:28.315Z" }, - { url = "https://files.pythonhosted.org/packages/9b/88/767af3a6630c15215f3a66700ec79598a375edd1fdc9d75a3ad522178c01/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:88c0fac061bac269076edeb3a209acefc96cd6167c239daf1c2b404ac48d7012", size = 1917360, upload-time = "2026-08-20T19:07:29.693Z" }, - { url = "https://files.pythonhosted.org/packages/35/c1/80dd2d20d6e57bc68e1dce1e84bf3e76c9577c1bf728199985c8b4ea0fd1/pyzmq-27.2.0-cp315-cp315t-win32.whl", hash = "sha256:ac126d48cf18aa955daabef43bf0009ff76ad4deee437d09ecf15388214b5beb", size = 591073, upload-time = "2026-08-20T19:07:31.341Z" }, - { url = "https://files.pythonhosted.org/packages/f8/b5/33b781666f3f52ae834bc9c8e38f4f0483a826c5a91cccc993292007bf10/pyzmq-27.2.0-cp315-cp315t-win_amd64.whl", hash = "sha256:edce90a1e588ec63adbf612cc0ad582de4169cd216c7ae53c15f42a2ee902f35", size = 670701, upload-time = "2026-08-20T19:07:32.895Z" }, - { url = "https://files.pythonhosted.org/packages/6e/97/bc4f0edefb992df4fdebcf9f0cc40f631cd4ed277e1ed59ef2cd99a5c8c5/pyzmq-27.2.0-cp315-cp315t-win_arm64.whl", hash = "sha256:a843094b4d3d633bc3623e47a2ff50742d6af02bc1f7606aa2e67e971e21878d", size = 581985, upload-time = "2026-08-20T19:07:34.19Z" }, - { url = "https://files.pythonhosted.org/packages/8c/26/1a7cd2d8e4e3c27d83a46960e22101b69f527843be140cb3375267aa8ca6/pyzmq-27.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:770a37f28ddfbe1d2c40a2e3ce37e5fd10831daa6ae9634105aa8a5d23507b00", size = 875015, upload-time = "2026-08-20T19:07:52.084Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ed/c8daf770ca31eb293bef40f801f05148e0a96ad538d55ec3bceb226c91ec/pyzmq-27.2.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b398c5fe102b41e1559f7ffdae760aabd5f432d73b047b4ae0eac4e01cb594d2", size = 774400, upload-time = "2026-08-20T19:07:53.596Z" }, - { url = "https://files.pythonhosted.org/packages/64/6f/b958b0785eb15ab7a78913e3df8de705dbf226ce1d0004998fa4872bdd59/pyzmq-27.2.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0e1af01858d6dc0c09cea57f9cb1ddf4601f04897b6bb1efc3a2038123c87d79", size = 877993, upload-time = "2026-08-20T19:07:55.192Z" }, - { url = "https://files.pythonhosted.org/packages/2d/5a/7a070d0e9911441061402013f0c64e5772ee781704c5dae093e6b051c6f6/pyzmq-27.2.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:211350c3ccd4746bc5a85e8fe961bad1f7f2f274f67cf1f785fad7f96f562eea", size = 614493, upload-time = "2026-08-20T19:07:56.77Z" }, - { url = "https://files.pythonhosted.org/packages/b3/28/833485224e1bd8960cbf3539db465ab2bf23b50de885dc4225a637480539/pyzmq-27.2.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dde5e291548ca0f397623b5e523db5c90172b32aa4fd3ba464a79ea31a580b43", size = 780573, upload-time = "2026-08-20T19:07:58.18Z" }, - { url = "https://files.pythonhosted.org/packages/90/8e/52239b9b5fe4f9cf77e272e6a8c548c01ba407d08d3aeeb3220816669ef1/pyzmq-27.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:94242bd4de6af7e74665e14a88630bccd615057f6acfaf08a3a432551d604645", size = 553963, upload-time = "2026-08-20T19:08:00.015Z" }, - { url = "https://files.pythonhosted.org/packages/93/22/7187a1f0bf2b8bf8dc6b91762438fb9b472f684f2dc4cb74a24bf8957943/pyzmq-27.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a7c1144dc61777938e932a2c9011b980b89fd8ff3733033b34c44c299187a6e1", size = 875015, upload-time = "2026-08-20T19:08:01.692Z" }, - { url = "https://files.pythonhosted.org/packages/92/71/09b71620ad52bad4eb68b1516978ecaf52ef623c3fa16e0732a03cf3274c/pyzmq-27.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:c218b816220d05acf6ab1bafca58926d95cbcc5fec5024724666030466308f0c", size = 774395, upload-time = "2026-08-20T19:08:03.108Z" }, - { url = "https://files.pythonhosted.org/packages/9c/cf/5c8eb9994a14ff5ee5b0cada339421748746c95aee0280c8b656741e8749/pyzmq-27.2.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ae6ebbc0bfe5a21ce21e32ba567bf73df2d93888109c65acbd42506cf9395759", size = 877994, upload-time = "2026-08-20T19:08:04.724Z" }, - { url = "https://files.pythonhosted.org/packages/97/64/e22094c5555e550b6450ecfcceb6a1205d893d9a18ae27764c8c45acfb16/pyzmq-27.2.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:679b5b1dde326a921ea2c9ec1f9ea3115bfe1b4735779bbc6eb0473a0ed93f71", size = 614492, upload-time = "2026-08-20T19:08:06.487Z" }, - { url = "https://files.pythonhosted.org/packages/d2/28/5b1042899caed18278c56d54a502f5254d463afe8aea1acbecc98e053391/pyzmq-27.2.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5c6d8744d10b5e1eadd90a7c58f8546acf6bf680ee463f7e6ada09ad6c9f802", size = 780574, upload-time = "2026-08-20T19:08:07.987Z" }, - { url = "https://files.pythonhosted.org/packages/77/a3/f134603a671c114c6b56eb912bba890f09e2d43b8a28d243be5a5507cf2f/pyzmq-27.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3ee8dd7031d5e23f632e0e7eee67183ca7d2536e0de35dc1e5d69f3471a791e8", size = 553961, upload-time = "2026-08-20T19:08:09.651Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e7/8d/5b3d5631c2f4b4b8862f64cd0c9eb777b5710eeb5125b4be8dd0a200a4c0/pyzmq-27.2.0.tar.gz", hash = "sha256:54d4259d1bfae24ecdb5ca79f7acc2eac6c286a02d6a0ae617797cb45f0726d3", size = 292316 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/de/c9d653d686ec686bac6ae5f953c784ffc4d2e4a33f9c20d0326aff549ca8/pyzmq-27.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:480dba27b145373b5e103890f17969d891bc9e86746d6b8b29dd70b0d4addc62", size = 1458528 }, + { url = "https://files.pythonhosted.org/packages/13/3a/e3ae8e56fdb87cefbef655dc1d808d5ac76eacf40af850c5d78fd81a7faf/pyzmq-27.2.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:722f0a6940be1a483c81029a271d950e04dc2ff113a42e21b3d2b7a0d8e59638", size = 985810 }, + { url = "https://files.pythonhosted.org/packages/03/ee/0ace0abf6315f3f481388826f1e9218802d9b5bb4a01bae5ce42018c3b4e/pyzmq-27.2.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ee556ed1cf836f96de9d5e545563116426d4a94f21b8041fdc79408eff18ebb", size = 713840 }, + { url = "https://files.pythonhosted.org/packages/3b/fd/aee8c87f4854a012e232a6557b1167b6cd207506f59f378954b2d8bb6ed1/pyzmq-27.2.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d64da42cae09e6b0c61368b4cc8ca80f23ce3af17584d08053f3dc957433d5ed", size = 888214 }, + { url = "https://files.pythonhosted.org/packages/94/2f/9be34eab874a26aa91315be0b27f94d43d35dad544cbdf28bcd41691afb2/pyzmq-27.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:376981d106598beb70be384f44d8f589832fd0051d184d38d10043da3cc3b080", size = 1705252 }, + { url = "https://files.pythonhosted.org/packages/7f/26/362344e337d6b5905d65d0c205e91090b39b3b5a59ec9b84dbe88a5dd60c/pyzmq-27.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:40d96cb7a8f6a43aa9617c00215c2b73e1b5e4a1d6cbc9f5860ed7ac682599f0", size = 2075298 }, + { url = "https://files.pythonhosted.org/packages/1d/ee/3b76b91e2bb8c5c12f8ae9ee553eb6254ba0bb048c2f0dea7d945420a0d3/pyzmq-27.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4ebc7889b31bc11c72e9f17ba3ebb0a8b0911cce413f41b498e55383a94819a3", size = 1926812 }, + { url = "https://files.pythonhosted.org/packages/9e/ae/83d0740f125a6ef91d5ed29e560679e4c5beeaa3ab1fc87e5fe1128c296d/pyzmq-27.2.0-cp310-cp310-win32.whl", hash = "sha256:650c6cd7cb39a069e7048261efe66fce8bf2e0052c831a7a099b7a0f2ea860d7", size = 571057 }, + { url = "https://files.pythonhosted.org/packages/ce/b3/3e99a7af1c25e84f68c6c7b16c51b664dd51e12d0bc3d252a48d298e9a08/pyzmq-27.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:82a09aa67871d4f2fcafd47bf670fb93210b232a7c2d4b8a54676314edf04033", size = 643232 }, + { url = "https://files.pythonhosted.org/packages/06/cb/785d002a08e630807141b102fd2740cc11623c366bb4606465450bd12e03/pyzmq-27.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bad4813f270592cedf56977e31ac1fc374fb0f6f67ea5134a5e37c19cb429a8e", size = 567590 }, + { url = "https://files.pythonhosted.org/packages/1d/2e/8897afa4538707d86645f51cc50e66b2b84900edb1be9dc9af2c2fc04e5d/pyzmq-27.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:9216132843d139a123f243c07fe70f7487dce5041093dd77040f9adb5dc91872", size = 1457109 }, + { url = "https://files.pythonhosted.org/packages/d1/bc/dbce7bc1654fa25b1e68b9bad9e547906f581ce919c186a88ed951cb794c/pyzmq-27.2.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d41ebb260b69329b7d4a2936d44c872c86dd785355b51366c8b14e07ed7e9373", size = 985701 }, + { url = "https://files.pythonhosted.org/packages/95/cf/6981738b57c83fef33f356141ad83bf51e92f2f70c9d5767affd1a699f07/pyzmq-27.2.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:468139ddb2e494d06e586bd3a6835077e8b3764560c8db552fe685c5867fc24e", size = 714285 }, + { url = "https://files.pythonhosted.org/packages/50/b5/13657961a845e29c28a4e7ac4202999ec90b3bba1890a5469ce2ae90359d/pyzmq-27.2.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39755dc4a923021bd0677990ffdbc21cff0e1ee1cf07fe3817acea153ef4cb67", size = 888465 }, + { url = "https://files.pythonhosted.org/packages/58/5a/ca7ee7a767413d4ba858e93748b95e30b35b8c139849fba94de4433ea2e5/pyzmq-27.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:714f8cbd66c7e405338d668f79d2fe83fe923defe348e843be998603cf92eeff", size = 1705731 }, + { url = "https://files.pythonhosted.org/packages/0b/8b/083f6184e4eba566c9a3cc9974b1b0fe327b7093788135ba8133edaa67a6/pyzmq-27.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1132805970045adb9f5f05dd57040978286a8e21a5475f2c2ddf1bc983b9a2c7", size = 2074243 }, + { url = "https://files.pythonhosted.org/packages/57/f5/249362b664ae725d534c8843214fa9fd7fccd74532a19e24603954a88a7d/pyzmq-27.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b26f2d0493b79ce3c3112c8a12649418915582ba4707b8ed9f44febf2be71f42", size = 1926859 }, + { url = "https://files.pythonhosted.org/packages/bf/cc/23c613c15f06d879f13364d14c17e5e4e8304049411e96c1410e6e56c3ea/pyzmq-27.2.0-cp311-cp311-win32.whl", hash = "sha256:44f261eca7dfb9904ea2b56428f59ab693bbe2715c0413a701f17b067ebf877c", size = 570747 }, + { url = "https://files.pythonhosted.org/packages/dc/bc/bbbcf89003c93f18e33665c26e3c48d75e3915c3dd22887f3a7aea2c5e26/pyzmq-27.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:8b86e04f55af0f4d8cd8ecf14c0b8b81ebc8fd66fa20126b753514628ecadc7e", size = 644845 }, + { url = "https://files.pythonhosted.org/packages/14/c5/4635d0ba2b8493edf6d5541fff0b07fa1d986fdfc29c596a53a21e20f9af/pyzmq-27.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:917d601e9540098f580d2723d0ce6402cdb6f02bc8dc2de74e0dca6e13bffd1b", size = 567495 }, + { url = "https://files.pythonhosted.org/packages/57/8a/153532fa53db30e116118164f3af269a1f3966b3e2ba32c89b12fe864bd8/pyzmq-27.2.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:591c8de5851c5ea372194469fe97587b97c3b641e9a70f31bb3474acbfde0241", size = 1431074 }, + { url = "https://files.pythonhosted.org/packages/c8/ef/c08b91248bb90a9efa81fa00ba81b69c157c74d0c5efbb2c319d91babb62/pyzmq-27.2.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:00e73942ef12cecbc7951c4a9104bb8ffaed742abb13af2da6833d90dd368cef", size = 973915 }, + { url = "https://files.pythonhosted.org/packages/b4/78/a3a3a86c2b00fadb92ece1ca4f8f028d62b2ce9ac3526097239ab2d6fba9/pyzmq-27.2.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f8079d0521fe94bbb401fe9407578b28f3701627c8be2c9f7e0c5b77dcb0109", size = 697722 }, + { url = "https://files.pythonhosted.org/packages/62/2c/d5828306f795e8d34676d266823b74e2101e0ad3760d12083de3e02abbb2/pyzmq-27.2.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dea74fd65f1fc5f7fe167916a473ebe6ed6174e5e5d9de11ea6583661be6cf43", size = 872258 }, + { url = "https://files.pythonhosted.org/packages/09/52/51253b78fd8739293e283407eeecb14215c02c71b6519af21f6eed8e69cd/pyzmq-27.2.0-cp312-abi3-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcc99ca132b667a4ed750afd42db4ea73288f18425a9b2e3c0af095665c491f5", size = 739591 }, + { url = "https://files.pythonhosted.org/packages/e6/3e/142c85b67a4c9678629b0cf6d5125b29663d75be69bfaa57a3cac344d780/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b8d5f66e4a8246cf77f7b8f7902af64f00553368fa0373c89d99b78f0ad79394", size = 1689031 }, + { url = "https://files.pythonhosted.org/packages/0e/ee/0776fb0f98ed1eb74d77240087fef0ab045b6ad15cb09555c6c5134c98ad/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:d1526b42a2e725b84ed226f37becedc250c6347594e5ed304e4e9aff68c9aec3", size = 2059547 }, + { url = "https://files.pythonhosted.org/packages/aa/0e/ec77f691a4aebe29ab6329f996fb0e0270c876a3016086e3ca6ef733bcae/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f707bcf2c1d007d14d70531d4dd7b41060881c73efa845580bf6faaf9ea24d42", size = 1910457 }, + { url = "https://files.pythonhosted.org/packages/30/97/1f5530ff4fc271b4597048371d5af972c2baab51be132ba15874e0327a6a/pyzmq-27.2.0-cp312-abi3-win32.whl", hash = "sha256:fdaaa4ea3242f6ad298eb5177eb042aea5c73c30e76d20caee7b15af20d24ec2", size = 563450 }, + { url = "https://files.pythonhosted.org/packages/02/8b/b83f7780dad22e0878e4c7bd9158ebd24ed12bc3d5e3a471cd0576f77ded/pyzmq-27.2.0-cp312-abi3-win_amd64.whl", hash = "sha256:2c218c6ab8bc447ba62054b581fd30209689d199c6ecb253f79615ca74a38e12", size = 628633 }, + { url = "https://files.pythonhosted.org/packages/52/aa/3918b5ac7f9987bd9c421b065074fd7409ded88f856f2c704a24341877ec/pyzmq-27.2.0-cp312-abi3-win_arm64.whl", hash = "sha256:348d6fd3e4b81ae4580622ea8c2ea60224e84b2ac1b3be4482e6edc7de06e7a3", size = 556006 }, + { url = "https://files.pythonhosted.org/packages/83/5e/d0541596b48c5a19f85dcbea83d6673d8e91681cdf853eb194c31fc9766e/pyzmq-27.2.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:c551b9e2f86dc625fcb1a032c0d68042678caf96a8dd7c28796766b673bd5b52", size = 1127193 }, + { url = "https://files.pythonhosted.org/packages/50/9f/8c7411bb283982d46e6d56dca6a095678c87eb0398daead12776d9881ac2/pyzmq-27.2.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:288cc790da0e3064a14a38ddc56ba169dada8c8af4cb86518db2bcbd380eedbb", size = 1166833 }, + { url = "https://files.pythonhosted.org/packages/f9/84/a849161ff88b2de9b991cc8ab332218824741122fdc4fdf222a5b822ac8c/pyzmq-27.2.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:3d45189c0c3c99f817b7fefff0d32eeef684cf33e1e3c0fc4281515357c54702", size = 1134452 }, + { url = "https://files.pythonhosted.org/packages/3c/34/ff4aaff0cfba2a4d7ad1a16ffedc52c6deb89fcf673d455085446b23f215/pyzmq-27.2.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d61910b52be5b2cd8b248dbcbe3a1b0275556a7d99fb613fc43323b546e273b8", size = 1167520 }, + { url = "https://files.pythonhosted.org/packages/b6/07/42111e9dc1041d78b4443d6eb1b82b027f1a58178dc8a38385effbc72ad5/pyzmq-27.2.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:3ab6eb88590e510ab16715c32dbba12000da9bee989fdadd9ee19a234c492eb7", size = 1466289 }, + { url = "https://files.pythonhosted.org/packages/4b/b4/def7a478458da78665840564161772e7e938600c32a89f28e8b221b54d2d/pyzmq-27.2.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1ecbdd131b9669f62d3a45afee5527c7ae9f141e4301267f21714c90bd21725f", size = 975868 }, + { url = "https://files.pythonhosted.org/packages/38/d5/e3e85f7fea37153097aaff49db9e33093909cc2a7b22c1ac4ebe546600fc/pyzmq-27.2.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3146385b94a760236c5eceff468a66a296a716ca98a2e0f9217b1518118466b1", size = 706054 }, + { url = "https://files.pythonhosted.org/packages/1c/ef/3b7d9449b223183222bf517245e1e53d5f1ab8c10be8b45f6a301b2f994a/pyzmq-27.2.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9846e881620dd62566ca76a53e384c3f37490faf4b9240aebc7498810dfca853", size = 878984 }, + { url = "https://files.pythonhosted.org/packages/be/a5/8b49dbd494f6dcfda69dc4cade322a4b02706ef4e3d30cc366d4e369899f/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d9527e3dbaef1edaeeb2446fa7379446814a43ade8adc7c4a5ebe69437815ddd", size = 1697489 }, + { url = "https://files.pythonhosted.org/packages/da/5a/4bb8280901130c26ea25f0cbb4a6d39d94250860c6b3dbd912f1cf48fca7/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:56b48fa9d478a3af7254f397697a62f5ad3e1bb677e200b2701f0c290d97e5af", size = 2064236 }, + { url = "https://files.pythonhosted.org/packages/de/38/f433af66922554adb2b5f79e897018c8e19a90b9eaeb49c4814f8355ebe4/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bf0b6e4ce1bb089751c504c5493d6b0557eabd02dd21b76e9086cf964234b103", size = 1917424 }, + { url = "https://files.pythonhosted.org/packages/36/81/ea1c1ae3f801d96ba2c269e056761ebcfe023476e651d3af2a7817962051/pyzmq-27.2.0-cp314-cp314t-win32.whl", hash = "sha256:fba8afcf265c6e9fbe1594cb045d4765c6c9a7d607653a8196067ef23566b843", size = 591103 }, + { url = "https://files.pythonhosted.org/packages/8a/04/149a627707e780fa9f2c1ede3590c14fa6b18b5576d15744342622299a50/pyzmq-27.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d1bc1d380a91d954ed5fc9f12915dba014eed0978d2de05ee7ca688bdaac144a", size = 670215 }, + { url = "https://files.pythonhosted.org/packages/30/ba/f9c3c1536c41ef3dbf765ea04218990e2056e558f98184ecd883767fc501/pyzmq-27.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c7cfb75caa83f5153c687e9d2107f64b5ef0ef0d6edd260d3ff920baaaa69101", size = 582252 }, + { url = "https://files.pythonhosted.org/packages/fa/00/78fe097a304a408275747ce43f20428789130b059c5649956277c20f30cf/pyzmq-27.2.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:c5129a8fe43ecc49b99eb75616603d483a3c2fcaef504988fafe8ea392aea98b", size = 1134295 }, + { url = "https://files.pythonhosted.org/packages/f2/83/1c36270658d2ee56e23a3f9ef5fbcb94cbd2f9fe966a6641f2f38e697162/pyzmq-27.2.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:baa2ce3485145653194d6c8c5beedd1e9f0bf46a0919c9fa2fe2204fc35b74d9", size = 1167492 }, + { url = "https://files.pythonhosted.org/packages/58/b2/f0ae223438d7faa991f6feefdc823815f11cc604f898738376b59fd96515/pyzmq-27.2.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:e1ed46048d1920cabc96d952a0d5cfe4127ad8db572c335aae4e3c57b9278d7f", size = 1465992 }, + { url = "https://files.pythonhosted.org/packages/59/46/fb56f3f37a6a0937b0e1d2885e808b5eedc171320bac85573cfae78fa9bc/pyzmq-27.2.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e0fa0bc6b1a184aee59b32efcd1b7f0e6d5b8f9387799e4c16a4cb66a86747d6", size = 976118 }, + { url = "https://files.pythonhosted.org/packages/21/82/a2c9bfd7c4d34eea1278493cd041bc000d41acb4463c89ceaad29dc813b6/pyzmq-27.2.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4bd6743e8bf854c3bfce892dd6578a514aabf128e37a4b2eafcf01856f7e44", size = 705968 }, + { url = "https://files.pythonhosted.org/packages/d6/12/b906b269116b6591dc15c0acc5d04c043957c8a531d336999731f4b1d899/pyzmq-27.2.0-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95369ed6626afcfe2ac89832fb1b917c077fbeb905fbbe5d918349ce0222b89b", size = 879011 }, + { url = "https://files.pythonhosted.org/packages/12/13/f96359534bfb77651c15f1fbfc4bfdd7ec3489d23f434706d39598dd0dcd/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:40124779c3a56ad5d91902df1ff89159cb414b6c1a0ee697abcc66cf5e6db62d", size = 1697496 }, + { url = "https://files.pythonhosted.org/packages/21/b4/2c007ae5f2fe5eca86cbfbc874ed86b5135f2f7812615dfd78606d3c93f6/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:ec8a318dfc27c7d946651b3d9e8025d5734f30c168a822195601827207bac09b", size = 2064347 }, + { url = "https://files.pythonhosted.org/packages/9b/88/767af3a6630c15215f3a66700ec79598a375edd1fdc9d75a3ad522178c01/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:88c0fac061bac269076edeb3a209acefc96cd6167c239daf1c2b404ac48d7012", size = 1917360 }, + { url = "https://files.pythonhosted.org/packages/35/c1/80dd2d20d6e57bc68e1dce1e84bf3e76c9577c1bf728199985c8b4ea0fd1/pyzmq-27.2.0-cp315-cp315t-win32.whl", hash = "sha256:ac126d48cf18aa955daabef43bf0009ff76ad4deee437d09ecf15388214b5beb", size = 591073 }, + { url = "https://files.pythonhosted.org/packages/f8/b5/33b781666f3f52ae834bc9c8e38f4f0483a826c5a91cccc993292007bf10/pyzmq-27.2.0-cp315-cp315t-win_amd64.whl", hash = "sha256:edce90a1e588ec63adbf612cc0ad582de4169cd216c7ae53c15f42a2ee902f35", size = 670701 }, + { url = "https://files.pythonhosted.org/packages/6e/97/bc4f0edefb992df4fdebcf9f0cc40f631cd4ed277e1ed59ef2cd99a5c8c5/pyzmq-27.2.0-cp315-cp315t-win_arm64.whl", hash = "sha256:a843094b4d3d633bc3623e47a2ff50742d6af02bc1f7606aa2e67e971e21878d", size = 581985 }, + { url = "https://files.pythonhosted.org/packages/8c/26/1a7cd2d8e4e3c27d83a46960e22101b69f527843be140cb3375267aa8ca6/pyzmq-27.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:770a37f28ddfbe1d2c40a2e3ce37e5fd10831daa6ae9634105aa8a5d23507b00", size = 875015 }, + { url = "https://files.pythonhosted.org/packages/b8/ed/c8daf770ca31eb293bef40f801f05148e0a96ad538d55ec3bceb226c91ec/pyzmq-27.2.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b398c5fe102b41e1559f7ffdae760aabd5f432d73b047b4ae0eac4e01cb594d2", size = 774400 }, + { url = "https://files.pythonhosted.org/packages/64/6f/b958b0785eb15ab7a78913e3df8de705dbf226ce1d0004998fa4872bdd59/pyzmq-27.2.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0e1af01858d6dc0c09cea57f9cb1ddf4601f04897b6bb1efc3a2038123c87d79", size = 877993 }, + { url = "https://files.pythonhosted.org/packages/2d/5a/7a070d0e9911441061402013f0c64e5772ee781704c5dae093e6b051c6f6/pyzmq-27.2.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:211350c3ccd4746bc5a85e8fe961bad1f7f2f274f67cf1f785fad7f96f562eea", size = 614493 }, + { url = "https://files.pythonhosted.org/packages/b3/28/833485224e1bd8960cbf3539db465ab2bf23b50de885dc4225a637480539/pyzmq-27.2.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dde5e291548ca0f397623b5e523db5c90172b32aa4fd3ba464a79ea31a580b43", size = 780573 }, + { url = "https://files.pythonhosted.org/packages/90/8e/52239b9b5fe4f9cf77e272e6a8c548c01ba407d08d3aeeb3220816669ef1/pyzmq-27.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:94242bd4de6af7e74665e14a88630bccd615057f6acfaf08a3a432551d604645", size = 553963 }, + { url = "https://files.pythonhosted.org/packages/93/22/7187a1f0bf2b8bf8dc6b91762438fb9b472f684f2dc4cb74a24bf8957943/pyzmq-27.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a7c1144dc61777938e932a2c9011b980b89fd8ff3733033b34c44c299187a6e1", size = 875015 }, + { url = "https://files.pythonhosted.org/packages/92/71/09b71620ad52bad4eb68b1516978ecaf52ef623c3fa16e0732a03cf3274c/pyzmq-27.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:c218b816220d05acf6ab1bafca58926d95cbcc5fec5024724666030466308f0c", size = 774395 }, + { url = "https://files.pythonhosted.org/packages/9c/cf/5c8eb9994a14ff5ee5b0cada339421748746c95aee0280c8b656741e8749/pyzmq-27.2.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ae6ebbc0bfe5a21ce21e32ba567bf73df2d93888109c65acbd42506cf9395759", size = 877994 }, + { url = "https://files.pythonhosted.org/packages/97/64/e22094c5555e550b6450ecfcceb6a1205d893d9a18ae27764c8c45acfb16/pyzmq-27.2.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:679b5b1dde326a921ea2c9ec1f9ea3115bfe1b4735779bbc6eb0473a0ed93f71", size = 614492 }, + { url = "https://files.pythonhosted.org/packages/d2/28/5b1042899caed18278c56d54a502f5254d463afe8aea1acbecc98e053391/pyzmq-27.2.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5c6d8744d10b5e1eadd90a7c58f8546acf6bf680ee463f7e6ada09ad6c9f802", size = 780574 }, + { url = "https://files.pythonhosted.org/packages/77/a3/f134603a671c114c6b56eb912bba890f09e2d43b8a28d243be5a5507cf2f/pyzmq-27.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3ee8dd7031d5e23f632e0e7eee67183ca7d2536e0de35dc1e5d69f3471a791e8", size = 553961 }, ] [[package]] @@ -2403,9 +2382,9 @@ dependencies = [ { name = "isodate", marker = "python_full_version < '3.11'" }, { name = "pyparsing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8d/99/d2fec85e5f6bdfe4367dea143119cb4469bf48710487939df0abf7e22003/rdflib-7.2.1.tar.gz", hash = "sha256:cf9b7fa25234e8925da8b1fb09700f8349b5f0f100e785fb4260e737308292ac", size = 4873802, upload-time = "2025-09-19T02:33:36.492Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/99/d2fec85e5f6bdfe4367dea143119cb4469bf48710487939df0abf7e22003/rdflib-7.2.1.tar.gz", hash = "sha256:cf9b7fa25234e8925da8b1fb09700f8349b5f0f100e785fb4260e737308292ac", size = 4873802 } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/98/7fa830bb4b9da21905683a5352aa0a01a1f3082328ae976aad341e980c23/rdflib-7.2.1-py3-none-any.whl", hash = "sha256:1a175bc1386a167a42fbfaba003bfa05c164a2a3ca3cb9c0c97f9c9638ca6ac2", size = 565423, upload-time = "2025-09-19T02:33:30.889Z" }, + { url = "https://files.pythonhosted.org/packages/31/98/7fa830bb4b9da21905683a5352aa0a01a1f3082328ae976aad341e980c23/rdflib-7.2.1-py3-none-any.whl", hash = "sha256:1a175bc1386a167a42fbfaba003bfa05c164a2a3ca3cb9c0c97f9c9638ca6ac2", size = 565423 }, ] [[package]] @@ -2417,78 +2396,78 @@ dependencies = [ { name = "nh3" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/d7/9309494fad74ee831d4546f69325b5519f37c6dfb2d9ba495db8c6d4f4ca/readme_renderer-46.0.tar.gz", hash = "sha256:af3e964914f6310a33ff67b72a4bdd940bed8d7c3bdecd2d14f40edf284bfe90", size = 38382, upload-time = "2026-08-28T15:18:32.49Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/d7/9309494fad74ee831d4546f69325b5519f37c6dfb2d9ba495db8c6d4f4ca/readme_renderer-46.0.tar.gz", hash = "sha256:af3e964914f6310a33ff67b72a4bdd940bed8d7c3bdecd2d14f40edf284bfe90", size = 38382 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/72/ac5ca81fe9121fcaa9d828d21017cba00a16a98e4ea5fb60c878f93dda4f/readme_renderer-46.0-py3-none-any.whl", hash = "sha256:d0dae1f74bb273b534770cb4cccb6bb78735540afdb03c2146f4e19dcd412560", size = 14239, upload-time = "2026-08-28T15:18:31.132Z" }, + { url = "https://files.pythonhosted.org/packages/4b/72/ac5ca81fe9121fcaa9d828d21017cba00a16a98e4ea5fb60c878f93dda4f/readme_renderer-46.0-py3-none-any.whl", hash = "sha256:d0dae1f74bb273b534770cb4cccb6bb78735540afdb03c2146f4e19dcd412560", size = 14239 }, ] [[package]] name = "regex" version = "2024.11.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494, upload-time = "2024-11-06T20:12:31.635Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91", size = 482674, upload-time = "2024-11-06T20:08:57.575Z" }, - { url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", size = 287684, upload-time = "2024-11-06T20:08:59.787Z" }, - { url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", size = 284589, upload-time = "2024-11-06T20:09:01.896Z" }, - { url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", size = 782511, upload-time = "2024-11-06T20:09:04.062Z" }, - { url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", size = 821149, upload-time = "2024-11-06T20:09:06.237Z" }, - { url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", size = 809707, upload-time = "2024-11-06T20:09:07.715Z" }, - { url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", size = 781702, upload-time = "2024-11-06T20:09:10.101Z" }, - { url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", size = 771976, upload-time = "2024-11-06T20:09:11.566Z" }, - { url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", size = 697397, upload-time = "2024-11-06T20:09:13.119Z" }, - { url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", size = 768726, upload-time = "2024-11-06T20:09:14.85Z" }, - { url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", size = 775098, upload-time = "2024-11-06T20:09:16.504Z" }, - { url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", size = 839325, upload-time = "2024-11-06T20:09:18.698Z" }, - { url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", size = 843277, upload-time = "2024-11-06T20:09:21.725Z" }, - { url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", size = 773197, upload-time = "2024-11-06T20:09:24.092Z" }, - { url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", size = 261714, upload-time = "2024-11-06T20:09:26.36Z" }, - { url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", size = 274042, upload-time = "2024-11-06T20:09:28.762Z" }, - { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669, upload-time = "2024-11-06T20:09:31.064Z" }, - { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684, upload-time = "2024-11-06T20:09:32.915Z" }, - { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589, upload-time = "2024-11-06T20:09:35.504Z" }, - { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121, upload-time = "2024-11-06T20:09:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275, upload-time = "2024-11-06T20:09:40.371Z" }, - { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257, upload-time = "2024-11-06T20:09:43.059Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727, upload-time = "2024-11-06T20:09:48.19Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667, upload-time = "2024-11-06T20:09:49.828Z" }, - { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963, upload-time = "2024-11-06T20:09:51.819Z" }, - { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700, upload-time = "2024-11-06T20:09:53.982Z" }, - { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592, upload-time = "2024-11-06T20:09:56.222Z" }, - { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929, upload-time = "2024-11-06T20:09:58.642Z" }, - { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213, upload-time = "2024-11-06T20:10:00.867Z" }, - { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734, upload-time = "2024-11-06T20:10:03.361Z" }, - { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052, upload-time = "2024-11-06T20:10:05.179Z" }, - { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781, upload-time = "2024-11-06T20:10:07.07Z" }, - { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455, upload-time = "2024-11-06T20:10:09.117Z" }, - { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759, upload-time = "2024-11-06T20:10:11.155Z" }, - { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976, upload-time = "2024-11-06T20:10:13.24Z" }, - { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077, upload-time = "2024-11-06T20:10:15.37Z" }, - { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160, upload-time = "2024-11-06T20:10:19.027Z" }, - { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896, upload-time = "2024-11-06T20:10:21.85Z" }, - { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997, upload-time = "2024-11-06T20:10:24.329Z" }, - { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725, upload-time = "2024-11-06T20:10:28.067Z" }, - { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481, upload-time = "2024-11-06T20:10:31.612Z" }, - { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896, upload-time = "2024-11-06T20:10:34.054Z" }, - { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138, upload-time = "2024-11-06T20:10:36.142Z" }, - { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692, upload-time = "2024-11-06T20:10:38.394Z" }, - { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135, upload-time = "2024-11-06T20:10:40.367Z" }, - { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567, upload-time = "2024-11-06T20:10:43.467Z" }, - { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525, upload-time = "2024-11-06T20:10:45.19Z" }, - { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324, upload-time = "2024-11-06T20:10:47.177Z" }, - { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617, upload-time = "2024-11-06T20:10:49.312Z" }, - { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023, upload-time = "2024-11-06T20:10:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072, upload-time = "2024-11-06T20:10:52.926Z" }, - { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130, upload-time = "2024-11-06T20:10:54.828Z" }, - { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857, upload-time = "2024-11-06T20:10:56.634Z" }, - { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006, upload-time = "2024-11-06T20:10:59.369Z" }, - { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650, upload-time = "2024-11-06T20:11:02.042Z" }, - { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545, upload-time = "2024-11-06T20:11:03.933Z" }, - { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045, upload-time = "2024-11-06T20:11:06.497Z" }, - { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182, upload-time = "2024-11-06T20:11:09.06Z" }, - { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733, upload-time = "2024-11-06T20:11:11.256Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122, upload-time = "2024-11-06T20:11:13.161Z" }, - { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545, upload-time = "2024-11-06T20:11:15Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91", size = 482674 }, + { url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", size = 782511 }, + { url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", size = 821149 }, + { url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", size = 809707 }, + { url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", size = 781702 }, + { url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", size = 771976 }, + { url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", size = 697397 }, + { url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", size = 768726 }, + { url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", size = 775098 }, + { url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", size = 839325 }, + { url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", size = 843277 }, + { url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", size = 773197 }, + { url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", size = 261714 }, + { url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", size = 274042 }, + { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, + { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, + { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, + { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, + { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, + { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, + { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, + { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, + { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, + { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, + { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, + { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, + { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, + { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, + { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, + { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, + { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, + { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, + { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, + { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, + { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, + { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, + { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, + { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, + { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, + { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, + { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, + { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, + { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, + { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, + { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, + { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, + { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, + { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, + { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, + { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, + { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, + { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, + { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, + { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, + { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, + { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, ] [[package]] @@ -2501,9 +2480,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 }, ] [[package]] @@ -2513,9 +2492,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 }, ] [[package]] @@ -2525,27 +2504,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490 }, ] [[package]] name = "rfc3986" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326 }, ] [[package]] name = "rfc3986-validator" version = "0.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, + { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242 }, ] [[package]] @@ -2556,35 +2535,35 @@ dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680 } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654 }, ] [[package]] name = "ruff" version = "0.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/b9/9bd84453ed6dd04688de9b3f3a4146a1698e8faae2ceeccce4e14c67ae17/ruff-0.14.0.tar.gz", hash = "sha256:62ec8969b7510f77945df916de15da55311fade8d6050995ff7f680afe582c57", size = 5452071, upload-time = "2025-10-07T18:21:55.763Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/4e/79d463a5f80654e93fa653ebfb98e0becc3f0e7cf6219c9ddedf1e197072/ruff-0.14.0-py3-none-linux_armv6l.whl", hash = "sha256:58e15bffa7054299becf4bab8a1187062c6f8cafbe9f6e39e0d5aface455d6b3", size = 12494532, upload-time = "2025-10-07T18:21:00.373Z" }, - { url = "https://files.pythonhosted.org/packages/ee/40/e2392f445ed8e02aa6105d49db4bfff01957379064c30f4811c3bf38aece/ruff-0.14.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:838d1b065f4df676b7c9957992f2304e41ead7a50a568185efd404297d5701e8", size = 13160768, upload-time = "2025-10-07T18:21:04.73Z" }, - { url = "https://files.pythonhosted.org/packages/75/da/2a656ea7c6b9bd14c7209918268dd40e1e6cea65f4bb9880eaaa43b055cd/ruff-0.14.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:703799d059ba50f745605b04638fa7e9682cc3da084b2092feee63500ff3d9b8", size = 12363376, upload-time = "2025-10-07T18:21:07.833Z" }, - { url = "https://files.pythonhosted.org/packages/42/e2/1ffef5a1875add82416ff388fcb7ea8b22a53be67a638487937aea81af27/ruff-0.14.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ba9a8925e90f861502f7d974cc60e18ca29c72bb0ee8bfeabb6ade35a3abde7", size = 12608055, upload-time = "2025-10-07T18:21:10.72Z" }, - { url = "https://files.pythonhosted.org/packages/4a/32/986725199d7cee510d9f1dfdf95bf1efc5fa9dd714d0d85c1fb1f6be3bc3/ruff-0.14.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e41f785498bd200ffc276eb9e1570c019c1d907b07cfb081092c8ad51975bbe7", size = 12318544, upload-time = "2025-10-07T18:21:13.741Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ed/4969cefd53315164c94eaf4da7cfba1f267dc275b0abdd593d11c90829a3/ruff-0.14.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30a58c087aef4584c193aebf2700f0fbcfc1e77b89c7385e3139956fa90434e2", size = 14001280, upload-time = "2025-10-07T18:21:16.411Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ad/96c1fc9f8854c37681c9613d825925c7f24ca1acfc62a4eb3896b50bacd2/ruff-0.14.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f8d07350bc7af0a5ce8812b7d5c1a7293cf02476752f23fdfc500d24b79b783c", size = 15027286, upload-time = "2025-10-07T18:21:19.577Z" }, - { url = "https://files.pythonhosted.org/packages/b3/00/1426978f97df4fe331074baf69615f579dc4e7c37bb4c6f57c2aad80c87f/ruff-0.14.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eec3bbbf3a7d5482b5c1f42d5fc972774d71d107d447919fca620b0be3e3b75e", size = 14451506, upload-time = "2025-10-07T18:21:22.779Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/9c1cea6e493c0cf0647674cca26b579ea9d2a213b74b5c195fbeb9678e15/ruff-0.14.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16b68e183a0e28e5c176d51004aaa40559e8f90065a10a559176713fcf435206", size = 13437384, upload-time = "2025-10-07T18:21:25.758Z" }, - { url = "https://files.pythonhosted.org/packages/29/b4/4cd6a4331e999fc05d9d77729c95503f99eae3ba1160469f2b64866964e3/ruff-0.14.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb732d17db2e945cfcbbc52af0143eda1da36ca8ae25083dd4f66f1542fdf82e", size = 13447976, upload-time = "2025-10-07T18:21:28.83Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c0/ac42f546d07e4f49f62332576cb845d45c67cf5610d1851254e341d563b6/ruff-0.14.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c958f66ab884b7873e72df38dcabee03d556a8f2ee1b8538ee1c2bbd619883dd", size = 13682850, upload-time = "2025-10-07T18:21:31.842Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c4/4b0c9bcadd45b4c29fe1af9c5d1dc0ca87b4021665dfbe1c4688d407aa20/ruff-0.14.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7eb0499a2e01f6e0c285afc5bac43ab380cbfc17cd43a2e1dd10ec97d6f2c42d", size = 12449825, upload-time = "2025-10-07T18:21:35.074Z" }, - { url = "https://files.pythonhosted.org/packages/4b/a8/e2e76288e6c16540fa820d148d83e55f15e994d852485f221b9524514730/ruff-0.14.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c63b2d99fafa05efca0ab198fd48fa6030d57e4423df3f18e03aa62518c565f", size = 12272599, upload-time = "2025-10-07T18:21:38.08Z" }, - { url = "https://files.pythonhosted.org/packages/18/14/e2815d8eff847391af632b22422b8207704222ff575dec8d044f9ab779b2/ruff-0.14.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:668fce701b7a222f3f5327f86909db2bbe99c30877c8001ff934c5413812ac02", size = 13193828, upload-time = "2025-10-07T18:21:41.216Z" }, - { url = "https://files.pythonhosted.org/packages/44/c6/61ccc2987cf0aecc588ff8f3212dea64840770e60d78f5606cd7dc34de32/ruff-0.14.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a86bf575e05cb68dcb34e4c7dfe1064d44d3f0c04bbc0491949092192b515296", size = 13628617, upload-time = "2025-10-07T18:21:44.04Z" }, - { url = "https://files.pythonhosted.org/packages/73/e6/03b882225a1b0627e75339b420883dc3c90707a8917d2284abef7a58d317/ruff-0.14.0-py3-none-win32.whl", hash = "sha256:7450a243d7125d1c032cb4b93d9625dea46c8c42b4f06c6b709baac168e10543", size = 12367872, upload-time = "2025-10-07T18:21:46.67Z" }, - { url = "https://files.pythonhosted.org/packages/41/77/56cf9cf01ea0bfcc662de72540812e5ba8e9563f33ef3d37ab2174892c47/ruff-0.14.0-py3-none-win_amd64.whl", hash = "sha256:ea95da28cd874c4d9c922b39381cbd69cb7e7b49c21b8152b014bd4f52acddc2", size = 13464628, upload-time = "2025-10-07T18:21:50.318Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2a/65880dfd0e13f7f13a775998f34703674a4554906167dce02daf7865b954/ruff-0.14.0-py3-none-win_arm64.whl", hash = "sha256:f42c9495f5c13ff841b1da4cb3c2a42075409592825dada7c5885c2c844ac730", size = 12565142, upload-time = "2025-10-07T18:21:53.577Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/41/b9/9bd84453ed6dd04688de9b3f3a4146a1698e8faae2ceeccce4e14c67ae17/ruff-0.14.0.tar.gz", hash = "sha256:62ec8969b7510f77945df916de15da55311fade8d6050995ff7f680afe582c57", size = 5452071 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/4e/79d463a5f80654e93fa653ebfb98e0becc3f0e7cf6219c9ddedf1e197072/ruff-0.14.0-py3-none-linux_armv6l.whl", hash = "sha256:58e15bffa7054299becf4bab8a1187062c6f8cafbe9f6e39e0d5aface455d6b3", size = 12494532 }, + { url = "https://files.pythonhosted.org/packages/ee/40/e2392f445ed8e02aa6105d49db4bfff01957379064c30f4811c3bf38aece/ruff-0.14.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:838d1b065f4df676b7c9957992f2304e41ead7a50a568185efd404297d5701e8", size = 13160768 }, + { url = "https://files.pythonhosted.org/packages/75/da/2a656ea7c6b9bd14c7209918268dd40e1e6cea65f4bb9880eaaa43b055cd/ruff-0.14.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:703799d059ba50f745605b04638fa7e9682cc3da084b2092feee63500ff3d9b8", size = 12363376 }, + { url = "https://files.pythonhosted.org/packages/42/e2/1ffef5a1875add82416ff388fcb7ea8b22a53be67a638487937aea81af27/ruff-0.14.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ba9a8925e90f861502f7d974cc60e18ca29c72bb0ee8bfeabb6ade35a3abde7", size = 12608055 }, + { url = "https://files.pythonhosted.org/packages/4a/32/986725199d7cee510d9f1dfdf95bf1efc5fa9dd714d0d85c1fb1f6be3bc3/ruff-0.14.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e41f785498bd200ffc276eb9e1570c019c1d907b07cfb081092c8ad51975bbe7", size = 12318544 }, + { url = "https://files.pythonhosted.org/packages/9a/ed/4969cefd53315164c94eaf4da7cfba1f267dc275b0abdd593d11c90829a3/ruff-0.14.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30a58c087aef4584c193aebf2700f0fbcfc1e77b89c7385e3139956fa90434e2", size = 14001280 }, + { url = "https://files.pythonhosted.org/packages/ab/ad/96c1fc9f8854c37681c9613d825925c7f24ca1acfc62a4eb3896b50bacd2/ruff-0.14.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f8d07350bc7af0a5ce8812b7d5c1a7293cf02476752f23fdfc500d24b79b783c", size = 15027286 }, + { url = "https://files.pythonhosted.org/packages/b3/00/1426978f97df4fe331074baf69615f579dc4e7c37bb4c6f57c2aad80c87f/ruff-0.14.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eec3bbbf3a7d5482b5c1f42d5fc972774d71d107d447919fca620b0be3e3b75e", size = 14451506 }, + { url = "https://files.pythonhosted.org/packages/58/d5/9c1cea6e493c0cf0647674cca26b579ea9d2a213b74b5c195fbeb9678e15/ruff-0.14.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16b68e183a0e28e5c176d51004aaa40559e8f90065a10a559176713fcf435206", size = 13437384 }, + { url = "https://files.pythonhosted.org/packages/29/b4/4cd6a4331e999fc05d9d77729c95503f99eae3ba1160469f2b64866964e3/ruff-0.14.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb732d17db2e945cfcbbc52af0143eda1da36ca8ae25083dd4f66f1542fdf82e", size = 13447976 }, + { url = "https://files.pythonhosted.org/packages/3b/c0/ac42f546d07e4f49f62332576cb845d45c67cf5610d1851254e341d563b6/ruff-0.14.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c958f66ab884b7873e72df38dcabee03d556a8f2ee1b8538ee1c2bbd619883dd", size = 13682850 }, + { url = "https://files.pythonhosted.org/packages/5f/c4/4b0c9bcadd45b4c29fe1af9c5d1dc0ca87b4021665dfbe1c4688d407aa20/ruff-0.14.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7eb0499a2e01f6e0c285afc5bac43ab380cbfc17cd43a2e1dd10ec97d6f2c42d", size = 12449825 }, + { url = "https://files.pythonhosted.org/packages/4b/a8/e2e76288e6c16540fa820d148d83e55f15e994d852485f221b9524514730/ruff-0.14.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c63b2d99fafa05efca0ab198fd48fa6030d57e4423df3f18e03aa62518c565f", size = 12272599 }, + { url = "https://files.pythonhosted.org/packages/18/14/e2815d8eff847391af632b22422b8207704222ff575dec8d044f9ab779b2/ruff-0.14.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:668fce701b7a222f3f5327f86909db2bbe99c30877c8001ff934c5413812ac02", size = 13193828 }, + { url = "https://files.pythonhosted.org/packages/44/c6/61ccc2987cf0aecc588ff8f3212dea64840770e60d78f5606cd7dc34de32/ruff-0.14.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a86bf575e05cb68dcb34e4c7dfe1064d44d3f0c04bbc0491949092192b515296", size = 13628617 }, + { url = "https://files.pythonhosted.org/packages/73/e6/03b882225a1b0627e75339b420883dc3c90707a8917d2284abef7a58d317/ruff-0.14.0-py3-none-win32.whl", hash = "sha256:7450a243d7125d1c032cb4b93d9625dea46c8c42b4f06c6b709baac168e10543", size = 12367872 }, + { url = "https://files.pythonhosted.org/packages/41/77/56cf9cf01ea0bfcc662de72540812e5ba8e9563f33ef3d37ab2174892c47/ruff-0.14.0-py3-none-win_amd64.whl", hash = "sha256:ea95da28cd874c4d9c922b39381cbd69cb7e7b49c21b8152b014bd4f52acddc2", size = 13464628 }, + { url = "https://files.pythonhosted.org/packages/c6/2a/65880dfd0e13f7f13a775998f34703674a4554906167dce02daf7865b954/ruff-0.14.0-py3-none-win_arm64.whl", hash = "sha256:f42c9495f5c13ff841b1da4cb3c2a42075409592825dada7c5885c2c844ac730", size = 12565142 }, ] [[package]] @@ -2600,9 +2579,9 @@ dependencies = [ { name = "requests" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/33/ff71da4c107897dc6e9a11557afe2ff3e391bc578ae4606ad7b9f8237ddf/sbol2-1.4.1.tar.gz", hash = "sha256:e43725df270718489c75392d00ac633165ed87f2f33898105649a6bea122c035", size = 10581853, upload-time = "2022-04-14T14:06:27.128Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/33/ff71da4c107897dc6e9a11557afe2ff3e391bc578ae4606ad7b9f8237ddf/sbol2-1.4.1.tar.gz", hash = "sha256:e43725df270718489c75392d00ac633165ed87f2f33898105649a6bea122c035", size = 10581853 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/09/ee6ebdba66f54847c9ef116bc101b7723e1b6a821f7c617f6059771014d4/sbol2-1.4.1-py3-none-any.whl", hash = "sha256:73ae876a47ae8d8991681e4f871391b7f3ff5bfec9ec35a9ab097b7beae2d684", size = 8487937, upload-time = "2022-04-14T14:06:22.146Z" }, + { url = "https://files.pythonhosted.org/packages/ab/09/ee6ebdba66f54847c9ef116bc101b7723e1b6a821f7c617f6059771014d4/sbol2-1.4.1-py3-none-any.whl", hash = "sha256:73ae876a47ae8d8991681e4f871391b7f3ff5bfec9ec35a9ab097b7beae2d684", size = 8487937 }, ] [[package]] @@ -2621,54 +2600,45 @@ dependencies = [ { name = "cryptography" }, { name = "jeepney" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554 }, ] [[package]] name = "seguid" version = "0.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/59/8e4a40a2e963dadc327da83efb1e5a8f69baf4f6ba88b663ad42fe5253ec/seguid-0.2.1.tar.gz", hash = "sha256:cc5482aea6264d5370076a82559b7ccd0810d4e38489437a9e9118affa9762e5", size = 11763, upload-time = "2025-07-27T08:36:04.982Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/59/8e4a40a2e963dadc327da83efb1e5a8f69baf4f6ba88b663ad42fe5253ec/seguid-0.2.1.tar.gz", hash = "sha256:cc5482aea6264d5370076a82559b7ccd0810d4e38489437a9e9118affa9762e5", size = 11763 } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/14/412c97e1e2ad05c2dad34f30cec8ddb8b010f118fff8f7b4e2817f0ff7ff/seguid-0.2.1-py3-none-any.whl", hash = "sha256:10767009dcee3ee1e1227e68b047e5bde82224de1fec11162c3afc4238ca6e08", size = 14656, upload-time = "2025-07-27T08:36:03.809Z" }, + { url = "https://files.pythonhosted.org/packages/57/14/412c97e1e2ad05c2dad34f30cec8ddb8b010f118fff8f7b4e2817f0ff7ff/seguid-0.2.1-py3-none-any.whl", hash = "sha256:10767009dcee3ee1e1227e68b047e5bde82224de1fec11162c3afc4238ca6e08", size = 14656 }, ] [[package]] name = "send2trash" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c5/f0/184b4b5f8d00f2a92cf96eec8967a3d550b52cf94362dad1100df9e48d57/send2trash-2.1.0.tar.gz", hash = "sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459", size = 17255, upload-time = "2026-01-14T06:27:36.056Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" }, -] - -[[package]] -name = "serpent" -version = "1.43" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/bb/58ab4dc0382c2386355cbcb7ffa490e542159de6012d500a602a6d0ed535/serpent-1.43.tar.gz", hash = "sha256:62dc242fd4ea2a50339f4f5aaaf6ecc55605ee74770d7eb2031e760d90a0d114", size = 91641, upload-time = "2026-05-30T16:30:13.881Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/f0/184b4b5f8d00f2a92cf96eec8967a3d550b52cf94362dad1100df9e48d57/send2trash-2.1.0.tar.gz", hash = "sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459", size = 17255 } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/b0/75745c948bc814d145cc46cb6da219c7acf3875822d47f08d6323ccf0458/serpent-1.43-py3-none-any.whl", hash = "sha256:7907f40151da126d57bbd675364d23898efa6b6601d79379cbae800236586b54", size = 9787, upload-time = "2026-05-30T16:30:12.027Z" }, + { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610 }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, ] [[package]] name = "soupsieve" version = "2.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/c3/1b817965ac12dc002d7c9cd7dfffdd7d4fbf9b45763ef2ffe7b86ee94670/soupsieve-2.10.tar.gz", hash = "sha256:49e9380d7d2905463583bafe285e818c7366a9ed7b3aee221c1ac79c905d8bc0", size = 124337, upload-time = "2026-09-24T02:36:35.048Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/c3/1b817965ac12dc002d7c9cd7dfffdd7d4fbf9b45763ef2ffe7b86ee94670/soupsieve-2.10.tar.gz", hash = "sha256:49e9380d7d2905463583bafe285e818c7366a9ed7b3aee221c1ac79c905d8bc0", size = 124337 } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/87/5ed59e1d0290564e2027ed066c52b059892c4637741337fc0967183c8d4d/soupsieve-2.10-py3-none-any.whl", hash = "sha256:8596eb8967d744174820280fa62b4542a2e955bfaccca73ed8a13c6eb8e9b502", size = 38019, upload-time = "2026-09-24T02:36:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/66/87/5ed59e1d0290564e2027ed066c52b059892c4637741337fc0967183c8d4d/soupsieve-2.10-py3-none-any.whl", hash = "sha256:8596eb8967d744174820280fa62b4542a2e955bfaccca73ed8a13c6eb8e9b502", size = 38019 }, ] [[package]] @@ -2680,9 +2650,9 @@ dependencies = [ { name = "executing" }, { name = "pure-eval" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, ] [[package]] @@ -2698,8 +2668,7 @@ dependencies = [ [package.optional-dependencies] automation = [ - { name = "opentrons" }, - { name = "pudupy" }, + { name = "opentrons", marker = "python_full_version < '3.11'" }, { name = "sbolinventory" }, ] dev = [ @@ -2715,6 +2684,10 @@ notebook = [ { name = "nbclient" }, { name = "nbformat" }, ] +simulation = [ + { name = "opentrons", marker = "python_full_version < '3.11'" }, + { name = "xlsxwriter" }, +] test = [ { name = "pytest" }, { name = "pytest-cov" }, @@ -2733,8 +2706,8 @@ requires-dist = [ { name = "jupyterlab", marker = "extra == 'notebook'", specifier = ">=4,<5" }, { name = "nbclient", marker = "extra == 'notebook'", specifier = ">=0.10,<1" }, { name = "nbformat", marker = "extra == 'notebook'", specifier = ">=5,<6" }, - { name = "opentrons", marker = "extra == 'automation'" }, - { name = "pudupy", marker = "extra == 'automation'" }, + { name = "opentrons", marker = "python_full_version == '3.10.*' and extra == 'automation'", specifier = "==8.8.2" }, + { name = "opentrons", marker = "python_full_version == '3.10.*' and extra == 'simulation'", specifier = "==8.8.2" }, { name = "pydna" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7,<9" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=7,<9" }, @@ -2744,8 +2717,9 @@ requires-dist = [ { name = "sbol2" }, { name = "sbolinventory", marker = "python_full_version >= '3.10' and extra == 'automation'", git = "https://github.com/DRAGGON-Lab/SBOLInventory.git" }, { name = "twine", marker = "extra == 'dev'", specifier = ">=5.0" }, + { name = "xlsxwriter", marker = "extra == 'simulation'", specifier = ">=3.2.5" }, ] -provides-extras = ["test", "dev", "automation", "notebook"] +provides-extras = ["simulation", "test", "dev", "automation", "notebook"] [package.metadata.requires-dev] dev = [{ name = "ruff", specifier = ">=0.14.0" }] @@ -2759,9 +2733,9 @@ dependencies = [ { name = "pywinpty", marker = "os_name == 'nt'" }, { name = "tornado" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154 }, ] [[package]] @@ -2771,84 +2745,84 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "webencodings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195 } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, + { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404 }, ] [[package]] name = "tomli" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, - { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, - { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, - { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, - { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, - { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, - { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, - { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, - { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, - { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, - { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, - { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, - { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, - { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, - { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, - { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, - { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, - { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, - { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, - { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, - { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, - { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, - { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, - { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, - { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, - { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, - { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, - { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, - { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, - { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, - { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, - { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, - { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, - { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, - { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236 }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084 }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832 }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052 }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555 }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128 }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445 }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165 }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891 }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796 }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121 }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070 }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859 }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296 }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124 }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698 }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819 }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766 }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771 }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586 }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792 }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909 }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946 }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705 }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244 }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637 }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925 }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045 }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835 }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109 }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930 }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964 }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065 }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088 }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193 }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488 }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669 }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709 }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563 }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756 }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408 }, ] [[package]] name = "tornado" version = "6.5.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/06/61/53d562a57b28c08eda40b258c0f975e360541943ad7c7bef897a40caafda/tornado-6.5.10.tar.gz", hash = "sha256:a6b1ccd08c04b4a06fb5aeb381be99de5ad1e5375c1785e31d78c880feb57687", size = 537910, upload-time = "2026-09-15T13:47:48.73Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/61/53d562a57b28c08eda40b258c0f975e360541943ad7c7bef897a40caafda/tornado-6.5.10.tar.gz", hash = "sha256:a6b1ccd08c04b4a06fb5aeb381be99de5ad1e5375c1785e31d78c880feb57687", size = 537910 } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/5b/ff5fc58fa2427c30dea74c90053f4fc5eda1e7f3833ed3ecc7147fe2b311/tornado-6.5.10-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9261783640e23258694a9ff0795df430a5a7b0a651d3dd53dd0969ad6be16da7", size = 465883, upload-time = "2026-09-15T13:47:35.463Z" }, - { url = "https://files.pythonhosted.org/packages/ad/f5/cd7be26c34a3315532f3aef5f092465da8f59c334dd439d3c14aaef16461/tornado-6.5.10-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:83e6cf438b106c6b3852d70960967bb1b70c87438050dca0981e4b9aa751a4c1", size = 464046, upload-time = "2026-09-15T13:47:37.178Z" }, - { url = "https://files.pythonhosted.org/packages/60/33/df6d7d04854a58619f8349a51e3edb138324130a7562b0bb21f115bb940f/tornado-6.5.10-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bdf942448169e5336451d0494d7e3d81cfa726d5aa312affdc4682dd62a62f6d", size = 467096, upload-time = "2026-09-15T13:47:38.559Z" }, - { url = "https://files.pythonhosted.org/packages/29/17/cc35dff68272d685cffd8600ffafbd8067e7d05e7348d9f80caddffbbd5f/tornado-6.5.10-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69acca6501eed74582b76dbbceee2a91613f54728e3e418346000d7103101676", size = 468067, upload-time = "2026-09-15T13:47:40.085Z" }, - { url = "https://files.pythonhosted.org/packages/c3/01/6e5349b4e1a53a4b4972a6716785e1fe7407f312063c3972690af8ff301b/tornado-6.5.10-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:66aaa3f57d30c6e6becee83ff28055d5930ac724214bde99393eefda83d5e015", size = 467901, upload-time = "2026-09-15T13:47:41.576Z" }, - { url = "https://files.pythonhosted.org/packages/28/5e/b4facf94370dba006819c8d304376f8b9fbec6b935b5e51bf45823a9790b/tornado-6.5.10-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4bd192b959f9128fb99b8898148070ba4574c9589b78bce42d1851131fe85828", size = 467308, upload-time = "2026-09-15T13:47:43.145Z" }, - { url = "https://files.pythonhosted.org/packages/56/ae/047938e828cafc8eca4c908fafb6588fee944e3af39a0af9d7b602499ae5/tornado-6.5.10-cp39-abi3-win32.whl", hash = "sha256:302eb1e0e3e159314eb591920529fdea80acca92df5510a2cec5bbd4f099ec72", size = 468387, upload-time = "2026-09-15T13:47:44.556Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d4/5901517f05affd752490f6a654ba31b7474664e8dd80bd045a00c220bd88/tornado-6.5.10-cp39-abi3-win_amd64.whl", hash = "sha256:37ae8f150cecfdbf747fc4e12f5e9a97ecd8cf1d4cdb3f119e2de84b11196918", size = 468828, upload-time = "2026-09-15T13:47:45.961Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1a/fd497f3a7f7b74bb04f4b94536b5c9f80742b5d50501fd27977652ddec16/tornado-6.5.10-cp39-abi3-win_arm64.whl", hash = "sha256:ce045d3c298fddd30e89a2777f97039d1b641eb9518ac7b26a4721903539c694", size = 467847, upload-time = "2026-09-15T13:47:47.283Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5b/ff5fc58fa2427c30dea74c90053f4fc5eda1e7f3833ed3ecc7147fe2b311/tornado-6.5.10-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9261783640e23258694a9ff0795df430a5a7b0a651d3dd53dd0969ad6be16da7", size = 465883 }, + { url = "https://files.pythonhosted.org/packages/ad/f5/cd7be26c34a3315532f3aef5f092465da8f59c334dd439d3c14aaef16461/tornado-6.5.10-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:83e6cf438b106c6b3852d70960967bb1b70c87438050dca0981e4b9aa751a4c1", size = 464046 }, + { url = "https://files.pythonhosted.org/packages/60/33/df6d7d04854a58619f8349a51e3edb138324130a7562b0bb21f115bb940f/tornado-6.5.10-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bdf942448169e5336451d0494d7e3d81cfa726d5aa312affdc4682dd62a62f6d", size = 467096 }, + { url = "https://files.pythonhosted.org/packages/29/17/cc35dff68272d685cffd8600ffafbd8067e7d05e7348d9f80caddffbbd5f/tornado-6.5.10-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69acca6501eed74582b76dbbceee2a91613f54728e3e418346000d7103101676", size = 468067 }, + { url = "https://files.pythonhosted.org/packages/c3/01/6e5349b4e1a53a4b4972a6716785e1fe7407f312063c3972690af8ff301b/tornado-6.5.10-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:66aaa3f57d30c6e6becee83ff28055d5930ac724214bde99393eefda83d5e015", size = 467901 }, + { url = "https://files.pythonhosted.org/packages/28/5e/b4facf94370dba006819c8d304376f8b9fbec6b935b5e51bf45823a9790b/tornado-6.5.10-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4bd192b959f9128fb99b8898148070ba4574c9589b78bce42d1851131fe85828", size = 467308 }, + { url = "https://files.pythonhosted.org/packages/56/ae/047938e828cafc8eca4c908fafb6588fee944e3af39a0af9d7b602499ae5/tornado-6.5.10-cp39-abi3-win32.whl", hash = "sha256:302eb1e0e3e159314eb591920529fdea80acca92df5510a2cec5bbd4f099ec72", size = 468387 }, + { url = "https://files.pythonhosted.org/packages/d8/d4/5901517f05affd752490f6a654ba31b7474664e8dd80bd045a00c220bd88/tornado-6.5.10-cp39-abi3-win_amd64.whl", hash = "sha256:37ae8f150cecfdbf747fc4e12f5e9a97ecd8cf1d4cdb3f119e2de84b11196918", size = 468828 }, + { url = "https://files.pythonhosted.org/packages/f3/1a/fd497f3a7f7b74bb04f4b94536b5c9f80742b5d50501fd27977652ddec16/tornado-6.5.10-cp39-abi3-win_arm64.whl", hash = "sha256:ce045d3c298fddd30e89a2777f97039d1b641eb9518ac7b26a4721903539c694", size = 467847 }, ] [[package]] name = "traitlets" version = "5.16.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/2e/a7fbfe268c8a3b32546930c0297c101d65a4a14c304ad5790a9f478f0e4e/traitlets-5.16.1.tar.gz", hash = "sha256:ed900c2b631aa3a112811139fa97b8d2c3bad5e989656bba4b7e52c7852c18c1", size = 166137, upload-time = "2026-08-03T08:32:36.848Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/2e/a7fbfe268c8a3b32546930c0297c101d65a4a14c304ad5790a9f478f0e4e/traitlets-5.16.1.tar.gz", hash = "sha256:ed900c2b631aa3a112811139fa97b8d2c3bad5e989656bba4b7e52c7852c18c1", size = 166137 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/66/0d785f0bc5e4315a96c989bb476d0fc07ea4f85132550c7b156ca2035d52/traitlets-5.16.1-py3-none-any.whl", hash = "sha256:f775618166caa0396c8e337099240f2bd3e5e917d203b2e6fbe21a58d3cb1f6b", size = 86211, upload-time = "2026-08-03T08:32:34.48Z" }, + { url = "https://files.pythonhosted.org/packages/ad/66/0d785f0bc5e4315a96c989bb476d0fc07ea4f85132550c7b156ca2035d52/traitlets-5.16.1-py3-none-any.whl", hash = "sha256:f775618166caa0396c8e337099240f2bd3e5e917d203b2e6fbe21a58d3cb1f6b", size = 86211 }, ] [[package]] @@ -2866,18 +2840,18 @@ dependencies = [ { name = "rich" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/3c/58f808a359700f39a967dffede33efeac809262c03303fa3eec6afff8f49/twine-7.0.0.tar.gz", hash = "sha256:85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177", size = 215032, upload-time = "2026-07-27T15:59:00.825Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/3c/58f808a359700f39a967dffede33efeac809262c03303fa3eec6afff8f49/twine-7.0.0.tar.gz", hash = "sha256:85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177", size = 215032 } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/08/ddcdc06225eaad6de0e48e1002b06d919dbde20582d0662c7af51308e5d6/twine-7.0.0-py3-none-any.whl", hash = "sha256:b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7", size = 43204, upload-time = "2026-07-27T15:58:59.26Z" }, + { url = "https://files.pythonhosted.org/packages/96/08/ddcdc06225eaad6de0e48e1002b06d919dbde20582d0662c7af51308e5d6/twine-7.0.0-py3-none-any.whl", hash = "sha256:b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7", size = 43204 }, ] [[package]] name = "typing-extensions" version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555 } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571 }, ] [[package]] @@ -2885,168 +2859,168 @@ name = "typing-inspection" version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928 } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750 }, ] [[package]] name = "tzdata" version = "2026.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/31/3d74fa778a63b98b7374323befcc0be5ab3bd94afd4096a0124e7379152c/tzdata-2026.4.tar.gz", hash = "sha256:f1b8bd365d8d210c55353f4d7f8d6d8561c0ba50d704b700d195a9424bba0d79", size = 199350, upload-time = "2026-09-12T12:56:03.251Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/31/3d74fa778a63b98b7374323befcc0be5ab3bd94afd4096a0124e7379152c/tzdata-2026.4.tar.gz", hash = "sha256:f1b8bd365d8d210c55353f4d7f8d6d8561c0ba50d704b700d195a9424bba0d79", size = 199350 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/bc/8737e8d54cf51106118039b83f485a4783112fab49ea9d044b234978a46e/tzdata-2026.4-py2.py3-none-any.whl", hash = "sha256:c2169a8b0a7a5e9674da5a135ccdfb2b3e671b333ed9fed17b41f73c34476e81", size = 347494, upload-time = "2026-09-12T12:56:01.67Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bc/8737e8d54cf51106118039b83f485a4783112fab49ea9d044b234978a46e/tzdata-2026.4-py2.py3-none-any.whl", hash = "sha256:c2169a8b0a7a5e9674da5a135ccdfb2b3e671b333ed9fed17b41f73c34476e81", size = 347494 }, ] [[package]] name = "uri-template" version = "1.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140 }, ] [[package]] name = "urllib3" version = "2.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 }, ] [[package]] name = "wcwidth" version = "0.2.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293 } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286 }, ] [[package]] name = "webcolors" version = "25.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905 }, ] [[package]] name = "webencodings" version = "0.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d5/a0/8fd707bcb776a7be556bad06a2ea5fb9bd519df78ef8e26f70ccf0f38bff/webencodings-0.6.1.tar.gz", hash = "sha256:565f9ad031c702dae404e27a099e3e09186a3ab1b9520f06d215502b651fd910", size = 15001, upload-time = "2026-08-15T14:22:57.549Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/a0/8fd707bcb776a7be556bad06a2ea5fb9bd519df78ef8e26f70ccf0f38bff/webencodings-0.6.1.tar.gz", hash = "sha256:565f9ad031c702dae404e27a099e3e09186a3ab1b9520f06d215502b651fd910", size = 15001 } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/c6/040cbc72480d789a5f40d63fb484d3106554c4dfa2d2b70ad5022057750f/webencodings-0.6.1-py3-none-any.whl", hash = "sha256:7fab6269c8bf237c657876b52058ccb182e861518d1c695c1a9aaa8c1c105d5b", size = 8745, upload-time = "2026-08-15T14:22:56.31Z" }, + { url = "https://files.pythonhosted.org/packages/77/c6/040cbc72480d789a5f40d63fb484d3106554c4dfa2d2b70ad5022057750f/webencodings-0.6.1-py3-none-any.whl", hash = "sha256:7fab6269c8bf237c657876b52058ccb182e861518d1c695c1a9aaa8c1c105d5b", size = 8745 }, ] [[package]] name = "websocket-client" version = "1.9.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/cb/a5abcc2891249f393827c650c6296660ce40374ac22d99ab9aea41f9d2a2/websocket_client-1.9.2.tar.gz", hash = "sha256:0fcb57545848be86992e128218fd96dd87a6769ffdb1a968dff79632b85604d0", size = 84110, upload-time = "2026-08-31T14:08:40.964Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/cb/a5abcc2891249f393827c650c6296660ce40374ac22d99ab9aea41f9d2a2/websocket_client-1.9.2.tar.gz", hash = "sha256:0fcb57545848be86992e128218fd96dd87a6769ffdb1a968dff79632b85604d0", size = 84110 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/d2/cc4dc1271e464942db7ee278baae2daa99ee77cb2af744025c04da585a3e/websocket_client-1.9.2-py3-none-any.whl", hash = "sha256:e1a673830a9c7bfa47b1cd3d5e4178f4c9651d80a4eab02c9c23a1c3ec6250ce", size = 95786, upload-time = "2026-08-31T14:08:39.899Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d2/cc4dc1271e464942db7ee278baae2daa99ee77cb2af744025c04da585a3e/websocket_client-1.9.2-py3-none-any.whl", hash = "sha256:e1a673830a9c7bfa47b1cd3d5e4178f4c9651d80a4eab02c9c23a1c3ec6250ce", size = 95786 }, ] [[package]] name = "wheel" version = "0.45.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545, upload-time = "2024-11-23T00:18:23.513Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494, upload-time = "2024-11-23T00:18:21.207Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494 }, ] [[package]] name = "wrapt" version = "1.17.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, - { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, - { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, - { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, - { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, - { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, - { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, - { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, - { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, - { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, - { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, - { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, - { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, - { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, - { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, - { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, - { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, - { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, - { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, - { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, - { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, - { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, - { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, - { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, - { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, - { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, - { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, - { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, - { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, - { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, - { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, - { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, - { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, - { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, - { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, - { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, - { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, - { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, - { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, - { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, - { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, - { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, - { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, - { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, - { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482 }, + { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676 }, + { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957 }, + { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975 }, + { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149 }, + { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209 }, + { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551 }, + { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464 }, + { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748 }, + { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810 }, + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482 }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674 }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959 }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376 }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604 }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782 }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076 }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457 }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745 }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806 }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998 }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020 }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098 }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036 }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156 }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102 }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732 }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705 }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877 }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885 }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003 }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025 }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108 }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072 }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214 }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105 }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766 }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711 }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885 }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896 }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132 }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091 }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172 }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163 }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963 }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945 }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857 }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178 }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310 }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266 }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544 }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283 }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366 }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571 }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094 }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659 }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946 }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717 }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334 }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471 }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591 }, ] [[package]] name = "xlsxwriter" version = "3.2.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/2c/c06ef49dc36e7954e55b802a8b231770d286a9758b3d936bd1e04ce5ba88/xlsxwriter-3.2.9.tar.gz", hash = "sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c", size = 215940, upload-time = "2025-09-16T00:16:21.63Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/2c/c06ef49dc36e7954e55b802a8b231770d286a9758b3d936bd1e04ce5ba88/xlsxwriter-3.2.9.tar.gz", hash = "sha256:254b1c37a368c444eac6e2f867405cc9e461b0ed97a3233b2ac1e574efb4140c", size = 215940 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3", size = 175315, upload-time = "2025-09-16T00:16:20.108Z" }, + { url = "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3", size = 175315 }, ] [[package]] name = "zipp" version = "3.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/27/f0ac6b846684cecce1ee93d32450c45ab607f65c2e0255f0092032d91f07/zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", size = 18454, upload-time = "2023-02-25T02:17:22.503Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/27/f0ac6b846684cecce1ee93d32450c45ab607f65c2e0255f0092032d91f07/zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", size = 18454 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/fa/c9e82bbe1af6266adf08afb563905eb87cab83fde00a0a08963510621047/zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556", size = 6758, upload-time = "2023-02-25T02:17:20.807Z" }, + { url = "https://files.pythonhosted.org/packages/5b/fa/c9e82bbe1af6266adf08afb563905eb87cab83fde00a0a08963510621047/zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556", size = 6758 }, ] From 47135efcc4247a01d2411bc253b8ee7dffd3d57d Mon Sep 17 00:00:00 2001 From: Mike Arpaia Date: Wed, 23 Sep 2026 21:45:51 -0600 Subject: [PATCH 2/5] Simplify the architecture --- LICENSE | 1 + docs/protocols.rst | 101 ++-- .../adapters/native_protocols.py | 55 -- .../adapters/opentrons/simulation.py | 2 +- .../adapters/protocol_requests.py | 42 -- src/buildcompiler/api/protocols.py | 35 +- src/buildcompiler/buildcompiler.py | 17 +- src/buildcompiler/domain/protocol_requests.py | 12 + src/buildcompiler/protocols/__init__.py | 34 +- .../protocols/allocation/__init__.py | 1 - .../protocols/allocation/models.py | 123 ----- .../protocols/allocation/plating.py | 30 - .../protocols/allocation/wells.py | 39 -- src/buildcompiler/protocols/artifacts.py | 43 -- src/buildcompiler/protocols/attribution.py | 26 - .../protocols/backends/__init__.py | 2 +- .../{opentrons => }/_simulation_worker.py | 15 +- .../protocols/backends/markdown.py | 32 +- .../protocols/backends/opentrons.py | 522 ++++++++++++++++++ .../protocols/backends/opentrons/__init__.py | 1 - .../backends/opentrons/allocation.py | 55 -- .../protocols/backends/opentrons/lowering.py | 110 ---- .../backends/opentrons/operations.py | 231 -------- .../protocols/backends/opentrons/palette.py | 29 - .../protocols/backends/opentrons/plating.py | 191 ------- .../protocols/backends/opentrons/profile.py | 131 ----- .../protocols/backends/opentrons/program.py | 40 -- .../protocols/backends/opentrons/rendering.py | 120 ---- .../protocols/backends/opentrons/setup.py | 71 --- .../protocols/backends/opentrons/tips.py | 76 --- .../backends/opentrons/transformation.py | 220 -------- .../backends/{opentrons => }/simulation.py | 6 +- src/buildcompiler/protocols/compiler.py | 190 +++---- src/buildcompiler/protocols/config.py | 7 - src/buildcompiler/protocols/handoffs.py | 82 --- src/buildcompiler/protocols/inputs.py | 102 +++- src/buildcompiler/protocols/materials.py | 40 -- .../protocols/methods/__init__.py | 2 +- .../protocols/methods/assembly.py | 184 +++++- .../protocols/methods/assembly_config.py | 92 --- .../protocols/methods/plating.py | 227 +++++++- .../protocols/methods/plating_config.py | 62 --- .../protocols/methods/transformation.py | 191 ++++++- .../methods/transformation_config.py | 56 -- src/buildcompiler/protocols/models.py | 436 +++++++++++++++ src/buildcompiler/protocols/plans.py | 43 -- src/buildcompiler/protocols/steps.py | 129 ----- src/buildcompiler/protocols/validation.py | 27 - src/buildcompiler/robotutils.py | 4 +- src/buildcompiler/stages/assembly_lvl1.py | 2 +- src/buildcompiler/stages/assembly_lvl2.py | 2 +- tests/automation/test_assembly_equivalence.py | 2 +- tests/automation/test_plating_equivalence.py | 10 +- .../test_transformation_equivalence.py | 2 +- tests/automation/test_workflow_equivalence.py | 16 +- .../test_protocols.py} | 14 +- tests/unit/protocols/test_compilation.py | 19 +- .../protocols/test_transformation_plating.py | 28 +- 58 files changed, 1917 insertions(+), 2465 deletions(-) delete mode 100644 src/buildcompiler/adapters/native_protocols.py delete mode 100644 src/buildcompiler/adapters/protocol_requests.py delete mode 100644 src/buildcompiler/protocols/allocation/__init__.py delete mode 100644 src/buildcompiler/protocols/allocation/models.py delete mode 100644 src/buildcompiler/protocols/allocation/plating.py delete mode 100644 src/buildcompiler/protocols/allocation/wells.py delete mode 100644 src/buildcompiler/protocols/artifacts.py delete mode 100644 src/buildcompiler/protocols/attribution.py rename src/buildcompiler/protocols/backends/{opentrons => }/_simulation_worker.py (87%) create mode 100644 src/buildcompiler/protocols/backends/opentrons.py delete mode 100644 src/buildcompiler/protocols/backends/opentrons/__init__.py delete mode 100644 src/buildcompiler/protocols/backends/opentrons/allocation.py delete mode 100644 src/buildcompiler/protocols/backends/opentrons/lowering.py delete mode 100644 src/buildcompiler/protocols/backends/opentrons/operations.py delete mode 100644 src/buildcompiler/protocols/backends/opentrons/palette.py delete mode 100644 src/buildcompiler/protocols/backends/opentrons/plating.py delete mode 100644 src/buildcompiler/protocols/backends/opentrons/profile.py delete mode 100644 src/buildcompiler/protocols/backends/opentrons/program.py delete mode 100644 src/buildcompiler/protocols/backends/opentrons/rendering.py delete mode 100644 src/buildcompiler/protocols/backends/opentrons/setup.py delete mode 100644 src/buildcompiler/protocols/backends/opentrons/tips.py delete mode 100644 src/buildcompiler/protocols/backends/opentrons/transformation.py rename src/buildcompiler/protocols/backends/{opentrons => }/simulation.py (92%) delete mode 100644 src/buildcompiler/protocols/config.py delete mode 100644 src/buildcompiler/protocols/handoffs.py delete mode 100644 src/buildcompiler/protocols/materials.py delete mode 100644 src/buildcompiler/protocols/methods/assembly_config.py delete mode 100644 src/buildcompiler/protocols/methods/plating_config.py delete mode 100644 src/buildcompiler/protocols/methods/transformation_config.py create mode 100644 src/buildcompiler/protocols/models.py delete mode 100644 src/buildcompiler/protocols/plans.py delete mode 100644 src/buildcompiler/protocols/steps.py delete mode 100644 src/buildcompiler/protocols/validation.py rename tests/unit/{adapters/test_native_protocols.py => api/test_protocols.py} (68%) diff --git a/LICENSE b/LICENSE index 4a7f1c1..68e2cd6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2025 Genetic Logic Lab +Portions copyright (c) 2022 Rudge Lab Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/docs/protocols.rst b/docs/protocols.rst index d17800f..3bc3758 100644 --- a/docs/protocols.rst +++ b/docs/protocols.rst @@ -1,32 +1,49 @@ Native protocol compilation =========================== -``buildcompiler.protocols`` owns the assembly, transformation and plating methods -previously supplied by PUDU's ``SBOLLoopAssembly``, ``HeatShockTransformation`` -and ``Plating`` classes. The planners, allocation records and artifact renderer -require neither PUDU nor the Opentrons SDK. Generated robot scripts import only -Opentrons. The old automated script writers also delegate to this compiler. +``buildcompiler.protocols`` compiles assembly, transformation and plating requests +into review documents, structured handoffs and standalone robot scripts. Planning +and rendering require no robot SDK. Generated scripts import only Opentrons. Structure and ownership ----------------------- -* ``domain.protocol_requests`` defines frozen ``MaterialRef``, - ``AssemblyReaction``, ``TransformationReaction`` and method-specific request - records. Requests retain full material identities and ordered components. -* ``protocols.methods`` contains pure planners and their immutable configurations. - ``TransformationConfig`` owns transformation parameters; its planner owns - chassis grouping, tube consumption and the two replicate axes. -* ``protocols.materials`` and ``protocols.steps`` define samples and shared - operations: transfer, distribute, mix, tip lifecycle and module operations. - ``ProtocolPlan`` records explicit inputs, intermediates, outputs and lineage. -* ``protocols.allocation`` defines container-qualified placements and output - manifests. Sample identity does not depend on a display label or well name. -* ``protocols.backends.opentrons`` owns supported hardware profiles, allocation, - tip scheduling, shared operation lowering and standalone source rendering. - It emits immutable ``OpentronsProgram`` records before rendering Python. -* ``protocols.backends.markdown`` describes the same logical plan and allocation. - ``ArtifactBundle.write`` is the explicit filesystem boundary. Compilation does - not simulate, write files, or mutate SBOL documents. +The package has two small subpackages. Each method keeps its configuration, +hardware profile, planner and well allocation in one file:: + + protocols/ + __init__.py + compiler.py + inputs.py + models.py + methods/ + __init__.py + assembly.py + transformation.py + plating.py + backends/ + __init__.py + markdown.py + opentrons.py + simulation.py + _simulation_worker.py + +``domain.protocol_requests`` defines immutable requests shared with build stages. +``inputs.py`` converts existing JSON and indexed stage records into those requests. +``models.py`` holds ``Sample``, shared operations such as ``Transfer`` and +``Distribute``, ``ProtocolPlan`` and ``OutputManifest``. A sample keeps its material +identity and parent sample IDs independently of its label and physical location. + +``ProtocolCompiler.plan(request)`` calls the appropriate method planner and returns +a plan with unassigned sample locations. ``compile(request, profile=...)`` also +allocates wells, then sends that same plan to both renderers. The Opentrons renderer +emits Python directly from the operations and checks tip use and pipette capacity. +There is no second command representation between the plan and the script. + +``CompiledProtocol`` contains ``plan``, ``script``, ``markdown``, ``manifest`` and a +read-only ``files`` mapping. ``write(directory)`` is the explicit filesystem +boundary. Compilation does not write files, simulate or mutate SBOL documents. +``backends.simulation.simulate_source`` runs the optional SDK in a subprocess. Compiling stage results ----------------------- @@ -56,9 +73,9 @@ Given stage results for corresponding material identities: ), ) - assembled.artifacts.write("results/assembly") - transformed.artifacts.write("results/transformation") - plated.artifacts.write("results/plating") + assembled.write("results/assembly") + transformed.write("results/transformation") + plated.write("results/plating") ``OutputManifest`` represents planned outputs, not evidence that a run happened. The transformation compiler preserves each assembly sample's ID as an upstream @@ -68,8 +85,8 @@ counts. These multiply the configured transformation replicates; the two axes are not interchangeable. Without an input manifest, DNA is allocated sequentially on the temperature module. -The plating profile is explicit here because PUDU's standalone plating default -uses a different source plate definition from its transformation default. The +The plating profile is explicit here because its standalone default uses a +different source plate definition from the transformation default. The compiler does not infer a physical plate transfer from material identity. Compiling existing JSON @@ -102,9 +119,10 @@ manifests retain identity and lineage throughout a workflow. Each compilation returns ``protocol.py``, ``protocol.md``, ``manifest.json``, ``compilation.json`` and a method-specific JSON handoff. ``compilation.json`` -records the resolved configuration, target profile, typed operation kinds, -containers, placements and PUDU reference revision. Writing refuses existing -files unless ``overwrite=True`` is supplied explicitly. +records the resolved configuration, target profile and plan, including operation +kinds and sample locations. These files are also available in memory, for example +``compiled.files["manifest.json"]``. Writing refuses existing files unless +``overwrite=True`` is supplied explicitly. Configuration and supported targets ------------------------------------ @@ -116,16 +134,15 @@ Profiles validate deck collisions, well offsets and tip offsets. Compilation rejects missing source bindings, duplicate placements, exhausted tips, unsupported profiles and capacity violations detected by the supported target. -The initial targets preserve PUDU's API 2.21 hardware choices, pipettes and labware. -They expose deck and starting-position configuration; they do not promise support -for arbitrary replacement instruments or labware definitions. Transformation and +The supported targets use API 2.21 with fixed pipette models and supported labware +definitions. Profiles expose deck and starting-position configuration, rather than +arbitrary instrument substitution. Transformation and plating each use one rack per pipette and reject batches that exhaust those racks. Assembly retains its explicit rack replacement schedule. -PUDU's default transformation simulation skips temperature operations. The native -plan retains those operations with an explicit simulation condition, so both the -review document and script show the distinction. ``water_testing=True`` omits -those operations altogether. +Transformation's temperature operations carry an explicit simulation condition: +they appear in the plan and review document but are skipped during ordinary +simulation. ``water_testing=True`` omits those operations altogether. Validation and migration boundary ---------------------------------- @@ -136,13 +153,11 @@ the independent PUDU implementation with native output and tests the connected three-stage handoff. It separately exercises transformation's temperature path inside the simulator. Hardware and experimental outcomes are not tested. -The native compiler does not reproduce PUDU's Excel presentation or simulation-time -file writes. It returns structured artifacts explicitly. Other PUDU protocol -families are outside this migration. Legacy orchestration, notebook APIs and the -legacy manual plating summary remain compatibility surfaces; their automated +The compiler returns structured artifacts explicitly; it does not produce Excel +workbooks or write handoff files during simulation. Legacy orchestration, notebook +APIs and the legacy manual plating summary remain compatibility surfaces; their automated assembly, transformation and plating script writers use the native compiler. Legacy multi-batch plating produces one standalone script per batch instead of passing an unsupported ``batches`` object into a single protocol. -PUDU's MIT attribution is retained in ``protocols.attribution`` and generated -scripts. +The MIT attribution is retained in the repository's ``LICENSE`` file. diff --git a/src/buildcompiler/adapters/native_protocols.py b/src/buildcompiler/adapters/native_protocols.py deleted file mode 100644 index 9b32fe6..0000000 --- a/src/buildcompiler/adapters/native_protocols.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Legacy JSON entry points backed by BuildCompiler's native protocol compiler.""" - -from dataclasses import fields - -from buildcompiler.protocols import ( - OpentronsPlatingProfile, - PlatingConfig, - PlatingRequest, - ProtocolCompiler, - assembly_request_from_json, - bacterium_manifest_from_json, - plasmid_manifest_from_json, - transformation_request_from_json, -) - - -def compile_assembly_payload(payload, *, name): - return ProtocolCompiler().compile( - assembly_request_from_json(payload, request_id=name) - ) - - -def compile_transformation_payload(payload, *, plasmid_locations): - request = transformation_request_from_json( - payload, request_id="BuildCompiler Transformation" - ) - inputs = ( - plasmid_manifest_from_json(plasmid_locations) if plasmid_locations else None - ) - return ProtocolCompiler().compile(request, inputs=inputs) - - -def compile_plating_payload(payload, *, advanced_params=None): - parameters = dict(payload) - parameters.update(advanced_params or {}) - inputs = bacterium_manifest_from_json(parameters) - parameters.pop("bacterium_locations") - name = parameters.pop("protocol_name", "BuildCompiler Plating") - config_names = {f.name for f in fields(PlatingConfig)} - profile_names = {f.name for f in fields(OpentronsPlatingProfile)} - config = PlatingConfig( - **{key: value for key, value in parameters.items() if key in config_names} - ) - profile = OpentronsPlatingProfile( - **{key: value for key, value in parameters.items() if key in profile_names} - ) - unknown = set(parameters) - config_names - profile_names - if unknown: - raise ValueError( - f"Unsupported native plating parameters: {sorted(unknown)}. Use a supported target profile." - ) - request = PlatingRequest(id=name, sample_ids=tuple(s.id for s in inputs.samples)) - return ProtocolCompiler(plating=config).compile( - request, inputs=inputs, profile=profile - ) diff --git a/src/buildcompiler/adapters/opentrons/simulation.py b/src/buildcompiler/adapters/opentrons/simulation.py index 340daa7..d0f05af 100644 --- a/src/buildcompiler/adapters/opentrons/simulation.py +++ b/src/buildcompiler/adapters/opentrons/simulation.py @@ -7,7 +7,7 @@ from pathlib import Path from buildcompiler.api.options import ProtocolOptions -from buildcompiler.protocols.backends.opentrons.simulation import simulate_source +from buildcompiler.protocols.backends.simulation import simulate_source class OptionalAutomationDependencyError(ImportError): diff --git a/src/buildcompiler/adapters/protocol_requests.py b/src/buildcompiler/adapters/protocol_requests.py deleted file mode 100644 index 86ae83b..0000000 --- a/src/buildcompiler/adapters/protocol_requests.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Translate selected build routes into typed protocol requests.""" - -from collections.abc import Sequence - -from buildcompiler.domain import IndexedBackbone, IndexedPlasmid, IndexedReagent -from buildcompiler.domain.protocol_requests import ( - AssemblyReaction, - AssemblyRequest, - MaterialRef, -) - - -def assembly_request_from_route( - *, - stage_id: str, - products: Sequence[IndexedPlasmid], - parts: Sequence[IndexedPlasmid], - backbone: IndexedBackbone, - restriction_enzyme: IndexedReagent, -) -> AssemblyRequest: - """Use actual produced identities and preserve the selected component order.""" - - def material(record) -> MaterialRef: - return MaterialRef( - identity=record.identity, - label=record.name or record.display_id or record.identity, - ) - - return AssemblyRequest( - id=stage_id, - source_stage_id=stage_id, - reactions=tuple( - AssemblyReaction( - id=f"{stage_id}/product/{index}", - product=material(product), - backbone=material(backbone), - parts=tuple(material(part) for part in parts), - restriction_enzyme=material(restriction_enzyme), - ) - for index, product in enumerate(products) - ), - ) diff --git a/src/buildcompiler/api/protocols.py b/src/buildcompiler/api/protocols.py index f516660..a876f56 100644 --- a/src/buildcompiler/api/protocols.py +++ b/src/buildcompiler/api/protocols.py @@ -1,22 +1,25 @@ """Compile stage requests and physical handoffs without rerunning SBOL operations.""" +from dataclasses import fields + from buildcompiler.domain import StageResult, StageStatus from buildcompiler.domain.protocol_requests import ( AssemblyRequest, TransformationRequest, ) -from buildcompiler.protocols.allocation.models import OutputManifest from buildcompiler.protocols import ( AssemblyConfig, CompiledProtocol, OpentronsAssemblyProfile, - ProtocolCompiler, - TransformationConfig, + OpentronsPlatingProfile, OpentronsTransformationProfile, PlatingConfig, PlatingRequest, - OpentronsPlatingProfile, + ProtocolCompiler, + TransformationConfig, ) +from buildcompiler.protocols.inputs import bacterium_manifest_from_json +from buildcompiler.protocols.models import OutputManifest def compile_assembly( @@ -92,3 +95,27 @@ def compile_plating( return ProtocolCompiler(plating=config).compile( request, profile=profile, inputs=inputs ) + + +def compile_plating_json(payload, *, advanced_params=None): + """Compile a JSON plating payload with explicitly supplied overrides.""" + + parameters = dict(payload) + parameters.update(advanced_params or {}) + inputs = bacterium_manifest_from_json(parameters) + parameters.pop("bacterium_locations") + name = parameters.pop("protocol_name", "BuildCompiler Plating") + config_names = {f.name for f in fields(PlatingConfig)} + profile_names = {f.name for f in fields(OpentronsPlatingProfile)} + config = PlatingConfig( + **{key: value for key, value in parameters.items() if key in config_names} + ) + profile = OpentronsPlatingProfile( + **{key: value for key, value in parameters.items() if key in profile_names} + ) + unknown = set(parameters) - config_names - profile_names + if unknown: + raise ValueError( + f"Unsupported native plating parameters: {sorted(unknown)}. Use a supported target profile." + ) + return compile_plating(inputs, request_id=name, config=config, profile=profile) diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index da86aa4..9e85c02 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -32,10 +32,10 @@ write_plate_map_json, write_plating_protocol_script, ) -from buildcompiler.adapters.native_protocols import ( - compile_assembly_payload, - compile_transformation_payload, - compile_plating_payload, +from buildcompiler.api.protocols import compile_plating_json +from buildcompiler.protocols import ( + ProtocolCompiler, assembly_request_from_json, transformation_request_from_json, + plasmid_manifest_from_json, ) from .adapters.pudu import ( legacy_assembly_routes_to_pudu_json, @@ -1797,7 +1797,7 @@ def _plasmid_sets_for_transformed_strains( def _write_pudu_assembly_protocol_script( self, path: Path, payload: list[dict[str, object]], protocol_name: str ) -> Path: - compiled = compile_assembly_payload(payload, name=protocol_name) + compiled = ProtocolCompiler().compile(assembly_request_from_json(payload, request_id=protocol_name)) path.write_text(compiled.script, encoding="utf-8") return path @@ -1805,8 +1805,9 @@ def _write_pudu_transformation_protocol_script( self, path: Path, transformation_payload: list[dict[str, object]], plasmid_locations: dict[str, list[str]], ) -> Path: - compiled = compile_transformation_payload( - transformation_payload, plasmid_locations=plasmid_locations + compiled = ProtocolCompiler().compile( + transformation_request_from_json(transformation_payload, request_id="BuildCompiler Transformation"), + inputs=plasmid_manifest_from_json(plasmid_locations) if plasmid_locations else None, ) path.write_text(compiled.script, encoding="utf-8") return path @@ -1814,7 +1815,7 @@ def _write_pudu_transformation_protocol_script( def _write_pudu_plating_protocol_script( self, path: Path, plating_payload: dict[str, object] ) -> Path: - compiled = compile_plating_payload(plating_payload) + compiled = compile_plating_json(plating_payload) path.write_text(compiled.script, encoding="utf-8") return path diff --git a/src/buildcompiler/domain/protocol_requests.py b/src/buildcompiler/domain/protocol_requests.py index 54f0bf3..07b3bb5 100644 --- a/src/buildcompiler/domain/protocol_requests.py +++ b/src/buildcompiler/domain/protocol_requests.py @@ -5,6 +5,8 @@ @dataclass(frozen=True, slots=True, kw_only=True) class MaterialRef: + """Stable material identity with a separate human-readable label.""" + identity: str label: str @@ -15,6 +17,8 @@ def __post_init__(self) -> None: @dataclass(frozen=True, slots=True, kw_only=True) class AssemblyReaction: + """A product and the ordered components needed for one assembly.""" + id: str product: MaterialRef backbone: MaterialRef @@ -30,6 +34,8 @@ def __post_init__(self) -> None: @dataclass(frozen=True, slots=True, kw_only=True) class AssemblyRequest: + """An ordered batch of assembly reactions from one build stage.""" + id: str reactions: tuple[AssemblyReaction, ...] source_stage_id: str | None = None @@ -45,6 +51,8 @@ def __post_init__(self) -> None: @dataclass(frozen=True, slots=True, kw_only=True) class TransformationReaction: + """A target strain, its chassis and ordered input plasmids.""" + id: str strain: MaterialRef chassis: MaterialRef @@ -59,6 +67,8 @@ def __post_init__(self) -> None: @dataclass(frozen=True, slots=True, kw_only=True) class TransformationRequest: + """An ordered batch of strain transformations from one build stage.""" + id: str reactions: tuple[TransformationReaction, ...] source_stage_id: str | None = None @@ -74,6 +84,8 @@ def __post_init__(self) -> None: @dataclass(frozen=True, slots=True, kw_only=True) class PlatingRequest: + """The exact upstream sample IDs selected for dilution and plating.""" + id: str sample_ids: tuple[str, ...] source_stage_id: str | None = None diff --git a/src/buildcompiler/protocols/__init__.py b/src/buildcompiler/protocols/__init__.py index a258393..e05a921 100644 --- a/src/buildcompiler/protocols/__init__.py +++ b/src/buildcompiler/protocols/__init__.py @@ -1,32 +1,33 @@ -"""Typed protocol planning and artifact compilation, without optional SDK imports.""" +"""Protocol planning, rendering and explicit output handoffs.""" from buildcompiler.domain.protocol_requests import ( AssemblyReaction, AssemblyRequest, MaterialRef, - TransformationRequest, - TransformationReaction, PlatingRequest, -) - -from buildcompiler.protocols.backends.opentrons.profile import ( - OpentronsAssemblyProfile, - OpentronsTransformationProfile, - OpentronsPlatingProfile, + TransformationReaction, + TransformationRequest, ) from buildcompiler.protocols.compiler import CompiledProtocol, ProtocolCompiler -from buildcompiler.protocols.config import AssemblyConfig from buildcompiler.protocols.inputs import ( assembly_request_from_json, + bacterium_manifest_from_json, + plasmid_manifest_from_json, transformation_request_from_json, ) -from buildcompiler.protocols.handoffs import ( - plasmid_manifest_from_json, - bacterium_manifest_from_json, +from buildcompiler.protocols.methods.assembly import ( + AssemblyConfig, + OpentronsAssemblyProfile, +) +from buildcompiler.protocols.methods.plating import ( + OpentronsPlatingProfile, + PlatingConfig, +) +from buildcompiler.protocols.methods.transformation import ( + OpentronsTransformationProfile, + TransformationConfig, ) -from buildcompiler.protocols.methods.transformation_config import TransformationConfig -from buildcompiler.protocols.methods.plating_config import PlatingConfig -from buildcompiler.protocols.plans import ProtocolPlan +from buildcompiler.protocols.models import OutputManifest, ProtocolPlan __all__ = [ "AssemblyConfig", @@ -48,4 +49,5 @@ "PlatingConfig", "OpentronsPlatingProfile", "bacterium_manifest_from_json", + "OutputManifest", ] diff --git a/src/buildcompiler/protocols/allocation/__init__.py b/src/buildcompiler/protocols/allocation/__init__.py deleted file mode 100644 index 78f2e70..0000000 --- a/src/buildcompiler/protocols/allocation/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Container geometry and explicit sample placement.""" diff --git a/src/buildcompiler/protocols/allocation/models.py b/src/buildcompiler/protocols/allocation/models.py deleted file mode 100644 index 4c1b8e0..0000000 --- a/src/buildcompiler/protocols/allocation/models.py +++ /dev/null @@ -1,123 +0,0 @@ -"""Allocation records are data, independent of any robot SDK.""" - -from dataclasses import dataclass - -from buildcompiler.protocols.materials import Sample -from buildcompiler.protocols.plans import ProtocolPlan - - -@dataclass(frozen=True, slots=True, kw_only=True) -class WellRef: - container_id: str - well_name: str - - -@dataclass(frozen=True, slots=True, kw_only=True) -class SamplePlacement: - sample_id: str - location: WellRef - - -@dataclass(frozen=True, slots=True, kw_only=True) -class ContainerSpec: - id: str - definition_id: str - parent: str - - -@dataclass(frozen=True, slots=True, kw_only=True) -class OutputManifest: - protocol_id: str - samples: tuple[Sample, ...] - placements: tuple[SamplePlacement, ...] - - def __post_init__(self) -> None: - ids = {s.id for s in self.samples} - if ( - len(ids) != len(self.samples) - or len(self.placements) != len(ids) - or {p.sample_id for p in self.placements} != ids - ): - raise ValueError( - "Manifest samples need unique ids and exactly one placement each." - ) - if len({p.location for p in self.placements}) != len(self.placements): - raise ValueError("Manifest sample locations must be unique.") - - def to_dict(self) -> dict: - locations = {p.sample_id: p.location for p in self.placements} - return { - "schema_version": "1.0", - "protocol_id": self.protocol_id, - "state": "planned", - "outputs": [ - { - "sample_id": sample.id, - "material_identity": sample.material.identity, - "label": sample.material.label, - "parent_sample_ids": list(sample.parent_ids), - "replicate": sample.replicate, - "source_sample_id": sample.source_sample_id, - "contents": list(sample.contents), - "dilution": sample.dilution, - "container_id": locations[sample.id].container_id, - "well_name": locations[sample.id].well_name, - } - for sample in self.samples - ], - } - - def plasmid_locations(self) -> dict[str, list[str]]: - """Export the PUDU handoff only when one container is unambiguous.""" - locations = {p.sample_id: p.location for p in self.placements} - if len({p.location.container_id for p in self.placements}) > 1: - raise ValueError( - "PUDU's location format cannot represent multiple containers." - ) - result: dict[str, list[str]] = {} - for sample in self.samples: - result.setdefault(sample.material.identity, []).append( - locations[sample.id].well_name - ) - return result - - def bacterium_locations(self) -> dict[str, list[str]]: - if len({p.location.container_id for p in self.placements}) > 1: - raise ValueError( - "PUDU's location format cannot represent multiple containers." - ) - locations = {p.sample_id: p.location for p in self.placements} - return { - locations[s.id].well_name: list(s.contents or (s.material.label,)) - for s in self.samples - } - - -@dataclass(frozen=True, slots=True, kw_only=True) -class AllocatedProtocolPlan: - protocol: ProtocolPlan - containers: tuple[ContainerSpec, ...] - placements: tuple[SamplePlacement, ...] - - def __post_init__(self) -> None: - sample_ids = {sample.id for sample in self.protocol.samples} - if ( - len(self.placements) != len(sample_ids) - or {p.sample_id for p in self.placements} != sample_ids - ): - raise ValueError("Every sample must have exactly one placement.") - if len({p.location for p in self.placements}) != len(self.placements): - raise ValueError("Two samples cannot occupy the same location.") - container_ids = {container.id for container in self.containers} - if len(container_ids) != len(self.containers): - raise ValueError("Container ids must be unique.") - if any(p.location.container_id not in container_ids for p in self.placements): - raise ValueError("Placements must reference declared containers.") - - def output_manifest(self) -> OutputManifest: - ids = set(self.protocol.output_sample_ids) - return OutputManifest( - protocol_id=self.protocol.id, - samples=tuple(s for s in self.protocol.samples if s.id in ids), - placements=tuple(p for p in self.placements if p.sample_id in ids), - ) diff --git a/src/buildcompiler/protocols/allocation/plating.py b/src/buildcompiler/protocols/allocation/plating.py deleted file mode 100644 index d672443..0000000 --- a/src/buildcompiler/protocols/allocation/plating.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Export the plated sample graph without recalculating a second plate layout.""" - -from buildcompiler.protocols.allocation.models import AllocatedProtocolPlan - - -def plating_layout( - allocation: AllocatedProtocolPlan, *, dilution_factor: float -) -> dict: - samples = {s.id: s for s in allocation.protocol.samples} - locations = {p.sample_id: p.location for p in allocation.placements} - plates = {} - outputs = [samples[sid] for sid in allocation.protocol.output_sample_ids] - # PUDU's export groups dilution first, then construct and replicate. - for sample in sorted(outputs, key=lambda s: s.dilution): - source = samples[sample.parent_ids[0]] - while source.role == "dilution": - source = samples[source.parent_ids[0]] - location = locations[sample.id] - plate = location.container_id.replace("agar_", "plate_") - factor = dilution_factor**sample.dilution - factor = int(factor) if float(factor).is_integer() else factor - entry = plates.setdefault(plate, {}).setdefault( - f"dilution_{sample.dilution}", {"ratio": f"1/{factor}", "wells": {}} - ) - entry["wells"][location.well_name] = { - "construct": ", ".join(source.contents), - "source_well": locations[source.id].well_name, - "replicate": sample.replicate, - } - return {"agar_plates": plates} diff --git a/src/buildcompiler/protocols/allocation/wells.py b/src/buildcompiler/protocols/allocation/wells.py deleted file mode 100644 index 934bd69..0000000 --- a/src/buildcompiler/protocols/allocation/wells.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Well geometry with explicit, deterministic column-major traversal.""" - -import re -from dataclasses import dataclass - - -@dataclass(frozen=True, slots=True, kw_only=True) -class WellGrid: - rows: int - columns: int - - def __post_init__(self) -> None: - if type(self.rows) is not int or not 1 <= self.rows <= 26: - raise ValueError("rows must be an integer between 1 and 26.") - if type(self.columns) is not int or self.columns < 1: - raise ValueError("columns must be a positive integer.") - - @property - def capacity(self) -> int: - return self.rows * self.columns - - def name(self, index: int) -> str: - if type(index) is not int or not 0 <= index < self.capacity: - raise ValueError(f"Well index {index} is outside this grid.") - return f"{chr(ord('A') + index % self.rows)}{index // self.rows + 1}" - - def index(self, name: str) -> int: - match = re.fullmatch(r"([A-Z])([1-9][0-9]*)", name) - if not match: - raise ValueError(f"Invalid well name: {name!r}") - row = ord(match[1]) - ord("A") - column = int(match[2]) - 1 - if row >= self.rows or column >= self.columns: - raise ValueError(f"Well {name} is outside this grid.") - return column * self.rows + row - - -PLATE_96 = WellGrid(rows=8, columns=12) -BLOCK_24 = WellGrid(rows=4, columns=6) diff --git a/src/buildcompiler/protocols/artifacts.py b/src/buildcompiler/protocols/artifacts.py deleted file mode 100644 index 1f371dd..0000000 --- a/src/buildcompiler/protocols/artifacts.py +++ /dev/null @@ -1,43 +0,0 @@ -"""In-memory artifacts and an explicit, opt-in filesystem boundary.""" - -from dataclasses import dataclass -from pathlib import Path - - -@dataclass(frozen=True, slots=True, kw_only=True) -class ProtocolArtifact: - name: str - media_type: str - content: str - - def __post_init__(self) -> None: - if ( - not self.name - or self.name in (".", "..") - or Path(self.name).name != self.name - or "\\" in self.name - ): - raise ValueError("Artifact names must be plain filenames.") - - -@dataclass(frozen=True, slots=True, kw_only=True) -class ArtifactBundle: - artifacts: tuple[ProtocolArtifact, ...] - - def __post_init__(self) -> None: - if len({artifact.name for artifact in self.artifacts}) != len(self.artifacts): - raise ValueError("Artifact filenames must be unique.") - - def write( - self, directory: str | Path, *, overwrite: bool = False - ) -> tuple[Path, ...]: - """Write only when called, refusing existing files by default.""" - output = Path(directory) - paths = tuple(output / artifact.name for artifact in self.artifacts) - if not overwrite and any(path.exists() for path in paths): - raise FileExistsError("One or more output artifacts already exist.") - output.mkdir(parents=True, exist_ok=True) - for path, artifact in zip(paths, self.artifacts, strict=True): - with path.open("w" if overwrite else "x", encoding="utf-8") as handle: - handle.write(artifact.content) - return paths diff --git a/src/buildcompiler/protocols/attribution.py b/src/buildcompiler/protocols/attribution.py deleted file mode 100644 index 83bf9d2..0000000 --- a/src/buildcompiler/protocols/attribution.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Attribution retained in source distributions and standalone generated scripts.""" - -PUDU_REVISION = "cdf7bde2128871c889fb25a4a0c168d5d8f0c1d3" - -PUDU_LICENSE = """MIT License - -Copyright (c) 2022 Rudge Lab - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" diff --git a/src/buildcompiler/protocols/backends/__init__.py b/src/buildcompiler/protocols/backends/__init__.py index a16601a..f0a0733 100644 --- a/src/buildcompiler/protocols/backends/__init__.py +++ b/src/buildcompiler/protocols/backends/__init__.py @@ -1 +1 @@ -"""Artifact renderers and target-specific compilation.""" +"""Render shared plans and explicitly run the optional robot simulator.""" diff --git a/src/buildcompiler/protocols/backends/opentrons/_simulation_worker.py b/src/buildcompiler/protocols/backends/_simulation_worker.py similarity index 87% rename from src/buildcompiler/protocols/backends/opentrons/_simulation_worker.py rename to src/buildcompiler/protocols/backends/_simulation_worker.py index 5c4bb37..311f5c0 100644 --- a/src/buildcompiler/protocols/backends/opentrons/_simulation_worker.py +++ b/src/buildcompiler/protocols/backends/_simulation_worker.py @@ -1,4 +1,4 @@ -"""Private worker, launched as a file so core imports never load the SDK. +"""Private simulator worker, launched as a file so core imports never load the SDK. The trace adapter is qualified against Opentrons 8.8.2. SDK calls are observed, not substituted; the official simulate() entry point runs both sides. @@ -7,11 +7,14 @@ import functools import inspect import json +import sys from importlib.metadata import version from pathlib import Path def main() -> None: + """Run one protocol in isolation and write a deterministic simulator trace.""" + from opentrons import protocol_api, types from opentrons.legacy_broker import LegacyBroker from opentrons.protocol_api import module_contexts @@ -24,6 +27,8 @@ def main() -> None: raise RuntimeError("The structured trace adapter requires opentrons==8.8.2.") def normalize(value): + """Strip unstable SDK object identity while preserving command semantics.""" + if value is None or type(value) in (str, int, float, bool): return value if value is protocol_api.OFF_DECK: @@ -62,6 +67,8 @@ def normalize(value): publish = LegacyBroker.publish def capture(self, topic, message): + """Observe nested commands without replacing SDK execution.""" + nonlocal depth if topic == "command": if message["$"] == "before": @@ -81,11 +88,14 @@ def capture(self, topic, message): LegacyBroker.publish = capture def observe(cls, method_name): + """Capture bound SDK arguments that the ordinary command log omits.""" + original = getattr(cls, method_name) signature = inspect.signature(original) @functools.wraps(original) def wrapper(*args, **kwargs): + """Record resolved arguments before delegating to the SDK method.""" bound = signature.bind(*args, **kwargs) bound.apply_defaults() parameters = { @@ -130,4 +140,7 @@ def wrapper(*args, **kwargs): if __name__ == "__main__": + # Resolve the installed SDK, not the sibling opentrons.py source renderer. + if sys.path[0] == str(Path(__file__).resolve().parent): + del sys.path[0] main() diff --git a/src/buildcompiler/protocols/backends/markdown.py b/src/buildcompiler/protocols/backends/markdown.py index 1580f46..6371757 100644 --- a/src/buildcompiler/protocols/backends/markdown.py +++ b/src/buildcompiler/protocols/backends/markdown.py @@ -1,43 +1,39 @@ -"""Render a review document directly from the same plan used by automation.""" +"""Readable review documents generated from the protocol plan.""" -from buildcompiler.protocols.allocation.models import AllocatedProtocolPlan -from buildcompiler.protocols.plans import ProtocolPlan -from buildcompiler.protocols.steps import ( +from buildcompiler.protocols.models import ( DeactivateSourceModule, + Distribute, DropTip, LidAction, + Mix, OperatorAction, + PickUpTip, + ProtocolPlan, RunTemperatureProgram, SetTemperature, Transfer, - Distribute, - PickUpTip, - Mix, ) def _escape(text: str) -> str: + """Keep labels on one Markdown line without breaking table syntax.""" + return text.replace("\\", "\\\\").replace("|", "\\|").replace("\n", " ") -def render_markdown( - plan: ProtocolPlan, *, allocation: AllocatedProtocolPlan | None = None -) -> str: +def render_markdown(plan: ProtocolPlan) -> str: """Render planned operations, never claim that an experiment has run.""" - if allocation is not None and allocation.protocol != plan: - raise ValueError("Allocation belongs to a different protocol plan.") samples = {sample.id: sample for sample in plan.samples} - locations = ( - {p.sample_id: p.location for p in allocation.placements} if allocation else {} - ) def describe(sample_id: str) -> str: + """Describe a sample with its replicate and assigned well when available.""" + sample = samples[sample_id] result = _escape(sample.material.label) if sample.replicate is not None: result += f" (replicate {sample.replicate})" - if sample_id in locations: - location = locations[sample_id] + if sample.location is not None: + location = sample.location result += f" [{location.container_id}/{location.well_name}]" return result @@ -93,7 +89,7 @@ def describe(sample_id: str) -> str: else: raise TypeError(f"Unsupported operation: {type(step).__name__}") if getattr(step, "skip_during_simulation", False): - action += " Omitted during Opentrons simulation (PUDU behavior)." + action += " Omitted during Opentrons simulation." lines.append(f"{index}. {action}") lines.extend(("", "## Planned outputs", "")) for sample_id in plan.output_sample_ids: diff --git a/src/buildcompiler/protocols/backends/opentrons.py b/src/buildcompiler/protocols/backends/opentrons.py new file mode 100644 index 0000000..9e99865 --- /dev/null +++ b/src/buildcompiler/protocols/backends/opentrons.py @@ -0,0 +1,522 @@ +"""Render allocated plans directly as standalone Opentrons Python protocols.""" + +import keyword +import math + +from buildcompiler.protocols.methods.assembly import OpentronsAssemblyProfile +from buildcompiler.protocols.methods.plating import OpentronsPlatingProfile +from buildcompiler.protocols.methods.transformation import ( + OpentronsTransformationProfile, +) +from buildcompiler.protocols.models import ( + PLATE_96, + DeactivateSourceModule, + Distribute, + DropTip, + LidAction, + Mix, + OperatorAction, + PickUpTip, + ProtocolPlan, + RunTemperatureProgram, + Sample, + SamplePoint, + SetTemperature, + Transfer, +) + +# Colors are display metadata; all lookups use sample or material identities. +COLORS = ( + "#4040BF", + "#BF4040", + "#40BF40", + "#A640BF", + "#BFBF40", + "#BF7340", + "#40BFBF", + "#BF40A6", + "#73BF40", + "#4073BF", + "#BF8C40", + "#40BF73", + "#7340BF", + "#A6BF40", + "#BF5940", + "#40A6BF", + "#BF4073", + "#59BF40", + "#BFA640", + "#40BFA6", + "#8CBF40", + "#40BF59", + "#40BF8C", + "#BF40A6", +) + +TargetProfile = ( + OpentronsAssemblyProfile | OpentronsTransformationProfile | OpentronsPlatingProfile +) + + +def _well(sample: Sample, point: SamplePoint | None = None) -> str: + """Format a bound well and its optional aspiration or dispense offset.""" + location = sample.location + if location is None: + raise ValueError(f"Sample {sample.id} has not been allocated.") + expression = f"{location.container_id}[{location.well_name!r}]" + if point is not None: + if point.bottom_mm is not None: + expression += f".bottom({point.bottom_mm!r})" + elif point.top_mm is not None: + expression += f".top({point.top_mm!r})" + elif point.track_conical_height: + expression = f"_conical_source({expression})" + return expression + + +def _assembly_tips( + count: int, profile: OpentronsAssemblyProfile +) -> tuple[list[str], list[list[str]]]: + """Schedule exact tips and rack swaps, including a partially used first rack.""" + first = PLATE_96.index(profile.initial_tip) + rack_count = (first + count + 95) // 96 + slots = profile.tiprack_positions + setup = [] + for index in range(rack_count): + slot = repr(slots[index]) if index < len(slots) else "protocol_api.OFF_DECK" + setup.append( + f"tips_{index} = protocol.load_labware('opentrons_96_tiprack_20ul', {slot})" + ) + racks = ", ".join(f"tips_{index}" for index in range(rack_count)) + setup.append( + f"pipette = protocol.load_instrument('p20_single_gen2', {profile.pipette_position!r}, tip_racks=[{racks}])" + ) + setup.append(f"pipette.starting_tip = tips_0[{profile.initial_tip!r}]") + pickups = [] + active_batch = 0 + for index in range(first, first + count): + rack, well = divmod(index, 96) + batch = rack // len(slots) + lines = [] + if batch != active_batch: + # Clear the occupied slots before bringing the next racks on deck. + for old in range( + active_batch * len(slots), + min((active_batch + 1) * len(slots), rack_count), + ): + lines.append( + f"protocol.move_labware(labware=tips_{old}, new_location=protocol_api.OFF_DECK)" + ) + for new in range( + batch * len(slots), min((batch + 1) * len(slots), rack_count) + ): + lines.append( + f"protocol.move_labware(labware=tips_{new}, new_location={slots[new % len(slots)]!r})" + ) + active_batch = batch + lines.append(f"pipette.pick_up_tip(tips_{rack}[{PLATE_96.name(well)!r}])") + pickups.append(lines) + return setup, pickups + + +def _validate_tips(plan: ProtocolPlan, starts: dict[str, str | None]) -> None: + """Reject a batch that would exhaust a pipette's single on-deck rack.""" + counts = dict.fromkeys(starts, 0) + for step in plan.steps: + if ( + isinstance(step, PickUpTip) + or isinstance(step, Transfer) + and step.new_tip + or isinstance(step, Distribute) + and step.new_tip == "once" + ): + counts[step.instrument] += 1 + for instrument, count in counts.items(): + available = 96 - PLATE_96.index(starts[instrument] or "A1") + if count > available: + raise ValueError( + f"{instrument} requires {count} tips but only {available} remain." + ) + + +def _instruments( + small_slot: str, large_slot: str, starts: dict[str, str | None], *, filtered: bool +) -> list[str]: + """Load the two pipettes and apply each rack's starting tip.""" + small_rack = ( + "opentrons_96_filtertiprack_20ul" if filtered else "opentrons_96_tiprack_20ul" + ) + lines = [ + f"small_tips = protocol.load_labware({small_rack!r}, {small_slot!r})", + f"large_tips = protocol.load_labware('opentrons_96_filtertiprack_200ul', {large_slot!r})", + ] + for name, model, mount in ( + ("small", "p20_single_gen2", "left"), + ("large", "p300_single_gen2", "right"), + ): + lines.append( + f"{name} = protocol.load_instrument({model!r}, {mount!r}, tip_racks=[{name}_tips])" + ) + if starts[name]: + lines.append(f"{name}.starting_tip = {name}_tips[{starts[name]!r}]") + return lines + + +def _liquid( + sample: Sample, + variable: str, + *, + name: str, + color: str, + description: str | None = None, + define: bool = True, +) -> list[str]: + """Define display metadata once and register a sample's initial volume.""" + lines = [] + if define: + arguments = f"name={name!r}, display_color={color!r}" + if description is not None: + arguments += f", description={description!r}" + lines.append(f"{variable} = protocol.define_liquid({arguments})") + lines.append( + f"{_well(sample)}.load_liquid(liquid={variable}, volume={sample.initial_volume_ul!r})" + ) + return lines + + +def _operations( + plan: ProtocolPlan, + capacities: dict[str, float], + liquids: dict[str, str], + pickups: list[list[str]] | None = None, +) -> list[str]: + """Emit operations in order while checking volumes and tip ownership.""" + samples = {s.id: s for s in plan.samples} + attached = dict.fromkeys(capacities, False) + tip_index = 0 + lines = [] + + def point(value: SamplePoint) -> str: + """Resolve a logical sample point to its allocated well expression.""" + return _well(samples[value.sample_id], value) + + def check_volume(instrument: str, *volumes: float) -> None: + """Check each individual pipette movement against the loaded capacity.""" + for volume in volumes: + if not math.isfinite(volume) or not 0 <= volume <= capacities[instrument]: + raise ValueError( + f"Operation exceeds supported {instrument} pipette capacity." + ) + + def require_tip(instrument: str) -> None: + """Reject operations that assume a tip without first attaching one.""" + if not attached[instrument]: + raise ValueError(f"An attached tip is required on {instrument}.") + + def pickup(instrument: str) -> None: + """Attach the next tip, including any scheduled rack replacement.""" + nonlocal tip_index + if attached[instrument]: + raise ValueError(f"A tip is already attached to {instrument}.") + if pickups is None: + lines.append(f"{instrument}.pick_up_tip()") + else: + lines.extend(pickups[tip_index]) + tip_index += 1 + attached[instrument] = True + + def drop(instrument: str) -> None: + """Emit a drop and clear the instrument's tracked tip state.""" + require_tip(instrument) + lines.append(f"{instrument}.drop_tip()") + attached[instrument] = False + + for step in plan.steps: + start = len(lines) + if isinstance(step, Transfer): + instrument = step.instrument + volume = ( + min(step.volume_ul, capacities[instrument]) + if step.limit_to_pipette_capacity + else step.volume_ul + ) + check_volume(instrument, volume, step.mix_before_ul) + source, destination = point(step.source), point(step.destination) + if step.new_tip: + pickup(instrument) + require_tip(instrument) + if step.mix_before_ul: + lines.append( + f"{instrument}.mix({step.mix_repetitions!r}, {step.mix_before_ul!r}, {source})" + ) + lines.extend( + ( + f"{instrument}.aspirate({volume!r}, {source}, rate={step.aspiration_rate!r})", + f"{instrument}.dispense({volume!r}, {destination}, rate={step.dispense_rate!r})", + ) + ) + if step.blow_out: + lines.append(f"{instrument}.blow_out()") + if step.touch_tip: + lines.append( + f"{instrument}.touch_tip(radius=0.5, v_offset=-14, speed=20)" + ) + if step.drop_tip: + drop(instrument) + elif isinstance(step, PickUpTip): + pickup(step.instrument) + elif isinstance(step, DropTip): + drop(step.instrument) + elif isinstance(step, Mix): + require_tip(step.instrument) + check_volume(step.instrument, step.volume_ul) + lines.append( + f"{step.instrument}.mix(repetitions={step.repetitions!r}, volume={step.volume_ul!r}, location={point(step.location)})" + ) + elif isinstance(step, Distribute): + # The SDK splits a group into refills; capacity applies to one + # destination plus disposal volume and air, not the whole group. + check_volume( + step.instrument, + step.volume_ul + step.disposal_volume_ul + (step.air_gap_ul or 0), + ) + if step.mix_before: + check_volume(step.instrument, step.mix_before[1]) + if step.new_tip == "never": + require_tip(step.instrument) + elif step.new_tip != "once" or attached[step.instrument]: + raise ValueError( + "A distribution with new_tip='once' requires an empty pipette." + ) + destinations = ", ".join(point(d) for d in step.destinations) + arguments = f"volume={step.volume_ul!r}, source={point(step.source)}, dest=[{destinations}], disposal_volume={step.disposal_volume_ul!r}, new_tip={step.new_tip!r}" + if step.mix_before: + arguments += f", mix_before={step.mix_before!r}" + if step.air_gap_ul is not None: + arguments += f", air_gap={step.air_gap_ul!r}" + lines.append(f"{step.instrument}.distribute({arguments})") + if step.track_liquid_sample_id: + liquid = liquids[step.track_liquid_sample_id] + for destination in step.destinations: + lines.append( + f"{point(destination)}.load_liquid(liquid={liquid}, volume={step.volume_ul!r})" + ) + elif isinstance(step, LidAction): + if step.action not in ("open", "close"): + raise ValueError("Unsupported lid action.") + lines.append(f"reaction_module.{step.action}_lid()") + elif isinstance(step, SetTemperature): + target = { + "source": "source_module.set_temperature", + "reaction": "reaction_module.set_block_temperature", + "lid": "reaction_module.set_lid_temperature", + }[step.module] + lines.append(f"{target}({step.celsius!r})") + elif isinstance(step, DeactivateSourceModule): + lines.append("source_module.deactivate()") + elif isinstance(step, RunTemperatureProgram): + program = step.program + stages = [ + {"temperature": s.celsius, "hold_time_minutes": s.minutes} + for s in program.steps + ] + lines.append( + f"reaction_module.execute_profile(steps={stages!r}, repetitions={program.repetitions!r}, block_max_volume={program.block_max_volume_ul!r})" + ) + elif isinstance(step, OperatorAction): + lines.append(f"protocol.comment({step.instruction!r})") + else: + raise TypeError(f"Unsupported protocol operation: {type(step).__name__}") + if getattr(step, "skip_during_simulation", False): + lines[start:] = [ + "if not protocol.is_simulating():", + *(f" {line}" for line in lines[start:]), + ] + if any(attached.values()): + raise ValueError("Protocol ends with an attached tip.") + return lines + + +def render_python(plan: ProtocolPlan, *, profile: TargetProfile) -> str: + """Generate a standalone script without importing or running the robot SDK.""" + containers = {c.id: c for c in plan.containers} + for name in containers: + if not name.isidentifier() or keyword.iskeyword(name): + raise ValueError(f"Invalid container identifier: {name!r}") + samples = {s.id: s for s in plan.samples} + liquids = {} + pickups = None + lines = [] + + def load(name: str) -> None: + """Load a declared container on its module or deck slot.""" + container = containers[name] + if container.parent in ("source_module", "reaction_module"): + lines.append( + f"{name} = {container.parent}.load_labware({container.definition_id!r})" + ) + else: + lines.append( + f"{name} = protocol.load_labware({container.definition_id!r}, {container.parent!r})" + ) + + if isinstance(profile, (OpentronsAssemblyProfile, OpentronsTransformationProfile)): + lines.append( + f"source_module = protocol.load_module('temperature module', {profile.temperature_module_position!r})" + ) + load("sources") + lines.append("reaction_module = protocol.load_module('thermocycler module')") + load("reactions") + else: + lines.append("reaction_module = protocol.load_module('thermocyclerModuleV1')") + load("reactions") + + if isinstance(profile, OpentronsAssemblyProfile): + setup, pickups = _assembly_tips( + sum(isinstance(s, Transfer) and s.new_tip for s in plan.steps), profile + ) + lines.extend(setup) + capacities = {"pipette": profile.max_volume_ul} + for index, sample_id in enumerate(plan.input_sample_ids): + sample = samples[sample_id] + label = sample.liquid_label or sample.material.label + variable = f"liquid_{index}" + liquids[sample_id] = variable + lines.extend( + _liquid( + sample, + variable, + name=label, + description=label, + color=COLORS[index % len(COLORS)], + ) + ) + elif isinstance(profile, OpentronsTransformationProfile): + if "dna_plate" in containers: + load("dna_plate") + load("tube_rack") + starts = {"small": profile.initial_tip_p20, "large": profile.initial_tip_p300} + _validate_tips(plan, starts) + lines.extend( + _instruments( + profile.tiprack_p20_position, + profile.tiprack_p200_position, + starts, + filtered=False, + ) + ) + capacities = {"small": 20, "large": 200} + dna_liquids = {} + for sample_id in plan.input_sample_ids: + sample = samples[sample_id] + variable = f"liquid_{len(liquids)}" + if sample.role == "dna": + define = sample.material.identity not in dna_liquids + if define: + dna_liquids[sample.material.identity] = (variable, len(dna_liquids)) + variable, color_index = dna_liquids[sample.material.identity] + lines.extend( + _liquid( + sample, + variable, + name=sample.material.label, + description=f"{sample.material.label} DNA construct", + color=COLORS[color_index % len(COLORS)], + define=define, + ) + ) + else: + lines.extend( + _liquid( + sample, + variable, + name=sample.liquid_label, + color=COLORS[(sample.replicate - 1) % len(COLORS)], + ) + ) + liquids[sample_id] = variable + elif isinstance(profile, OpentronsPlatingProfile): + starts = { + "small": profile.initial_small_tip, + "large": profile.initial_large_tip, + } + _validate_tips(plan, starts) + lines.extend( + _instruments( + profile.small_tiprack_position, + profile.large_tiprack_position, + starts, + filtered=True, + ) + ) + capacities = {"small": 20, "large": 200} + load("tube_rack") + bacteria_index = 0 + for sample_id in plan.input_sample_ids: + sample = samples[sample_id] + variable = f"liquid_{len(liquids)}" + liquids[sample_id] = variable + if sample.role == "broth": + lines.extend( + _liquid( + sample, + variable, + name="liquid_broth", + description="Liquid broth for dilutions", + color="#D2B48C", + ) + ) + else: + lines.extend( + _liquid( + sample, + variable, + name="transformed_bacteria", + description=sample.liquid_label, + color=COLORS[bacteria_index % len(COLORS)], + ) + ) + bacteria_index += 1 + for name in sorted( + (name for name in containers if name.startswith(("dilution_", "agar_"))), + key=lambda name: (not name.startswith("dilution_"), name), + ): + load(name) + else: + raise TypeError(f"Unsupported profile: {type(profile).__name__}") + + lines.extend(_operations(plan, capacities, liquids, pickups)) + metadata = { + "protocolName": plan.id, + "author": "BuildCompiler", + "apiLevel": profile.api_level, + } + header = [ + "# Generated by BuildCompiler.", + "from opentrons import protocol_api", + "", + f"metadata = {metadata!r}", + "", + ] + if isinstance(profile, OpentronsPlatingProfile): + header.extend( + [ + "def _conical_source(well):", + ' """Follow the liquid surface, falling back to the default below 20%."""', + " try:", + " volume = well.current_liquid_volume()", + " if volume is None or volume < well.max_volume * 0.2:", + " return well", + " return well.bottom(max((volume / well.max_volume) * (well.depth - 10) - 10, 3))", + " except Exception:", + " return well", + "", + ] + ) + header.extend( + [ + "def run(protocol: protocol_api.ProtocolContext):", + ' """Load the deck and execute the compiled protocol in order."""', + ] + ) + return "\n".join([*header, *(f" {line}" for line in lines)]) + "\n" diff --git a/src/buildcompiler/protocols/backends/opentrons/__init__.py b/src/buildcompiler/protocols/backends/opentrons/__init__.py deleted file mode 100644 index 010ad0c..0000000 --- a/src/buildcompiler/protocols/backends/opentrons/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Opentrons support; the SDK is needed only in the simulation worker.""" diff --git a/src/buildcompiler/protocols/backends/opentrons/allocation.py b/src/buildcompiler/protocols/backends/opentrons/allocation.py deleted file mode 100644 index 4a12946..0000000 --- a/src/buildcompiler/protocols/backends/opentrons/allocation.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Deterministic allocation for the supported assembly hardware profile.""" - -from buildcompiler.protocols.allocation.models import ( - AllocatedProtocolPlan, - ContainerSpec, - SamplePlacement, - WellRef, -) -from buildcompiler.protocols.allocation.wells import BLOCK_24, PLATE_96 -from buildcompiler.protocols.plans import ProtocolPlan -from buildcompiler.protocols.backends.opentrons.profile import OpentronsAssemblyProfile - - -def allocate( - plan: ProtocolPlan, *, profile: OpentronsAssemblyProfile -) -> AllocatedProtocolPlan: - if len(plan.input_sample_ids) > BLOCK_24.capacity: - raise ValueError("Assembly source block capacity exceeded.") - if ( - len(plan.output_sample_ids) + profile.thermocycler_starting_well - > PLATE_96.capacity - ): - raise ValueError("Assembly reaction plate capacity exceeded.") - placements = tuple( - SamplePlacement( - sample_id=sample_id, - location=WellRef(container_id="sources", well_name=BLOCK_24.name(index)), - ) - for index, sample_id in enumerate(plan.input_sample_ids) - ) + tuple( - SamplePlacement( - sample_id=sample_id, - location=WellRef( - container_id="reactions", - well_name=PLATE_96.name(index + profile.thermocycler_starting_well), - ), - ) - for index, sample_id in enumerate(plan.output_sample_ids) - ) - return AllocatedProtocolPlan( - protocol=plan, - containers=( - ContainerSpec( - id="sources", - definition_id="opentrons_24_aluminumblock_nest_1.5ml_snapcap", - parent="source_module", - ), - ContainerSpec( - id="reactions", - definition_id="nest_96_wellplate_100ul_pcr_full_skirt", - parent="reaction_module", - ), - ), - placements=placements, - ) diff --git a/src/buildcompiler/protocols/backends/opentrons/lowering.py b/src/buildcompiler/protocols/backends/opentrons/lowering.py deleted file mode 100644 index ea9462b..0000000 --- a/src/buildcompiler/protocols/backends/opentrons/lowering.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Lower shared operations into explicit SDK calls, preserving PUDU ordering.""" - -from buildcompiler.protocols.backends.opentrons.operations import lower_operations - -from buildcompiler.protocols.allocation.models import AllocatedProtocolPlan -from buildcompiler.protocols.steps import ( - Transfer, -) -from buildcompiler.protocols.backends.opentrons.profile import OpentronsAssemblyProfile -from buildcompiler.protocols.backends.opentrons.program import ( - Call, - OpentronsProgram, - Reference, - SetStartingTip, -) -from buildcompiler.protocols.backends.opentrons.tips import schedule_tips - -from buildcompiler.protocols.backends.opentrons.palette import COLORS - - -def lower( - plan: AllocatedProtocolPlan, *, profile: OpentronsAssemblyProfile -) -> OpentronsProgram: - logical = plan.protocol - tips = schedule_tips( - sum(isinstance(s, Transfer) and s.new_tip for s in logical.steps), - profile=profile, - ) - instructions: list[Call | SetStartingTip] = [ - Call( - target="protocol", - method="load_module", - kwargs=( - ("module_name", "temperature module"), - ("location", profile.temperature_module_position), - ), - result="source_module", - ), - Call( - target="source_module", - method="load_labware", - args=(plan.containers[0].definition_id,), - result="sources", - ), - Call( - target="protocol", - method="load_module", - args=("thermocycler module",), - result="reaction_module", - ), - Call( - target="reaction_module", - method="load_labware", - args=(plan.containers[1].definition_id,), - result="reactions", - ), - *tips.setup, - Call( - target="protocol", - method="load_instrument", - args=("p20_single_gen2", profile.pipette_position), - kwargs=(("tip_racks", tuple(Reference(c.result) for c in tips.setup)),), - result="pipette", - ), - SetStartingTip(pipette="pipette", tip=Reference("tips_0", profile.initial_tip)), - ] - locations = { - placement.sample_id: placement.location for placement in plan.placements - } - samples = {sample.id: sample for sample in logical.samples} - for index, sample_id in enumerate(logical.input_sample_ids): - sample = samples[sample_id] - label = sample.liquid_label or sample.material.label - liquid_id = f"liquid_{index}" - instructions.extend( - ( - Call( - target="protocol", - method="define_liquid", - kwargs=( - ("name", label), - ("description", label), - ("display_color", COLORS[index % len(COLORS)]), - ), - result=liquid_id, - ), - Call( - target=f"well:{sample_id}", - method="load_liquid", - args=(Reference(liquid_id),), - kwargs=(("volume", sample.initial_volume_ul),), - ), - ) - ) - well_references = { - f"well:{sample_id}": Reference(location.container_id, location.well_name) - for sample_id, location in locations.items() - } - instructions.extend( - lower_operations( - plan, - capacities={"pipette": profile.max_volume_ul}, - pickups={"pipette": tips.pickups}, - ) - ) - return OpentronsProgram( - api_level=profile.api_level, - instructions=tuple(instructions), - wells=tuple(well_references.items()), - ) diff --git a/src/buildcompiler/protocols/backends/opentrons/operations.py b/src/buildcompiler/protocols/backends/opentrons/operations.py deleted file mode 100644 index b34021b..0000000 --- a/src/buildcompiler/protocols/backends/opentrons/operations.py +++ /dev/null @@ -1,231 +0,0 @@ -"""Shared lowering of liquid handling, tip lifecycle and module operations.""" - -import math -from collections.abc import Mapping - -from buildcompiler.protocols.allocation.models import AllocatedProtocolPlan -from buildcompiler.protocols.materials import SamplePoint -from buildcompiler.protocols.steps import ( - DeactivateSourceModule, - Distribute, - DropTip, - LidAction, - Mix, - OperatorAction, - PickUpTip, - RunTemperatureProgram, - SetTemperature, - Transfer, -) -from buildcompiler.protocols.backends.opentrons.program import Call, Record, Reference - - -def lower_operations( - plan: AllocatedProtocolPlan, - *, - capacities: Mapping[str, float], - pickups: Mapping[str, tuple[tuple[Call, ...], ...]] | None = None, -) -> tuple[Call, ...]: - locations = {p.sample_id: p.location for p in plan.placements} - instructions = [] - attached = {instrument: False for instrument in capacities} - tip_indices = dict.fromkeys(capacities, 0) - - def point(value: SamplePoint) -> Reference: - location = locations[value.sample_id] - return Reference( - location.container_id, - location.well_name, - value.bottom_mm, - value.top_mm, - value.track_conical_height, - ) - - def check_volume(instrument: str, *volumes: float) -> None: - for volume in volumes: - if not math.isfinite(volume) or not 0 <= volume <= capacities[instrument]: - raise ValueError( - f"Operation exceeds supported {instrument} pipette capacity." - ) - - def pickup(instrument: str) -> None: - if attached[instrument]: - raise ValueError(f"A tip is already attached to {instrument}.") - if pickups is None or instrument not in pickups: - instructions.append(Call(target=instrument, method="pick_up_tip")) - else: - instructions.extend(pickups[instrument][tip_indices[instrument]]) - tip_indices[instrument] += 1 - attached[instrument] = True - - def require_tip(instrument: str) -> None: - if not attached[instrument]: - raise ValueError(f"An attached tip is required on {instrument}.") - - def drop(instrument: str) -> None: - require_tip(instrument) - instructions.append(Call(target=instrument, method="drop_tip")) - attached[instrument] = False - - for step in plan.protocol.steps: - if isinstance(step, Transfer): - instrument = step.instrument - volume = ( - min(step.volume_ul, capacities[instrument]) - if step.limit_to_pipette_capacity - else step.volume_ul - ) - check_volume(instrument, volume, step.mix_before_ul) - source, destination = point(step.source), point(step.destination) - if step.new_tip: - pickup(instrument) - require_tip(instrument) - if step.mix_before_ul: - instructions.append( - Call( - target=instrument, - method="mix", - args=(step.mix_repetitions, step.mix_before_ul, source), - ) - ) - instructions.extend( - ( - Call( - target=instrument, - method="aspirate", - args=(volume, source), - kwargs=(("rate", step.aspiration_rate),), - ), - Call( - target=instrument, - method="dispense", - args=(volume, destination), - kwargs=(("rate", step.dispense_rate),), - ), - ) - ) - if step.blow_out: - instructions.append(Call(target=instrument, method="blow_out")) - if step.touch_tip: - instructions.append( - Call( - target=instrument, - method="touch_tip", - kwargs=(("radius", 0.5), ("v_offset", -14), ("speed", 20)), - ) - ) - if step.drop_tip: - drop(instrument) - elif isinstance(step, PickUpTip): - pickup(step.instrument) - elif isinstance(step, DropTip): - drop(step.instrument) - elif isinstance(step, Mix): - require_tip(step.instrument) - check_volume(step.instrument, step.volume_ul) - instructions.append( - Call( - target=step.instrument, - method="mix", - kwargs=( - ("repetitions", step.repetitions), - ("volume", step.volume_ul), - ("location", point(step.location)), - ), - ) - ) - elif isinstance(step, Distribute): - check_volume( - step.instrument, - step.volume_ul + step.disposal_volume_ul + (step.air_gap_ul or 0), - ) - if step.mix_before: - check_volume(step.instrument, step.mix_before[1]) - if step.new_tip == "never": - require_tip(step.instrument) - elif step.new_tip != "once" or attached[step.instrument]: - raise ValueError( - "A distribution with new_tip='once' requires an empty pipette." - ) - kwargs = [ - ("volume", step.volume_ul), - ("source", point(step.source)), - ("dest", tuple(point(d) for d in step.destinations)), - ("disposal_volume", step.disposal_volume_ul), - ("new_tip", step.new_tip), - ] - if step.mix_before: - kwargs.append(("mix_before", step.mix_before)) - if step.air_gap_ul is not None: - kwargs.append(("air_gap", step.air_gap_ul)) - instructions.append( - Call(target=step.instrument, method="distribute", kwargs=tuple(kwargs)) - ) - if step.track_liquid_sample_id: - for destination in step.destinations: - instructions.append( - Call( - target=f"well:{destination.sample_id}", - method="load_liquid", - kwargs=( - ( - "liquid", - Reference(f"liquid:{step.track_liquid_sample_id}"), - ), - ("volume", step.volume_ul), - ), - ) - ) - elif isinstance(step, LidAction): - instructions.append( - Call(target="reaction_module", method=f"{step.action}_lid") - ) - elif isinstance(step, SetTemperature): - target, method = { - "source": ("source_module", "set_temperature"), - "reaction": ("reaction_module", "set_block_temperature"), - "lid": ("reaction_module", "set_lid_temperature"), - }[step.module] - instructions.append( - Call( - target=target, - method=method, - args=(step.celsius,), - skip_during_simulation=step.skip_during_simulation, - ) - ) - elif isinstance(step, DeactivateSourceModule): - instructions.append(Call(target="source_module", method="deactivate")) - elif isinstance(step, RunTemperatureProgram): - instructions.append( - Call( - target="reaction_module", - method="execute_profile", - kwargs=( - ( - "steps", - tuple( - Record( - ( - ("temperature", s.celsius), - ("hold_time_minutes", s.minutes), - ) - ) - for s in step.program.steps - ), - ), - ("repetitions", step.program.repetitions), - ("block_max_volume", step.program.block_max_volume_ul), - ), - skip_during_simulation=step.skip_during_simulation, - ) - ) - elif isinstance(step, OperatorAction): - instructions.append( - Call(target="protocol", method="comment", args=(step.instruction,)) - ) - else: - raise TypeError(f"Unsupported protocol operation: {type(step).__name__}") - if any(attached.values()): - raise ValueError("Protocol ends with an attached tip.") - return tuple(instructions) diff --git a/src/buildcompiler/protocols/backends/opentrons/palette.py b/src/buildcompiler/protocols/backends/opentrons/palette.py deleted file mode 100644 index eedfb05..0000000 --- a/src/buildcompiler/protocols/backends/opentrons/palette.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Shared display metadata, independent of sample identity.""" - -# PUDU display palette (MIT, Rudge Lab); display metadata never controls identity. -COLORS = ( - "#4040BF", - "#BF4040", - "#40BF40", - "#A640BF", - "#BFBF40", - "#BF7340", - "#40BFBF", - "#BF40A6", - "#73BF40", - "#4073BF", - "#BF8C40", - "#40BF73", - "#7340BF", - "#A6BF40", - "#BF5940", - "#40A6BF", - "#BF4073", - "#59BF40", - "#BFA640", - "#40BFA6", - "#8CBF40", - "#40BF59", - "#40BF8C", - "#BF40A6", -) diff --git a/src/buildcompiler/protocols/backends/opentrons/plating.py b/src/buildcompiler/protocols/backends/opentrons/plating.py deleted file mode 100644 index feb667e..0000000 --- a/src/buildcompiler/protocols/backends/opentrons/plating.py +++ /dev/null @@ -1,191 +0,0 @@ -"""Plating resource allocation and setup, using shared liquid handling lowering.""" - -from collections import Counter - -from buildcompiler.protocols.allocation.models import ( - AllocatedProtocolPlan, - ContainerSpec, - OutputManifest, - SamplePlacement, - WellRef, -) -from buildcompiler.protocols.allocation.wells import PLATE_96, WellGrid -from buildcompiler.protocols.plans import ProtocolPlan -from buildcompiler.protocols.backends.opentrons.palette import COLORS -from buildcompiler.protocols.backends.opentrons.operations import lower_operations -from buildcompiler.protocols.backends.opentrons.profile import OpentronsPlatingProfile -from buildcompiler.protocols.backends.opentrons.program import ( - Call, - OpentronsProgram, - Reference, - SetStartingTip, -) -from buildcompiler.protocols.backends.opentrons.setup import ( - load_liquid, - validate_tip_capacity, - well_aliases, -) - - -def allocate_plating( - plan: ProtocolPlan, *, profile: OpentronsPlatingProfile, inputs: OutputManifest -) -> AllocatedProtocolPlan: - upstream = {p.sample_id: p.location for p in inputs.placements} - selected = [s for s in plan.samples if s.role == "bacteria"] - if len({upstream[s.source_sample_id].container_id for s in selected}) != 1: - raise ValueError("The plating target accepts one source plate.") - containers = [ - ContainerSpec( - id="reactions", - definition_id=profile.thermocycler_labware, - parent="reaction_module", - ), - ContainerSpec( - id="tube_rack", - definition_id="opentrons_15_tuberack_falcon_15ml_conical", - parent=profile.tube_rack_position, - ), - ] - counts = Counter((s.role, s.dilution) for s in plan.samples) - indices = Counter() - placements = [] - for sample in plan.samples: - if sample.role == "broth": - location = WellRef( - container_id="tube_rack", - well_name=WellGrid(rows=3, columns=5).name(profile.lb_tube_position), - ) - if sample.initial_volume_ul > 15000: - raise ValueError("Broth loading exceeds tube capacity.") - elif sample.role == "bacteria": - well = upstream[sample.source_sample_id].well_name - PLATE_96.index(well) - location = WellRef(container_id="reactions", well_name=well) - elif sample.role in ("dilution", "agar"): - per_dilution = counts[sample.role, sample.dilution] - if per_dilution > 96: - raise ValueError("A dilution exceeds plate capacity.") - two_plates = counts[sample.role, 2] and per_dilution > 48 - plate = sample.dilution if two_plates else 1 - index = indices[sample.role, sample.dilution] - indices[sample.role, sample.dilution] += 1 - if sample.dilution == 2 and not two_plates: - index += 48 - name = f"{sample.role}_{plate}" - if name not in {c.id for c in containers}: - containers.append( - ContainerSpec( - id=name, - definition_id="nest_96_wellplate_100ul_pcr_full_skirt", - parent=getattr(profile, f"{sample.role}_plate_position{plate}"), - ) - ) - location = WellRef(container_id=name, well_name=PLATE_96.name(index)) - else: - raise ValueError(f"Unknown plating sample role {sample.role!r}.") - placements.append(SamplePlacement(sample_id=sample.id, location=location)) - return AllocatedProtocolPlan( - protocol=plan, containers=tuple(containers), placements=tuple(placements) - ) - - -def lower_plating( - plan: AllocatedProtocolPlan, *, profile: OpentronsPlatingProfile -) -> OpentronsProgram: - validate_tip_capacity( - plan, - starts={"small": profile.initial_small_tip, "large": profile.initial_large_tip}, - ) - calls = [ - Call( - target="protocol", - method="load_module", - args=("thermocyclerModuleV1",), - result="reaction_module", - ), - Call( - target="reaction_module", - method="load_labware", - args=(profile.thermocycler_labware,), - result="reactions", - ), - Call( - target="protocol", - method="load_labware", - args=("opentrons_96_filtertiprack_20ul", profile.small_tiprack_position), - result="small_tips", - ), - Call( - target="protocol", - method="load_labware", - args=("opentrons_96_filtertiprack_200ul", profile.large_tiprack_position), - result="large_tips", - ), - ] - for instrument, pipette, mount, tip in ( - ("small", "p20_single_gen2", "left", profile.initial_small_tip), - ("large", "p300_single_gen2", "right", profile.initial_large_tip), - ): - calls.append( - Call( - target="protocol", - method="load_instrument", - args=(pipette, mount), - kwargs=(("tip_racks", (Reference(f"{instrument}_tips"),)),), - result=instrument, - ) - ) - if tip: - calls.append( - SetStartingTip( - pipette=instrument, tip=Reference(f"{instrument}_tips", tip) - ) - ) - tube = next(c for c in plan.containers if c.id == "tube_rack") - calls.append( - Call( - target="protocol", - method="load_labware", - args=(tube.definition_id, tube.parent), - result=tube.id, - ) - ) - bacteria_index = 0 - for sample in plan.protocol.samples: - if sample.role == "broth": - calls.extend( - load_liquid( - sample.id, - name="liquid_broth", - description="Liquid broth for dilutions", - color="#D2B48C", - volume=sample.initial_volume_ul, - ) - ) - elif sample.role == "bacteria": - calls.extend( - load_liquid( - sample.id, - name="transformed_bacteria", - description=sample.liquid_label, - color=COLORS[bacteria_index % len(COLORS)], - volume=sample.initial_volume_ul, - ) - ) - bacteria_index += 1 - for container in sorted( - (c for c in plan.containers if c.id.startswith(("dilution_", "agar_"))), - key=lambda c: (not c.id.startswith("dilution_"), c.id), - ): - calls.append( - Call( - target="protocol", - method="load_labware", - args=(container.definition_id, container.parent), - result=container.id, - ) - ) - calls.extend(lower_operations(plan, capacities={"small": 20, "large": 200})) - return OpentronsProgram( - api_level=profile.api_level, instructions=tuple(calls), wells=well_aliases(plan) - ) diff --git a/src/buildcompiler/protocols/backends/opentrons/profile.py b/src/buildcompiler/protocols/backends/opentrons/profile.py deleted file mode 100644 index 96d8eea..0000000 --- a/src/buildcompiler/protocols/backends/opentrons/profile.py +++ /dev/null @@ -1,131 +0,0 @@ -"""The initial, explicitly supported PUDU assembly hardware profile.""" - -from dataclasses import dataclass - -from buildcompiler.protocols.allocation.wells import PLATE_96 - - -@dataclass(frozen=True, slots=True, kw_only=True) -class OpentronsAssemblyProfile: - api_level: str = "2.21" - temperature_module_position: str = "1" - thermocycler_starting_well: int = 0 - tiprack_positions: tuple[str, ...] = ("2", "3", "4", "5", "6", "9") - pipette_position: str = "left" - initial_tip: str = "A1" - - def __post_init__(self) -> None: - if self.api_level != "2.21": - raise ValueError("This backend currently supports API 2.21.") - if self.pipette_position not in ("left", "right"): - raise ValueError("Invalid pipette mount.") - PLATE_96.name(self.thermocycler_starting_well) - PLATE_96.index(self.initial_tip) - if not isinstance(self.tiprack_positions, tuple) or not self.tiprack_positions: - raise ValueError("tiprack_positions must be a nonempty tuple.") - slots = (self.temperature_module_position, *self.tiprack_positions) - if len(set(slots)) != len(slots) or not set(slots) <= { - "1", - "2", - "3", - "4", - "5", - "6", - "9", - }: - raise ValueError("Deck slots collide or overlap the thermocycler/trash.") - - @property - def max_volume_ul(self) -> float: - return 20 - - -def validate_slots(slots: tuple[str, ...]) -> None: - if len(set(slots)) != len(slots) or not set(slots) <= { - "1", - "2", - "3", - "4", - "5", - "6", - "9", - }: - raise ValueError("Deck slots collide or overlap the thermocycler/trash.") - - -@dataclass(frozen=True, slots=True, kw_only=True) -class OpentronsTransformationProfile: - api_level: str = "2.21" - temperature_module_position: str = "1" - dna_plate_position: str = "2" - tube_rack_position: str = "3" - tiprack_p20_position: str = "9" - tiprack_p200_position: str = "6" - thermocycler_starting_well: int = 0 - initial_dna_well: int = 0 - initial_tip_p20: str | None = None - initial_tip_p300: str | None = None - - def __post_init__(self) -> None: - from buildcompiler.protocols.allocation.wells import BLOCK_24 - - if self.api_level != "2.21": - raise ValueError("This backend currently supports API 2.21.") - PLATE_96.name(self.thermocycler_starting_well) - BLOCK_24.name(self.initial_dna_well) - for tip in (self.initial_tip_p20, self.initial_tip_p300): - if tip is not None: - PLATE_96.index(tip) - validate_slots( - ( - self.temperature_module_position, - self.dna_plate_position, - self.tube_rack_position, - self.tiprack_p20_position, - self.tiprack_p200_position, - ) - ) - - -@dataclass(frozen=True, slots=True, kw_only=True) -class OpentronsPlatingProfile: - api_level: str = "2.21" - thermocycler_labware: str = "biorad_96_wellplate_200ul_pcr" - small_tiprack_position: str = "9" - large_tiprack_position: str = "1" - initial_small_tip: str | None = None - initial_large_tip: str | None = None - dilution_plate_position1: str = "2" - dilution_plate_position2: str = "3" - agar_plate_position1: str = "5" - agar_plate_position2: str = "6" - tube_rack_position: str = "4" - lb_tube_position: int = 0 - - def __post_init__(self) -> None: - if self.api_level != "2.21": - raise ValueError("This backend currently supports API 2.21.") - if self.thermocycler_labware not in ( - "biorad_96_wellplate_200ul_pcr", - "nest_96_wellplate_100ul_pcr_full_skirt", - ): - raise ValueError("Unsupported plating source labware.") - if ( - type(self.lb_tube_position) is not int - or not 0 <= self.lb_tube_position < 15 - ): - raise ValueError("lb_tube_position must index the 15-tube rack.") - for tip in (self.initial_small_tip, self.initial_large_tip): - if tip is not None: - PLATE_96.index(tip) - validate_slots( - ( - self.small_tiprack_position, - self.large_tiprack_position, - self.dilution_plate_position1, - self.dilution_plate_position2, - self.agar_plate_position1, - self.agar_plate_position2, - self.tube_rack_position, - ) - ) diff --git a/src/buildcompiler/protocols/backends/opentrons/program.py b/src/buildcompiler/protocols/backends/opentrons/program.py deleted file mode 100644 index ede2aa9..0000000 --- a/src/buildcompiler/protocols/backends/opentrons/program.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Immutable SDK call records produced by lowering; never live SDK objects.""" - -from dataclasses import dataclass - - -@dataclass(frozen=True, slots=True) -class Reference: - name: str - well: str | None = None - bottom_mm: float | None = None - top_mm: float | None = None - track_conical_height: bool = False - - -@dataclass(frozen=True, slots=True) -class Record: - entries: tuple[tuple[str, object], ...] - - -@dataclass(frozen=True, slots=True, kw_only=True) -class Call: - target: str - method: str - args: tuple = () - kwargs: tuple[tuple[str, object], ...] = () - result: str | None = None - skip_during_simulation: bool = False - - -@dataclass(frozen=True, slots=True, kw_only=True) -class SetStartingTip: - pipette: str - tip: Reference - - -@dataclass(frozen=True, slots=True, kw_only=True) -class OpentronsProgram: - api_level: str - instructions: tuple[Call | SetStartingTip, ...] - wells: tuple[tuple[str, Reference], ...] = () diff --git a/src/buildcompiler/protocols/backends/opentrons/rendering.py b/src/buildcompiler/protocols/backends/opentrons/rendering.py deleted file mode 100644 index a3a7bbb..0000000 --- a/src/buildcompiler/protocols/backends/opentrons/rendering.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Emit standalone Python with literal data and a closed set of SDK methods.""" - -import keyword -import math - -from buildcompiler.protocols.attribution import PUDU_LICENSE -from buildcompiler.protocols.backends.opentrons.program import ( - Call, - OpentronsProgram, - Record, - Reference, - SetStartingTip, -) - -METHODS = frozenset( - { - "load_module", - "load_labware", - "load_instrument", - "define_liquid", - "load_liquid", - "pick_up_tip", - "drop_tip", - "mix", - "aspirate", - "dispense", - "distribute", - "blow_out", - "touch_tip", - "open_lid", - "close_lid", - "set_temperature", - "set_block_temperature", - "set_lid_temperature", - "deactivate", - "execute_profile", - "comment", - "move_labware", - } -) - - -def _literal(value: object) -> str: - if isinstance(value, Reference): - expression = f"resources[{value.name!r}]" - if value.well is not None: - expression += f"[{value.well!r}]" - if value.bottom_mm is not None: - expression += f".bottom({_literal(value.bottom_mm)})" - if value.top_mm is not None: - expression += f".top({_literal(value.top_mm)})" - if value.track_conical_height: - expression = f"_conical_source({expression})" - return expression - if isinstance(value, Record): - return ( - "{" - + ", ".join(f"{key!r}: {_literal(item)}" for key, item in value.entries) - + "}" - ) - if isinstance(value, tuple): - return "[" + ", ".join(_literal(item) for item in value) + "]" - if value is None or type(value) in (str, bool, int): - return repr(value) - if type(value) is float and math.isfinite(value): - return repr(value) - raise TypeError(f"Unsupported program literal: {type(value).__name__}") - - -def render_python(program: OpentronsProgram, *, name: str) -> str: - """Return source; importing or calling this function performs no SDK work.""" - metadata = { - "protocolName": name, - "author": "BuildCompiler", - "apiLevel": program.api_level, - } - lines = [ - *(f"# {line}" for line in PUDU_LICENSE.splitlines()), - "# Generated by BuildCompiler. Protocol behavior derived from PUDU (MIT).", - "from opentrons import protocol_api", - "", - f"metadata = {metadata!r}", - "", - "def _conical_source(well):", - " # PUDU SmartPipette's conical-tube policy; requires API liquid tracking.", - " try:", - " volume = well.current_liquid_volume()", - " if volume is None or volume < well.max_volume * 0.2:", - " return well", - " return well.bottom(max((volume / well.max_volume) * (well.depth - 10) - 10, 3))", - " except Exception:", - " return well", - "", - "def run(protocol: protocol_api.ProtocolContext):", - " resources = {'protocol': protocol, 'OFF_DECK': protocol_api.OFF_DECK}", - ] - wells = dict(program.wells) - for instruction in program.instructions: - if isinstance(instruction, SetStartingTip): - lines.append( - f" resources[{instruction.pipette!r}].starting_tip = {_literal(instruction.tip)}" - ) - continue - if not isinstance(instruction, Call) or instruction.method not in METHODS: - raise ValueError("Unsupported SDK instruction.") - arguments = [_literal(value) for value in instruction.args] - for key, value in instruction.kwargs: - if not key.isidentifier() or keyword.iskeyword(key): - raise ValueError(f"Invalid keyword: {key!r}") - arguments.append(f"{key}={_literal(value)}") - target = _literal(wells.get(instruction.target, Reference(instruction.target))) - expression = f"{target}.{instruction.method}({', '.join(arguments)})" - if instruction.result is not None: - expression = f"resources[{instruction.result!r}] = {expression}" - if instruction.skip_during_simulation: - lines.append(" if not protocol.is_simulating():") - lines.append(f" {expression}") - else: - lines.append(f" {expression}") - return "\n".join(lines) + "\n" diff --git a/src/buildcompiler/protocols/backends/opentrons/setup.py b/src/buildcompiler/protocols/backends/opentrons/setup.py deleted file mode 100644 index 6358a63..0000000 --- a/src/buildcompiler/protocols/backends/opentrons/setup.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Shared resource loading and validation for Opentrons targets.""" - -from buildcompiler.protocols.allocation.models import AllocatedProtocolPlan -from buildcompiler.protocols.allocation.wells import PLATE_96 -from buildcompiler.protocols.steps import Distribute, PickUpTip, Transfer -from buildcompiler.protocols.backends.opentrons.program import Call, Reference - - -def well_aliases(plan: AllocatedProtocolPlan) -> tuple[tuple[str, Reference], ...]: - return tuple( - ( - f"well:{p.sample_id}", - Reference(p.location.container_id, p.location.well_name), - ) - for p in plan.placements - ) - - -def load_liquid( - sample_id: str, - *, - name: str, - color: str, - volume: float, - description: str | None = None, - liquid_id: str | None = None, - define: bool = True, -) -> tuple[Call, ...]: - liquid_id = liquid_id or f"liquid:{sample_id}" - calls = [] - if define: - kwargs = [("name", name), ("display_color", color)] - if description is not None: - kwargs.append(("description", description)) - calls.append( - Call( - target="protocol", - method="define_liquid", - kwargs=tuple(kwargs), - result=liquid_id, - ) - ) - calls.append( - Call( - target=f"well:{sample_id}", - method="load_liquid", - kwargs=(("liquid", Reference(liquid_id)), ("volume", volume)), - ) - ) - return tuple(calls) - - -def validate_tip_capacity( - plan: AllocatedProtocolPlan, *, starts: dict[str, str | None] -) -> None: - counts = dict.fromkeys(starts, 0) - for step in plan.protocol.steps: - if ( - isinstance(step, PickUpTip) - or isinstance(step, Transfer) - and step.new_tip - or isinstance(step, Distribute) - and step.new_tip == "once" - ): - counts[step.instrument] += 1 - for instrument, count in counts.items(): - available = 96 - PLATE_96.index(starts[instrument] or "A1") - if count > available: - raise ValueError( - f"{instrument} requires {count} tips but only {available} remain." - ) diff --git a/src/buildcompiler/protocols/backends/opentrons/tips.py b/src/buildcompiler/protocols/backends/opentrons/tips.py deleted file mode 100644 index 2e43118..0000000 --- a/src/buildcompiler/protocols/backends/opentrons/tips.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Explicit tip schedules and rack replacement boundaries.""" - -from dataclasses import dataclass - -from buildcompiler.protocols.allocation.wells import PLATE_96 -from buildcompiler.protocols.backends.opentrons.profile import OpentronsAssemblyProfile -from buildcompiler.protocols.backends.opentrons.program import Call, Reference - - -@dataclass(frozen=True, slots=True, kw_only=True) -class TipSchedule: - setup: tuple[Call, ...] - pickups: tuple[tuple[Call, ...], ...] - - -def schedule_tips(count: int, *, profile: OpentronsAssemblyProfile) -> TipSchedule: - """Assign exact tips, accounting for a partially used first rack.""" - first = PLATE_96.index(profile.initial_tip) - rack_count = (first + count + 95) // 96 - slots = profile.tiprack_positions - setup = tuple( - Call( - target="protocol", - method="load_labware", - args=( - "opentrons_96_tiprack_20ul", - slots[index] if index < len(slots) else Reference("OFF_DECK"), - ), - result=f"tips_{index}", - ) - for index in range(rack_count) - ) - pickups = [] - active_batch = 0 - for index in range(first, first + count): - rack, well = divmod(index, 96) - batch = rack // len(slots) - calls = [] - if batch != active_batch: - for old in range( - active_batch * len(slots), - min((active_batch + 1) * len(slots), rack_count), - ): - calls.append( - Call( - target="protocol", - method="move_labware", - kwargs=( - ("labware", Reference(f"tips_{old}")), - ("new_location", Reference("OFF_DECK")), - ), - ) - ) - for new in range( - batch * len(slots), min((batch + 1) * len(slots), rack_count) - ): - calls.append( - Call( - target="protocol", - method="move_labware", - kwargs=( - ("labware", Reference(f"tips_{new}")), - ("new_location", slots[new % len(slots)]), - ), - ) - ) - active_batch = batch - calls.append( - Call( - target="pipette", - method="pick_up_tip", - args=(Reference(f"tips_{rack}", PLATE_96.name(well)),), - ) - ) - pickups.append(tuple(calls)) - return TipSchedule(setup=setup, pickups=tuple(pickups)) diff --git a/src/buildcompiler/protocols/backends/opentrons/transformation.py b/src/buildcompiler/protocols/backends/opentrons/transformation.py deleted file mode 100644 index c0e575f..0000000 --- a/src/buildcompiler/protocols/backends/opentrons/transformation.py +++ /dev/null @@ -1,220 +0,0 @@ -"""Transformation allocation and resource setup; method ordering lives elsewhere.""" - -from buildcompiler.protocols.allocation.models import ( - AllocatedProtocolPlan, - ContainerSpec, - OutputManifest, - SamplePlacement, - WellRef, -) -from buildcompiler.protocols.allocation.wells import BLOCK_24, PLATE_96 -from buildcompiler.protocols.plans import ProtocolPlan -from buildcompiler.protocols.backends.opentrons.palette import COLORS -from buildcompiler.protocols.backends.opentrons.operations import lower_operations -from buildcompiler.protocols.backends.opentrons.profile import ( - OpentronsTransformationProfile, -) -from buildcompiler.protocols.backends.opentrons.program import ( - Call, - OpentronsProgram, - Reference, - SetStartingTip, -) -from buildcompiler.protocols.backends.opentrons.setup import ( - load_liquid, - validate_tip_capacity, - well_aliases, -) - - -def allocate_transformation( - plan: ProtocolPlan, - *, - profile: OpentronsTransformationProfile, - inputs: OutputManifest | None = None, -) -> AllocatedProtocolPlan: - containers = [ - ContainerSpec( - id="sources", - definition_id="opentrons_24_aluminumblock_nest_1.5ml_snapcap", - parent="source_module", - ), - ContainerSpec( - id="reactions", - definition_id="nest_96_wellplate_100ul_pcr_full_skirt", - parent="reaction_module", - ), - ] - upstream = {} - if inputs is not None: - if len({p.location.container_id for p in inputs.placements}) != 1: - raise ValueError("The transformation target accepts one source DNA plate.") - containers.append( - ContainerSpec( - id="dna_plate", - definition_id="nest_96_wellplate_100ul_pcr_full_skirt", - parent=profile.dna_plate_position, - ) - ) - upstream = {p.sample_id: p.location for p in inputs.placements} - containers.append( - ContainerSpec( - id="tube_rack", - definition_id="opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap", - parent=profile.tube_rack_position, - ) - ) - placements = [] - dna_index, tube_index, reaction_index = ( - profile.initial_dna_well, - 0, - profile.thermocycler_starting_well, - ) - for sample in plan.samples: - if sample.role == "dna": - if inputs is None: - location = WellRef( - container_id="sources", well_name=BLOCK_24.name(dna_index) - ) - dna_index += 1 - else: - well = upstream[sample.source_sample_id].well_name - PLATE_96.index(well) - location = WellRef(container_id="dna_plate", well_name=well) - elif sample.role in ("cells", "media"): - location = WellRef( - container_id="tube_rack", well_name=BLOCK_24.name(tube_index) - ) - tube_index += 1 - if sample.initial_volume_ul > 1500: - raise ValueError("Reagent loading exceeds tube capacity.") - elif sample.role == "reaction": - location = WellRef( - container_id="reactions", well_name=PLATE_96.name(reaction_index) - ) - reaction_index += 1 - else: - raise ValueError(f"Unknown transformation sample role {sample.role!r}.") - placements.append(SamplePlacement(sample_id=sample.id, location=location)) - return AllocatedProtocolPlan( - protocol=plan, containers=tuple(containers), placements=tuple(placements) - ) - - -def lower_transformation( - plan: AllocatedProtocolPlan, *, profile: OpentronsTransformationProfile -) -> OpentronsProgram: - validate_tip_capacity( - plan, - starts={"small": profile.initial_tip_p20, "large": profile.initial_tip_p300}, - ) - containers = {c.id: c for c in plan.containers} - calls = [ - Call( - target="protocol", - method="load_module", - args=("temperature module", profile.temperature_module_position), - result="source_module", - ), - Call( - target="source_module", - method="load_labware", - args=(containers["sources"].definition_id,), - result="sources", - ), - Call( - target="protocol", - method="load_module", - args=("thermocycler module",), - result="reaction_module", - ), - Call( - target="reaction_module", - method="load_labware", - args=(containers["reactions"].definition_id,), - result="reactions", - ), - ] - for name in ("dna_plate", "tube_rack"): - if name in containers: - container = containers[name] - calls.append( - Call( - target="protocol", - method="load_labware", - args=(container.definition_id, container.parent), - result=name, - ) - ) - calls.extend( - ( - Call( - target="protocol", - method="load_labware", - args=("opentrons_96_tiprack_20ul", profile.tiprack_p20_position), - result="small_tips", - ), - Call( - target="protocol", - method="load_labware", - args=( - "opentrons_96_filtertiprack_200ul", - profile.tiprack_p200_position, - ), - result="large_tips", - ), - ) - ) - for instrument, pipette, mount, tip in ( - ("small", "p20_single_gen2", "left", profile.initial_tip_p20), - ("large", "p300_single_gen2", "right", profile.initial_tip_p300), - ): - calls.append( - Call( - target="protocol", - method="load_instrument", - args=(pipette, mount), - kwargs=(("tip_racks", (Reference(f"{instrument}_tips"),)),), - result=instrument, - ) - ) - if tip: - calls.append( - SetStartingTip( - pipette=instrument, tip=Reference(f"{instrument}_tips", tip) - ) - ) - dna_liquids = {} - for sample in plan.protocol.samples: - if sample.role == "dna": - define = sample.material.identity not in dna_liquids - if define: - dna_liquids[sample.material.identity] = ( - f"liquid:{sample.id}", - len(dna_liquids), - ) - liquid_id, color_index = dna_liquids[sample.material.identity] - calls.extend( - load_liquid( - sample.id, - name=sample.material.label, - description=f"{sample.material.label} DNA construct", - color=COLORS[color_index % len(COLORS)], - volume=sample.initial_volume_ul, - liquid_id=liquid_id, - define=define, - ) - ) - elif sample.role in ("cells", "media"): - calls.extend( - load_liquid( - sample.id, - name=sample.liquid_label, - color=COLORS[(sample.replicate - 1) % len(COLORS)], - volume=sample.initial_volume_ul, - ) - ) - calls.extend(lower_operations(plan, capacities={"small": 20, "large": 200})) - return OpentronsProgram( - api_level=profile.api_level, instructions=tuple(calls), wells=well_aliases(plan) - ) diff --git a/src/buildcompiler/protocols/backends/opentrons/simulation.py b/src/buildcompiler/protocols/backends/simulation.py similarity index 92% rename from src/buildcompiler/protocols/backends/opentrons/simulation.py rename to src/buildcompiler/protocols/backends/simulation.py index fcc7f4f..0cd9d57 100644 --- a/src/buildcompiler/protocols/backends/opentrons/simulation.py +++ b/src/buildcompiler/protocols/backends/simulation.py @@ -1,4 +1,4 @@ -"""Run the real simulator in an isolated process and temporary working directory.""" +"""Run Opentrons simulation in an isolated process and temporary directory.""" import json import os @@ -15,6 +15,8 @@ class SimulationError(RuntimeError): @dataclass(frozen=True, slots=True, kw_only=True) class SimulationResult: + """Structured robot actions, observed SDK calls and captured process output.""" + simulator_version: str actions: tuple[dict, ...] sdk_calls: tuple[dict, ...] @@ -33,7 +35,7 @@ def simulate_source( """Simulate explicitly supplied source; never called during compilation. Source must be trusted just like any Python protocol. Process isolation - prevents SDK state and PUDU's simulation file writes leaking between runs; + prevents SDK state and protocol file writes leaking between runs; it is not a security sandbox for arbitrary Python. """ worker = Path(__file__).with_name("_simulation_worker.py") diff --git a/src/buildcompiler/protocols/compiler.py b/src/buildcompiler/protocols/compiler.py index fd491ed..036528f 100644 --- a/src/buildcompiler/protocols/compiler.py +++ b/src/buildcompiler/protocols/compiler.py @@ -1,71 +1,94 @@ -"""Compilation facade; planning, rendering, writing and simulation are separate.""" +"""Compile requests into plans, review documents and standalone robot scripts.""" import json +from collections.abc import Mapping from dataclasses import asdict, dataclass +from pathlib import Path +from types import MappingProxyType from buildcompiler.domain.protocol_requests import ( AssemblyRequest, - TransformationRequest, PlatingRequest, ProtocolRequest, + TransformationRequest, ) - -from buildcompiler.protocols.allocation.models import ( - AllocatedProtocolPlan, - OutputManifest, -) -from buildcompiler.protocols.allocation.plating import plating_layout -from buildcompiler.protocols.artifacts import ArtifactBundle, ProtocolArtifact -from buildcompiler.protocols.attribution import PUDU_REVISION from buildcompiler.protocols.backends.markdown import render_markdown -from buildcompiler.protocols.backends.opentrons.allocation import allocate -from buildcompiler.protocols.backends.opentrons.lowering import lower -from buildcompiler.protocols.backends.opentrons.profile import ( +from buildcompiler.protocols.backends.opentrons import TargetProfile, render_python +from buildcompiler.protocols.methods.assembly import ( + AssemblyConfig, OpentronsAssemblyProfile, - OpentronsTransformationProfile, - OpentronsPlatingProfile, + allocate_assembly, + plan_assembly, ) -from buildcompiler.protocols.backends.opentrons.plating import ( +from buildcompiler.protocols.methods.plating import ( + OpentronsPlatingProfile, + PlatingConfig, allocate_plating, - lower_plating, + plan_plating, + plating_layout, ) -from buildcompiler.protocols.backends.opentrons.transformation import ( +from buildcompiler.protocols.methods.transformation import ( + OpentronsTransformationProfile, + TransformationConfig, allocate_transformation, - lower_transformation, + plan_transformation, ) -from buildcompiler.protocols.backends.opentrons.program import OpentronsProgram -from buildcompiler.protocols.backends.opentrons.rendering import render_python -from buildcompiler.protocols.config import AssemblyConfig -from buildcompiler.protocols.methods.assembly import plan_assembly -from buildcompiler.protocols.methods.transformation import plan_transformation -from buildcompiler.protocols.methods.transformation_config import TransformationConfig -from buildcompiler.protocols.methods.plating import plan_plating -from buildcompiler.protocols.methods.plating_config import PlatingConfig -from buildcompiler.protocols.plans import ProtocolPlan +from buildcompiler.protocols.models import OutputManifest, ProtocolPlan + + +def _json(value: object) -> str: + """Use the same deterministic encoding for every JSON artifact.""" + return json.dumps(value, indent=2, sort_keys=True, allow_nan=False) + "\n" @dataclass(frozen=True, slots=True, kw_only=True) class CompiledProtocol: - allocation: AllocatedProtocolPlan - program: OpentronsProgram + """A located plan and its files; compilation itself performs no file writes.""" + + plan: ProtocolPlan script: str markdown: str manifest: OutputManifest - artifacts: ArtifactBundle + files: Mapping[str, str] + + def __post_init__(self) -> None: + for name in self.files: + if ( + not name + or name in (".", "..") + or Path(name).name != name + or "\\" in name + ): + raise ValueError("Artifact names must be plain filenames.") + object.__setattr__(self, "files", MappingProxyType(dict(self.files))) + + def write( + self, directory: str | Path, *, overwrite: bool = False + ) -> tuple[Path, ...]: + """Write the complete bundle, refusing existing files unless requested.""" + directory = Path(directory) + paths = tuple(directory / name for name in self.files) + if not overwrite and any(path.exists() for path in paths): + raise FileExistsError("One or more output artifacts already exist.") + directory.mkdir(parents=True, exist_ok=True) + for path, content in zip(paths, self.files.values(), strict=True): + with path.open("w" if overwrite else "x", encoding="utf-8") as handle: + handle.write(content) + return paths @dataclass(frozen=True, slots=True, kw_only=True) class ProtocolCompiler: + """Select a method, place its samples and render both outputs from one plan.""" + assembly: AssemblyConfig = AssemblyConfig() transformation: TransformationConfig = TransformationConfig() plating: PlatingConfig = PlatingConfig() def plan( - self, - request: ProtocolRequest, - *, - inputs: OutputManifest | None = None, + self, request: ProtocolRequest, *, inputs: OutputManifest | None = None ) -> ProtocolPlan: + """Expand a request into operations without assigning hardware locations.""" if isinstance(request, AssemblyRequest): if inputs is not None: raise ValueError( @@ -88,22 +111,19 @@ def compile( self, request: ProtocolRequest, *, - profile: OpentronsAssemblyProfile - | OpentronsTransformationProfile - | OpentronsPlatingProfile - | None = None, + profile: TargetProfile | None = None, inputs: OutputManifest | None = None, ) -> CompiledProtocol: + """Return an allocated plan and files, leaving writing and simulation explicit.""" plan = self.plan(request, inputs=inputs) if isinstance(request, AssemblyRequest): profile = profile or OpentronsAssemblyProfile() if not isinstance(profile, OpentronsAssemblyProfile): raise TypeError("Assembly requires an OpentronsAssemblyProfile.") method, config = "assembly", self.assembly - allocated = allocate(plan, profile=profile) - program = lower(allocated, profile=profile) + plan = allocate_assembly(plan, profile=profile) handoff_name = "transformation_input.json" - handoff = allocated.output_manifest().plasmid_locations() + handoff = plan.output_manifest().plasmid_locations() elif isinstance(request, TransformationRequest): profile = profile or OpentronsTransformationProfile() if not isinstance(profile, OpentronsTransformationProfile): @@ -111,11 +131,10 @@ def compile( "Transformation requires an OpentronsTransformationProfile." ) method, config = "transformation", self.transformation - allocated = allocate_transformation(plan, profile=profile, inputs=inputs) - program = lower_transformation(allocated, profile=profile) + plan = allocate_transformation(plan, profile=profile, inputs=inputs) handoff_name = "plating_input.json" handoff = { - "bacterium_locations": allocated.output_manifest().bacterium_locations() + "bacterium_locations": plan.output_manifest().bacterium_locations() } else: profile = profile or OpentronsPlatingProfile() @@ -126,65 +145,34 @@ def compile( raise ValueError( "Dilution volume exceeds the supported plate capacity." ) - allocated = allocate_plating(plan, profile=profile, inputs=inputs) - program = lower_plating(allocated, profile=profile) + plan = allocate_plating(plan, profile=profile, inputs=inputs) handoff_name = "plating_layout.json" - handoff = plating_layout(allocated, dilution_factor=config.dilution_factor) - script = render_python(program, name=request.id) - markdown = render_markdown(plan, allocation=allocated) - manifest = allocated.output_manifest() - artifacts = ArtifactBundle( - artifacts=( - ProtocolArtifact( - name="protocol.py", media_type="text/x-python", content=script - ), - ProtocolArtifact( - name="compilation.json", - media_type="application/json", - content=json.dumps( - { - "schema_version": "1.0", - "method": method, - "reference_revision": PUDU_REVISION, - "configuration": asdict(config), - "target_profile": asdict(profile), - "plan": { - **asdict(plan), - "steps": [ - {"kind": type(s).__name__, **asdict(s)} - for s in plan.steps - ], - }, - "containers": [asdict(c) for c in allocated.containers], - "placements": [asdict(p) for p in allocated.placements], - }, - indent=2, - sort_keys=True, - allow_nan=False, - ) - + "\n", - ), - ProtocolArtifact( - name="protocol.md", media_type="text/markdown", content=markdown - ), - ProtocolArtifact( - name="manifest.json", - media_type="application/json", - content=json.dumps(manifest.to_dict(), indent=2, sort_keys=True) - + "\n", - ), - ProtocolArtifact( - name=handoff_name, - media_type="application/json", - content=json.dumps(handoff, indent=2, sort_keys=True) + "\n", - ), - ) - ) + handoff = plating_layout(plan, dilution_factor=config.dilution_factor) + script = render_python(plan, profile=profile) + markdown = render_markdown(plan) + manifest = plan.output_manifest() + metadata = { + "schema_version": "1.0", + "method": method, + "configuration": asdict(config), + "target_profile": asdict(profile), + "plan": { + **asdict(plan), + "steps": [ + {"kind": type(step).__name__, **asdict(step)} for step in plan.steps + ], + }, + } return CompiledProtocol( - allocation=allocated, - program=program, + plan=plan, script=script, markdown=markdown, manifest=manifest, - artifacts=artifacts, + files={ + "protocol.py": script, + "protocol.md": markdown, + "manifest.json": _json(manifest.to_dict()), + "compilation.json": _json(metadata), + handoff_name: _json(handoff), + }, ) diff --git a/src/buildcompiler/protocols/config.py b/src/buildcompiler/protocols/config.py deleted file mode 100644 index 8121ce7..0000000 --- a/src/buildcompiler/protocols/config.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Public method configurations; implementations live with their planners.""" - -from buildcompiler.protocols.methods.assembly_config import AssemblyConfig -from buildcompiler.protocols.methods.transformation_config import TransformationConfig -from buildcompiler.protocols.methods.plating_config import PlatingConfig - -__all__ = ["AssemblyConfig", "TransformationConfig", "PlatingConfig"] diff --git a/src/buildcompiler/protocols/handoffs.py b/src/buildcompiler/protocols/handoffs.py deleted file mode 100644 index 3a13916..0000000 --- a/src/buildcompiler/protocols/handoffs.py +++ /dev/null @@ -1,82 +0,0 @@ -"""Explicit adapters for PUDU's location-only handoff formats.""" - -from collections.abc import Mapping - -from buildcompiler.protocols.allocation.models import ( - OutputManifest, - SamplePlacement, - WellRef, -) -from buildcompiler.protocols.allocation.wells import PLATE_96 -from buildcompiler.protocols.inputs import material_ref -from buildcompiler.protocols.materials import Sample -from buildcompiler.domain.protocol_requests import MaterialRef - - -def plasmid_manifest_from_json( - payload: Mapping[str, list[str]], *, protocol_id: str = "imported-assembly" -) -> OutputManifest: - samples, placements = [], [] - for index, (identity, wells) in enumerate(payload.items()): - if not isinstance(wells, (list, tuple)) or not wells: - raise ValueError("Each plasmid requires a nonempty list of source wells.") - for replicate, well in enumerate(wells, 1): - PLATE_96.index(well) - sample = Sample( - id=f"{protocol_id}/{index}/{replicate}", - material=material_ref(identity), - replicate=replicate, - ) - samples.append(sample) - placements.append( - SamplePlacement( - sample_id=sample.id, - location=WellRef(container_id="source_plate", well_name=well), - ) - ) - return OutputManifest( - protocol_id=protocol_id, samples=tuple(samples), placements=tuple(placements) - ) - - -def bacterium_manifest_from_json( - payload: Mapping, *, protocol_id: str = "imported-transformation" -) -> OutputManifest: - locations = payload.get("bacterium_locations") - if not isinstance(locations, Mapping) or not locations: - raise ValueError("Plating JSON requires nonempty bacterium_locations.") - samples, placements = [], [] - for index, (well, contents) in enumerate(locations.items()): - PLATE_96.index(well) - if isinstance(contents, str): - labels = (contents,) - elif ( - isinstance(contents, (list, tuple)) - and contents - and all(isinstance(v, str) for v in contents) - ): - labels = tuple(contents) - else: - raise ValueError( - "Bacterium contents must be a string or nonempty list of strings." - ) - # The legacy format has labels only: do not pretend they are SBOL identities. - sample = Sample( - id=f"{protocol_id}/{index}", - material=MaterialRef( - identity=f"urn:buildcompiler:imported:{protocol_id}:{index}", - label=", ".join(labels), - ), - contents=labels, - liquid_label=str(contents), - ) - samples.append(sample) - placements.append( - SamplePlacement( - sample_id=sample.id, - location=WellRef(container_id="source_plate", well_name=well), - ) - ) - return OutputManifest( - protocol_id=protocol_id, samples=tuple(samples), placements=tuple(placements) - ) diff --git a/src/buildcompiler/protocols/inputs.py b/src/buildcompiler/protocols/inputs.py index fbf8b44..fa5593c 100644 --- a/src/buildcompiler/protocols/inputs.py +++ b/src/buildcompiler/protocols/inputs.py @@ -1,8 +1,9 @@ -"""Explicit conversion from existing assembly JSON to typed protocol inputs.""" +"""Decode protocol inputs and stage records without importing robot dependencies.""" from collections.abc import Mapping, Sequence from urllib.parse import urlsplit +from buildcompiler.domain import IndexedBackbone, IndexedPlasmid, IndexedReagent from buildcompiler.domain.protocol_requests import ( AssemblyReaction, AssemblyRequest, @@ -10,9 +11,12 @@ TransformationReaction, TransformationRequest, ) +from buildcompiler.protocols.models import PLATE_96, OutputManifest, Sample, WellRef def material_ref(identity: str, label: str | None = None) -> MaterialRef: + """Keep the full identity and derive a display label when none is supplied.""" + if not isinstance(identity, str) or not identity: raise ValueError("Material identities must be nonempty strings.") if label is None: @@ -31,7 +35,7 @@ def assembly_request_from_json( request_id: str, source_stage_id: str | None = None, ) -> AssemblyRequest: - """Decode this one format explicitly; never guess a method from its keys.""" + """Decode products and ordered components into an assembly batch.""" if isinstance(payload, (str, bytes)) or not isinstance(payload, Sequence): raise TypeError("Assembly JSON must be a sequence of reaction objects.") reactions = [] @@ -64,6 +68,8 @@ def transformation_request_from_json( request_id: str, source_stage_id: str | None = None, ) -> TransformationRequest: + """Decode strains, chassis and ordered plasmids into a transformation batch.""" + if isinstance(payload, (str, bytes)) or not isinstance(payload, Sequence): raise TypeError("Transformation JSON must be a sequence of reaction objects.") reactions = [] @@ -85,3 +91,95 @@ def transformation_request_from_json( return TransformationRequest( id=request_id, reactions=tuple(reactions), source_stage_id=source_stage_id ) + + +def plasmid_manifest_from_json( + payload: Mapping[str, list[str]], *, protocol_id: str = "imported-assembly" +) -> OutputManifest: + """Import identity-to-well mappings as distinct physical source replicates.""" + + samples = [] + for index, (identity, wells) in enumerate(payload.items()): + if not isinstance(wells, (list, tuple)) or not wells: + raise ValueError("Each plasmid requires a nonempty list of source wells.") + for replicate, well in enumerate(wells, 1): + PLATE_96.index(well) + sample = Sample( + id=f"{protocol_id}/{index}/{replicate}", + material=material_ref(identity), + replicate=replicate, + location=WellRef(container_id="source_plate", well_name=well), + ) + samples.append(sample) + return OutputManifest(protocol_id=protocol_id, samples=tuple(samples)) + + +def bacterium_manifest_from_json( + payload: Mapping, *, protocol_id: str = "imported-transformation" +) -> OutputManifest: + """Import culture-well labels, assigning local identities when none are present.""" + + locations = payload.get("bacterium_locations") + if not isinstance(locations, Mapping) or not locations: + raise ValueError("Plating JSON requires nonempty bacterium_locations.") + samples = [] + for index, (well, contents) in enumerate(locations.items()): + PLATE_96.index(well) + if isinstance(contents, str): + labels = (contents,) + elif ( + isinstance(contents, (list, tuple)) + and contents + and all(isinstance(v, str) for v in contents) + ): + labels = tuple(contents) + else: + raise ValueError( + "Bacterium contents must be a string or nonempty list of strings." + ) + # The legacy format has labels only: do not pretend they are SBOL identities. + sample = Sample( + id=f"{protocol_id}/{index}", + material=MaterialRef( + identity=f"urn:buildcompiler:imported:{protocol_id}:{index}", + label=", ".join(labels), + ), + contents=labels, + liquid_label=str(contents), + location=WellRef(container_id="source_plate", well_name=well), + ) + samples.append(sample) + return OutputManifest(protocol_id=protocol_id, samples=tuple(samples)) + + +def assembly_request_from_route( + *, + stage_id: str, + products: Sequence[IndexedPlasmid], + parts: Sequence[IndexedPlasmid], + backbone: IndexedBackbone, + restriction_enzyme: IndexedReagent, +) -> AssemblyRequest: + """Use actual produced identities and preserve the selected component order.""" + + def material(record) -> MaterialRef: + """Keep indexed identities and choose the most specific display label.""" + return MaterialRef( + identity=record.identity, + label=record.name or record.display_id or record.identity, + ) + + return AssemblyRequest( + id=stage_id, + source_stage_id=stage_id, + reactions=tuple( + AssemblyReaction( + id=f"{stage_id}/product/{index}", + product=material(product), + backbone=material(backbone), + parts=tuple(material(part) for part in parts), + restriction_enzyme=material(restriction_enzyme), + ) + for index, product in enumerate(products) + ), + ) diff --git a/src/buildcompiler/protocols/materials.py b/src/buildcompiler/protocols/materials.py deleted file mode 100644 index f7fb24a..0000000 --- a/src/buildcompiler/protocols/materials.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Samples retain material identity independently of their physical placement.""" - -from dataclasses import dataclass - -from buildcompiler.domain.protocol_requests import MaterialRef - - -@dataclass(frozen=True, slots=True, kw_only=True) -class Sample: - id: str - material: MaterialRef - parent_ids: tuple[str, ...] = () - replicate: int | None = None - initial_volume_ul: float | None = None - liquid_label: str | None = None - role: str = "material" - source_sample_id: str | None = None - contents: tuple[str, ...] = () - dilution: int | None = None - - -@dataclass(frozen=True, slots=True, kw_only=True) -class SamplePoint: - sample_id: str - bottom_mm: float | None = None - top_mm: float | None = None - track_conical_height: bool = False - - def __post_init__(self) -> None: - if ( - sum( - ( - self.bottom_mm is not None, - self.top_mm is not None, - self.track_conical_height, - ) - ) - > 1 - ): - raise ValueError("A sample point requires one location policy.") diff --git a/src/buildcompiler/protocols/methods/__init__.py b/src/buildcompiler/protocols/methods/__init__.py index ee5bc75..e1937a8 100644 --- a/src/buildcompiler/protocols/methods/__init__.py +++ b/src/buildcompiler/protocols/methods/__init__.py @@ -1 +1 @@ -"""Pure protocol planners, grouped by scientific method.""" +"""Method-specific configurations, planners and well allocation.""" diff --git a/src/buildcompiler/protocols/methods/assembly.py b/src/buildcompiler/protocols/methods/assembly.py index d945e03..1358dee 100644 --- a/src/buildcompiler/protocols/methods/assembly.py +++ b/src/buildcompiler/protocols/methods/assembly.py @@ -1,29 +1,147 @@ -"""Assembly planning extracted from PUDU's SBOLLoopAssembly. +"""Assembly method: configuration, planning and well allocation.""" -The operation order and default parameters preserve that implementation. -No robot, filesystem, or mutable inventory is used while planning. -""" +import math +from dataclasses import dataclass from buildcompiler.domain.protocol_requests import AssemblyRequest, MaterialRef - -from buildcompiler.protocols.config import AssemblyConfig -from buildcompiler.protocols.materials import Sample, SamplePoint -from buildcompiler.protocols.plans import ProtocolPlan -from buildcompiler.protocols.steps import ( +from buildcompiler.protocols.models import ( + BLOCK_24, + PLATE_96, + ConfigOverrides, + ContainerSpec, DeactivateSourceModule, DropTip, LidAction, OperatorAction, + ProtocolPlan, RunTemperatureProgram, + Sample, + SamplePoint, SetTemperature, Step, + TemperatureProgram, + TemperatureStep, Transfer, + WellRef, + positive, + positive_integer, + validate_slots, ) + +@dataclass(frozen=True, slots=True, kw_only=True) +class AssemblyConfig(ConfigOverrides): + """Reaction volumes, replicate count and assembly incubation programs.""" + + volume_total_reaction: float = 20 + volume_part: float = 2 + volume_restriction_enzyme: float = 2 + volume_t4_dna_ligase: float = 4 + volume_t4_dna_ligase_buffer: float = 2 + replicates: int = 1 + aspiration_rate: float = 0.5 + dispense_rate: float = 1 + water_testing: bool = False + source_temperature: float = 4 + lid_temperature: float = 42 + hold_temperature: float = 4 + cycling: TemperatureProgram = TemperatureProgram( + steps=( + TemperatureStep(celsius=42, minutes=2), + TemperatureStep(celsius=16, minutes=5), + ), + repetitions=75, + block_max_volume_ul=30, + ) + finishing: TemperatureProgram = TemperatureProgram( + steps=( + TemperatureStep(celsius=60, minutes=10), + TemperatureStep(celsius=80, minutes=10), + ), + repetitions=1, + block_max_volume_ul=30, + ) + + def __post_init__(self) -> None: + positive_integer(self.replicates, "replicates") + if type(self.water_testing) is not bool: + raise ValueError("water_testing must be a boolean.") + for name in ( + "volume_total_reaction", + "volume_part", + "volume_restriction_enzyme", + "volume_t4_dna_ligase", + "volume_t4_dna_ligase_buffer", + "aspiration_rate", + "dispense_rate", + ): + positive(getattr(self, name), name) + for name in ("source_temperature", "lid_temperature", "hold_temperature"): + value = getattr(self, name) + if type(value) not in (int, float) or not math.isfinite(value): + raise ValueError(f"{name} must be a finite number.") + for program in (self.cycling, self.finishing): + if not isinstance(program, TemperatureProgram): + raise TypeError( + "Temperature programs must be TemperatureProgram records." + ) + if not isinstance(program.steps, tuple) or not program.steps: + raise ValueError( + "Temperature programs require a nonempty tuple of steps." + ) + if type(program.repetitions) is not int or program.repetitions < 1: + raise ValueError("Program repetitions must be positive integers.") + if ( + not math.isfinite(program.block_max_volume_ul) + or program.block_max_volume_ul <= 0 + ): + raise ValueError("Program block volume must be finite and positive.") + for step in program.steps: + if ( + not math.isfinite(step.celsius) + or not math.isfinite(step.minutes) + or step.minutes < 0 + ): + raise ValueError("Invalid temperature program step.") + + +@dataclass(frozen=True, slots=True, kw_only=True) +class OpentronsAssemblyProfile: + """Assembly deck slots, pipette mount and starting well/tip offsets.""" + + api_level: str = "2.21" + temperature_module_position: str = "1" + thermocycler_starting_well: int = 0 + tiprack_positions: tuple[str, ...] = ("2", "3", "4", "5", "6", "9") + pipette_position: str = "left" + initial_tip: str = "A1" + + def __post_init__(self) -> None: + if self.api_level != "2.21": + raise ValueError("This backend currently supports API 2.21.") + if self.pipette_position not in ("left", "right"): + raise ValueError("Invalid pipette mount.") + PLATE_96.name(self.thermocycler_starting_well) + PLATE_96.index(self.initial_tip) + if not isinstance(self.tiprack_positions, tuple) or not self.tiprack_positions: + raise ValueError("tiprack_positions must be a nonempty tuple.") + validate_slots((self.temperature_module_position, *self.tiprack_positions)) + + @property + def max_volume_ul(self) -> float: + """Maximum transfer volume of the supported assembly pipette.""" + + return 20 + + WATER = MaterialRef(identity="urn:buildcompiler:water", label="Deionized Water") + + BUFFER = MaterialRef( identity="urn:buildcompiler:ligase-buffer", label="T4 DNA Ligase Buffer" ) + + LIGASE = MaterialRef(identity="urn:buildcompiler:ligase", label="T4 DNA Ligase") @@ -38,7 +156,7 @@ def plan_assembly(request: AssemblyRequest, *, config: AssemblyConfig) -> Protoc if previous is not None and previous != material: raise ValueError(f"Conflicting labels for {material.identity}.") parts[material.identity] = material - # Match PUDU's source ordering, but retain full identities as keys. + # Sort labels for stable source positions; retain identities for lookups. ordered = ( WATER, BUFFER, @@ -122,8 +240,7 @@ def plan_assembly(request: AssemblyRequest, *, config: AssemblyConfig) -> Protoc drop_tip=addition_index != len(additions) - 1, ) ) - # PUDU finishes using the final addition's tip. This is explicit - # in the plan rather than hidden inside a liquid-transfer helper. + # Finish mixing with the tip retained from the final addition. for mix_index in range(int(config.volume_total_reaction / 10)): steps.append( Transfer( @@ -143,7 +260,7 @@ def plan_assembly(request: AssemblyRequest, *, config: AssemblyConfig) -> Protoc steps.append( OperatorAction( id="remove-reagents", - instruction="Take out the reagents since the temperature module will be turn off", + instruction="Remove the reagents before the source temperature module is deactivated.", ) ) if not config.water_testing: @@ -169,3 +286,44 @@ def plan_assembly(request: AssemblyRequest, *, config: AssemblyConfig) -> Protoc output_sample_ids=tuple(output_ids), steps=tuple(steps), ) + + +def allocate_assembly( + plan: ProtocolPlan, *, profile: OpentronsAssemblyProfile +) -> ProtocolPlan: + """Assign sources and reactions in column-major order on their plates.""" + if len(plan.input_sample_ids) > BLOCK_24.capacity: + raise ValueError("Assembly source block capacity exceeded.") + if ( + len(plan.output_sample_ids) + profile.thermocycler_starting_well + > PLATE_96.capacity + ): + raise ValueError("Assembly reaction plate capacity exceeded.") + locations = { + sample_id: WellRef(container_id="sources", well_name=BLOCK_24.name(i)) + for i, sample_id in enumerate(plan.input_sample_ids) + } + locations.update( + { + sample_id: WellRef( + container_id="reactions", + well_name=PLATE_96.name(i + profile.thermocycler_starting_well), + ) + for i, sample_id in enumerate(plan.output_sample_ids) + } + ) + return plan.with_locations( + ( + ContainerSpec( + id="sources", + definition_id="opentrons_24_aluminumblock_nest_1.5ml_snapcap", + parent="source_module", + ), + ContainerSpec( + id="reactions", + definition_id="nest_96_wellplate_100ul_pcr_full_skirt", + parent="reaction_module", + ), + ), + locations, + ) diff --git a/src/buildcompiler/protocols/methods/assembly_config.py b/src/buildcompiler/protocols/methods/assembly_config.py deleted file mode 100644 index 9e04200..0000000 --- a/src/buildcompiler/protocols/methods/assembly_config.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Resolved, immutable method configuration. Defaults preserve PUDU assembly. - -Assembly defaults and operation ordering originate in PUDU (MIT, Rudge Lab). -See buildcompiler.protocols.attribution. A method configuration intentionally contains no deck layout. -""" - -import math -from dataclasses import dataclass - -from buildcompiler.protocols.steps import TemperatureProgram, TemperatureStep -from buildcompiler.protocols.validation import ConfigOverrides - - -@dataclass(frozen=True, slots=True, kw_only=True) -class AssemblyConfig(ConfigOverrides): - volume_total_reaction: float = 20 - volume_part: float = 2 - volume_restriction_enzyme: float = 2 - volume_t4_dna_ligase: float = 4 - volume_t4_dna_ligase_buffer: float = 2 - replicates: int = 1 - aspiration_rate: float = 0.5 - dispense_rate: float = 1 - water_testing: bool = False - source_temperature: float = 4 - lid_temperature: float = 42 - hold_temperature: float = 4 - cycling: TemperatureProgram = TemperatureProgram( - steps=( - TemperatureStep(celsius=42, minutes=2), - TemperatureStep(celsius=16, minutes=5), - ), - repetitions=75, - block_max_volume_ul=30, - ) - finishing: TemperatureProgram = TemperatureProgram( - steps=( - TemperatureStep(celsius=60, minutes=10), - TemperatureStep(celsius=80, minutes=10), - ), - repetitions=1, - block_max_volume_ul=30, - ) - - def __post_init__(self) -> None: - if type(self.replicates) is not int or self.replicates < 1: - raise ValueError("replicates must be a positive integer.") - if type(self.water_testing) is not bool: - raise ValueError("water_testing must be a boolean.") - for name in ( - "volume_total_reaction", - "volume_part", - "volume_restriction_enzyme", - "volume_t4_dna_ligase", - "volume_t4_dna_ligase_buffer", - "aspiration_rate", - "dispense_rate", - ): - value = getattr(self, name) - if ( - type(value) not in (int, float) - or not math.isfinite(value) - or value <= 0 - ): - raise ValueError(f"{name} must be a finite positive number.") - for name in ("source_temperature", "lid_temperature", "hold_temperature"): - value = getattr(self, name) - if type(value) not in (int, float) or not math.isfinite(value): - raise ValueError(f"{name} must be a finite number.") - for program in (self.cycling, self.finishing): - if not isinstance(program, TemperatureProgram): - raise TypeError( - "Temperature programs must be TemperatureProgram records." - ) - if not isinstance(program.steps, tuple) or not program.steps: - raise ValueError( - "Temperature programs require a nonempty tuple of steps." - ) - if type(program.repetitions) is not int or program.repetitions < 1: - raise ValueError("Program repetitions must be positive integers.") - if ( - not math.isfinite(program.block_max_volume_ul) - or program.block_max_volume_ul <= 0 - ): - raise ValueError("Program block volume must be finite and positive.") - for step in program.steps: - if ( - not math.isfinite(step.celsius) - or not math.isfinite(step.minutes) - or step.minutes < 0 - ): - raise ValueError("Invalid temperature program step.") diff --git a/src/buildcompiler/protocols/methods/plating.py b/src/buildcompiler/protocols/methods/plating.py index 30a382a..9d050d1 100644 --- a/src/buildcompiler/protocols/methods/plating.py +++ b/src/buildcompiler/protocols/methods/plating.py @@ -1,26 +1,144 @@ -"""Pure plating plan, with explicit intermediate samples and shared operations. +"""Plating method: configuration, planning and well allocation.""" -Derived from pudu/plating.py (MIT); see protocols/attribution.py. -""" +from collections import Counter +from dataclasses import dataclass from buildcompiler.domain.protocol_requests import MaterialRef, PlatingRequest -from buildcompiler.protocols.materials import Sample, SamplePoint -from buildcompiler.protocols.methods.plating_config import PlatingConfig -from buildcompiler.protocols.plans import ProtocolPlan -from buildcompiler.protocols.steps import ( +from buildcompiler.protocols.models import ( + PLATE_96, + ConfigOverrides, + ContainerSpec, Distribute, DropTip, LidAction, Mix, + OutputManifest, PickUpTip, + ProtocolPlan, + Sample, + SamplePoint, SetTemperature, Transfer, + WellGrid, + WellRef, + positive, + positive_integer, + validate_slots, ) +@dataclass(frozen=True, slots=True, kw_only=True) +class PlatingConfig(ConfigOverrides): + """Dilution volumes, spotting replicates and liquid-handling rates.""" + + volume_total_reaction: float = 20 + volume_bacteria_transfer: float = 2 + volume_colony: float = 4 + dilution_factor: float = 10 + volume_lb: float = 10000 + replicates: int = 1 + number_dilutions: int = 2 + max_colonies: int = 192 + aspiration_rate: float = 0.5 + dispense_rate: float = 1 + + def __post_init__(self) -> None: + for name in ( + "volume_total_reaction", + "volume_bacteria_transfer", + "volume_colony", + "dilution_factor", + "volume_lb", + "aspiration_rate", + "dispense_rate", + ): + positive(getattr(self, name), name) + for name in ("replicates", "number_dilutions", "max_colonies"): + positive_integer(getattr(self, name), name) + if self.replicates > 8 or self.number_dilutions > 2: + raise ValueError( + "Plating supports at most eight replicates and two dilutions." + ) + if self.dilution_factor <= 1 or self.dilution_volume <= 1: + raise ValueError("Dilution factor and dilution volume must exceed one.") + required = self.volume_colony * self.replicates + if self.number_dilutions == 2: + required += self.volume_bacteria_transfer + if self.dilution_volume < required: + raise ValueError( + "Dilution volume is insufficient for plating and subsequent seeding." + ) + + @property + def volume_lb_transfer(self) -> float: + """Broth added before inoculating each dilution well.""" + + return self.volume_bacteria_transfer * (self.dilution_factor - 1) + + @property + def dilution_volume(self) -> float: + """Final volume of a dilution before seeding or spotting.""" + + return self.volume_bacteria_transfer * self.dilution_factor + + @property + def mix_volume(self) -> float: + """Mix below the liquid volume and the small-pipette limit.""" + + return min(19, self.dilution_volume - 1) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class OpentronsPlatingProfile: + """Plating source labware, deck slots and starting tip/tube positions.""" + + api_level: str = "2.21" + thermocycler_labware: str = "biorad_96_wellplate_200ul_pcr" + small_tiprack_position: str = "9" + large_tiprack_position: str = "1" + initial_small_tip: str | None = None + initial_large_tip: str | None = None + dilution_plate_position1: str = "2" + dilution_plate_position2: str = "3" + agar_plate_position1: str = "5" + agar_plate_position2: str = "6" + tube_rack_position: str = "4" + lb_tube_position: int = 0 + + def __post_init__(self) -> None: + if self.api_level != "2.21": + raise ValueError("This backend currently supports API 2.21.") + if self.thermocycler_labware not in ( + "biorad_96_wellplate_200ul_pcr", + "nest_96_wellplate_100ul_pcr_full_skirt", + ): + raise ValueError("Unsupported plating source labware.") + if ( + type(self.lb_tube_position) is not int + or not 0 <= self.lb_tube_position < 15 + ): + raise ValueError("lb_tube_position must index the 15-tube rack.") + for tip in (self.initial_small_tip, self.initial_large_tip): + if tip is not None: + PLATE_96.index(tip) + validate_slots( + ( + self.small_tiprack_position, + self.large_tiprack_position, + self.dilution_plate_position1, + self.dilution_plate_position2, + self.agar_plate_position1, + self.agar_plate_position2, + self.tube_rack_position, + ) + ) + + def plan_plating( request: PlatingRequest, *, config: PlatingConfig, inputs: tuple[Sample, ...] ) -> ProtocolPlan: + """Build dilution intermediates and plated replicates with explicit lineage.""" + available = {s.id: s for s in inputs} if set(request.sample_ids) - available.keys(): raise ValueError("Plating sources are missing from the source manifest.") @@ -102,6 +220,7 @@ def plan_plating( steps.append(DropTip(id="broth-tip-drop", instrument="large")) def transfer(id, source, destination, volume, *, agar=False): + """Keep the current tip and use the agar offset only for spotting.""" return Transfer( id=id, source=SamplePoint(sample_id=source.id), @@ -121,6 +240,7 @@ def transfer(id, source, destination, volume, *, agar=False): for index, source in enumerate(sources): steps.append(PickUpTip(id=f"dilution-tip/{index}", instrument="small")) parent = source + # Finish seeding the dilution series before removing volume for spots. for dilution in range(1, config.number_dilutions + 1): destination = dilutions[index, dilution] steps.append( @@ -141,6 +261,8 @@ def transfer(id, source, destination, volume, *, agar=False): ) ) parent = destination + # Retain the seeding tip for the first dilution's spots; subsequent + # dilution groups each start with a fresh tip. for dilution in range(1, config.number_dilutions + 1): if dilution > 1: steps.append( @@ -167,3 +289,94 @@ def transfer(id, source, destination, volume, *, agar=False): output_sample_ids=tuple(s.id for s in spots.values()), steps=tuple(steps), ) + + +def allocate_plating( + plan: ProtocolPlan, *, profile: OpentronsPlatingProfile, inputs: OutputManifest +) -> ProtocolPlan: + """Place each dilution in a plate half or on its own plate when needed.""" + + upstream = {p.id: p.location for p in inputs.samples} + selected = [s for s in plan.samples if s.role == "bacteria"] + if len({upstream[s.source_sample_id].container_id for s in selected}) != 1: + raise ValueError("The plating target accepts one source plate.") + containers = [ + ContainerSpec( + id="reactions", + definition_id=profile.thermocycler_labware, + parent="reaction_module", + ), + ContainerSpec( + id="tube_rack", + definition_id="opentrons_15_tuberack_falcon_15ml_conical", + parent=profile.tube_rack_position, + ), + ] + counts = Counter((s.role, s.dilution) for s in plan.samples) + indices = Counter() + locations = {} + for sample in plan.samples: + if sample.role == "broth": + location = WellRef( + container_id="tube_rack", + well_name=WellGrid(rows=3, columns=5).name(profile.lb_tube_position), + ) + if sample.initial_volume_ul > 15000: + raise ValueError("Broth loading exceeds tube capacity.") + elif sample.role == "bacteria": + well = upstream[sample.source_sample_id].well_name + PLATE_96.index(well) + location = WellRef(container_id="reactions", well_name=well) + elif sample.role in ("dilution", "agar"): + per_dilution = counts[sample.role, sample.dilution] + if per_dilution > 96: + raise ValueError("A dilution exceeds plate capacity.") + # Two dilutions share opposite plate halves until either needs + # more than half a plate; then each gets its own plate. + two_plates = counts[sample.role, 2] and per_dilution > 48 + plate = sample.dilution if two_plates else 1 + index = indices[sample.role, sample.dilution] + indices[sample.role, sample.dilution] += 1 + if sample.dilution == 2 and not two_plates: + index += 48 + name = f"{sample.role}_{plate}" + if name not in {c.id for c in containers}: + containers.append( + ContainerSpec( + id=name, + definition_id="nest_96_wellplate_100ul_pcr_full_skirt", + parent=getattr(profile, f"{sample.role}_plate_position{plate}"), + ) + ) + location = WellRef(container_id=name, well_name=PLATE_96.name(index)) + else: + raise ValueError(f"Unknown plating sample role {sample.role!r}.") + locations[sample.id] = location + return plan.with_locations(tuple(containers), locations) + + +def plating_layout(plan: ProtocolPlan, *, dilution_factor: float) -> dict: + """Export agar wells by tracing each spot back to its source culture.""" + + samples = {s.id: s for s in plan.samples} + locations = {s.id: s.location for s in plan.samples} + plates = {} + outputs = [samples[sid] for sid in plan.output_sample_ids] + # Group by dilution while preserving source and replicate order. + for sample in sorted(outputs, key=lambda s: s.dilution): + source = samples[sample.parent_ids[0]] + while source.role == "dilution": + source = samples[source.parent_ids[0]] + location = locations[sample.id] + plate = location.container_id.replace("agar_", "plate_") + factor = dilution_factor**sample.dilution + factor = int(factor) if float(factor).is_integer() else factor + entry = plates.setdefault(plate, {}).setdefault( + f"dilution_{sample.dilution}", {"ratio": f"1/{factor}", "wells": {}} + ) + entry["wells"][location.well_name] = { + "construct": ", ".join(source.contents), + "source_well": locations[source.id].well_name, + "replicate": sample.replicate, + } + return {"agar_plates": plates} diff --git a/src/buildcompiler/protocols/methods/plating_config.py b/src/buildcompiler/protocols/methods/plating_config.py deleted file mode 100644 index 374e1db..0000000 --- a/src/buildcompiler/protocols/methods/plating_config.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Immutable dilution and plating configuration, preserving PUDU defaults.""" - -from dataclasses import dataclass - -from buildcompiler.protocols.validation import ( - ConfigOverrides, - positive, - positive_integer, -) - - -@dataclass(frozen=True, slots=True, kw_only=True) -class PlatingConfig(ConfigOverrides): - volume_total_reaction: float = 20 - volume_bacteria_transfer: float = 2 - volume_colony: float = 4 - dilution_factor: float = 10 - volume_lb: float = 10000 - replicates: int = 1 - number_dilutions: int = 2 - max_colonies: int = 192 - aspiration_rate: float = 0.5 - dispense_rate: float = 1 - - def __post_init__(self) -> None: - for name in ( - "volume_total_reaction", - "volume_bacteria_transfer", - "volume_colony", - "dilution_factor", - "volume_lb", - "aspiration_rate", - "dispense_rate", - ): - positive(getattr(self, name), name) - for name in ("replicates", "number_dilutions", "max_colonies"): - positive_integer(getattr(self, name), name) - if self.replicates > 8 or self.number_dilutions > 2: - raise ValueError( - "PUDU plating supports at most eight replicates and two dilutions." - ) - if self.dilution_factor <= 1 or self.dilution_volume <= 1: - raise ValueError("Dilution factor and dilution volume must exceed one.") - required = self.volume_colony * self.replicates - if self.number_dilutions == 2: - required += self.volume_bacteria_transfer - if self.dilution_volume < required: - raise ValueError( - "Dilution volume is insufficient for plating and subsequent seeding." - ) - - @property - def volume_lb_transfer(self) -> float: - return self.volume_bacteria_transfer * (self.dilution_factor - 1) - - @property - def dilution_volume(self) -> float: - return self.volume_bacteria_transfer * self.dilution_factor - - @property - def mix_volume(self) -> float: - return min(19, self.dilution_volume - 1) diff --git a/src/buildcompiler/protocols/methods/transformation.py b/src/buildcompiler/protocols/methods/transformation.py index 9af4e3f..6a7b6ab 100644 --- a/src/buildcompiler/protocols/methods/transformation.py +++ b/src/buildcompiler/protocols/methods/transformation.py @@ -1,33 +1,124 @@ -"""Pure heat-shock planning, preserving PUDU's ordering and tube grouping. - -Derived from pudu/transformation.py (MIT); see protocols/attribution.py. -Source-location replicates and method replicates are distinct axes. -""" +"""Transformation method: configuration, planning and well allocation.""" from collections import Counter +from dataclasses import dataclass from itertools import groupby from math import ceil from buildcompiler.domain.protocol_requests import MaterialRef, TransformationRequest -from buildcompiler.protocols.materials import Sample, SamplePoint -from buildcompiler.protocols.methods.transformation_config import TransformationConfig -from buildcompiler.protocols.plans import ProtocolPlan -from buildcompiler.protocols.steps import ( +from buildcompiler.protocols.models import ( + BLOCK_24, + PLATE_96, + ConfigOverrides, + ContainerSpec, Distribute, LidAction, + OutputManifest, + ProtocolPlan, RunTemperatureProgram, + Sample, + SamplePoint, SetTemperature, TemperatureProgram, + TemperatureStep, Transfer, + WellRef, + positive, + positive_integer, + validate_slots, ) +@dataclass(frozen=True, slots=True, kw_only=True) +class TransformationConfig(ConfigOverrides): + """Transfer volumes, source tube volumes, replicates and incubations.""" + + volume_dna: float = 20 + transfer_volume_dna: float = 2 + transfer_volume_competent_cell: float = 20 + tube_volume_competent_cell: float = 100 + transfer_volume_recovery_media: float = 60 + tube_volume_recovery_media: float = 1200 + replicates: int = 2 + aspiration_rate: float = 0.5 + dispense_rate: float = 1 + water_testing: bool = False + cold_incubation1: TemperatureStep = TemperatureStep(celsius=4, minutes=30) + heat_shock: TemperatureStep = TemperatureStep(celsius=42, minutes=1) + cold_incubation2: TemperatureStep = TemperatureStep(celsius=4, minutes=2) + recovery_incubation: TemperatureStep = TemperatureStep(celsius=37, minutes=60) + + def __post_init__(self) -> None: + positive_integer(self.replicates, "replicates") + for name in ( + "volume_dna", + "transfer_volume_dna", + "transfer_volume_competent_cell", + "tube_volume_competent_cell", + "transfer_volume_recovery_media", + "tube_volume_recovery_media", + "aspiration_rate", + "dispense_rate", + ): + positive(getattr(self, name), name) + if type(self.water_testing) is not bool: + raise ValueError("water_testing must be a boolean.") + if self.tube_volume_competent_cell < self.transfer_volume_competent_cell: + raise ValueError("A cell tube must supply at least one transfer.") + if self.tube_volume_recovery_media < self.transfer_volume_recovery_media: + raise ValueError("A media tube must supply at least one transfer.") + for step in ( + self.cold_incubation1, + self.heat_shock, + self.cold_incubation2, + self.recovery_incubation, + ): + if not isinstance(step, TemperatureStep): + raise TypeError("Incubations must be TemperatureStep records.") + + +@dataclass(frozen=True, slots=True, kw_only=True) +class OpentronsTransformationProfile: + """Transformation deck slots and source, reaction and tip offsets.""" + + api_level: str = "2.21" + temperature_module_position: str = "1" + dna_plate_position: str = "2" + tube_rack_position: str = "3" + tiprack_p20_position: str = "9" + tiprack_p200_position: str = "6" + thermocycler_starting_well: int = 0 + initial_dna_well: int = 0 + initial_tip_p20: str | None = None + initial_tip_p300: str | None = None + + def __post_init__(self) -> None: + if self.api_level != "2.21": + raise ValueError("This backend currently supports API 2.21.") + PLATE_96.name(self.thermocycler_starting_well) + BLOCK_24.name(self.initial_dna_well) + for tip in (self.initial_tip_p20, self.initial_tip_p300): + if tip is not None: + PLATE_96.index(tip) + validate_slots( + ( + self.temperature_module_position, + self.dna_plate_position, + self.tube_rack_position, + self.tiprack_p20_position, + self.tiprack_p200_position, + ) + ) + + def plan_transformation( request: TransformationRequest, *, config: TransformationConfig, inputs: tuple[Sample, ...] | None = None, ) -> ProtocolPlan: + """Expand source and method replicates, grouping transfers by source tube.""" + plasmids = {p.identity: p for r in request.reactions for p in r.plasmids} chassis = {r.chassis.identity: r.chassis for r in request.reactions} ordered_plasmids = sorted(plasmids.values(), key=lambda p: (p.label, p.identity)) @@ -36,6 +127,8 @@ def plan_transformation( for sample in inputs or (): if sample.material.identity in available: available[sample.material.identity].append(sample) + # Each source location gets its own set of transformation replicates. Pair + # plasmids by source index, so every plasmid must have the same source count. if inputs is not None: if any(not samples for samples in available.values()): raise ValueError( @@ -154,6 +247,8 @@ def plan_transformation( ), ) ) + # Combine consecutive transfers from the same tube without reordering + # reactions. A chassis can recur later after another chassis was handled. for index, (source_id, group) in enumerate( groupby(cell_transfers, key=lambda pair: pair[0].id) ): @@ -184,6 +279,7 @@ def plan_transformation( drop_tip=False, ) ) + # Reuse the DNA tip for the finishing movements, then discard it. for movement in range(2): steps.append( Transfer( @@ -218,6 +314,7 @@ def plan_transformation( ) ) steps.append(LidAction(id="open-for-recovery", action="open")) + # Use the same tube boundaries that assigned each output's media parent. for index, medium in enumerate(media): destinations = outputs[index * per_media_tube : (index + 1) * per_media_tube] steps.append( @@ -252,3 +349,79 @@ def plan_transformation( output_sample_ids=tuple(s.id for s in outputs), steps=tuple(steps), ) + + +def allocate_transformation( + plan: ProtocolPlan, + *, + profile: OpentronsTransformationProfile, + inputs: OutputManifest | None = None, +) -> ProtocolPlan: + """Bind mapped DNA wells or allocate fresh sources, tubes and reactions.""" + + containers = [ + ContainerSpec( + id="sources", + definition_id="opentrons_24_aluminumblock_nest_1.5ml_snapcap", + parent="source_module", + ), + ContainerSpec( + id="reactions", + definition_id="nest_96_wellplate_100ul_pcr_full_skirt", + parent="reaction_module", + ), + ] + upstream = {} + if inputs is not None: + if len({p.location.container_id for p in inputs.samples}) != 1: + raise ValueError("The transformation target accepts one source DNA plate.") + containers.append( + ContainerSpec( + id="dna_plate", + definition_id="nest_96_wellplate_100ul_pcr_full_skirt", + parent=profile.dna_plate_position, + ) + ) + upstream = {p.id: p.location for p in inputs.samples} + containers.append( + ContainerSpec( + id="tube_rack", + definition_id="opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap", + parent=profile.tube_rack_position, + ) + ) + locations = {} + dna_index, tube_index, reaction_index = ( + profile.initial_dna_well, + 0, + profile.thermocycler_starting_well, + ) + for sample in plan.samples: + if sample.role == "dna": + if inputs is None: + location = WellRef( + container_id="sources", well_name=BLOCK_24.name(dna_index) + ) + dna_index += 1 + else: + # The plate changes its deck role, but the source well and + # upstream sample identity remain the same. + well = upstream[sample.source_sample_id].well_name + PLATE_96.index(well) + location = WellRef(container_id="dna_plate", well_name=well) + elif sample.role in ("cells", "media"): + location = WellRef( + container_id="tube_rack", well_name=BLOCK_24.name(tube_index) + ) + tube_index += 1 + if sample.initial_volume_ul > 1500: + raise ValueError("Reagent loading exceeds tube capacity.") + elif sample.role == "reaction": + location = WellRef( + container_id="reactions", well_name=PLATE_96.name(reaction_index) + ) + reaction_index += 1 + else: + raise ValueError(f"Unknown transformation sample role {sample.role!r}.") + locations[sample.id] = location + return plan.with_locations(tuple(containers), locations) diff --git a/src/buildcompiler/protocols/methods/transformation_config.py b/src/buildcompiler/protocols/methods/transformation_config.py deleted file mode 100644 index 6807b77..0000000 --- a/src/buildcompiler/protocols/methods/transformation_config.py +++ /dev/null @@ -1,56 +0,0 @@ -"""PUDU heat-shock method parameters; no labware or deck configuration.""" - -from dataclasses import dataclass - -from buildcompiler.protocols.steps import TemperatureStep -from buildcompiler.protocols.validation import ( - ConfigOverrides, - positive, - positive_integer, -) - - -@dataclass(frozen=True, slots=True, kw_only=True) -class TransformationConfig(ConfigOverrides): - volume_dna: float = 20 - transfer_volume_dna: float = 2 - transfer_volume_competent_cell: float = 20 - tube_volume_competent_cell: float = 100 - transfer_volume_recovery_media: float = 60 - tube_volume_recovery_media: float = 1200 - replicates: int = 2 - aspiration_rate: float = 0.5 - dispense_rate: float = 1 - water_testing: bool = False - cold_incubation1: TemperatureStep = TemperatureStep(celsius=4, minutes=30) - heat_shock: TemperatureStep = TemperatureStep(celsius=42, minutes=1) - cold_incubation2: TemperatureStep = TemperatureStep(celsius=4, minutes=2) - recovery_incubation: TemperatureStep = TemperatureStep(celsius=37, minutes=60) - - def __post_init__(self) -> None: - positive_integer(self.replicates, "replicates") - for name in ( - "volume_dna", - "transfer_volume_dna", - "transfer_volume_competent_cell", - "tube_volume_competent_cell", - "transfer_volume_recovery_media", - "tube_volume_recovery_media", - "aspiration_rate", - "dispense_rate", - ): - positive(getattr(self, name), name) - if type(self.water_testing) is not bool: - raise ValueError("water_testing must be a boolean.") - if self.tube_volume_competent_cell < self.transfer_volume_competent_cell: - raise ValueError("A cell tube must supply at least one transfer.") - if self.tube_volume_recovery_media < self.transfer_volume_recovery_media: - raise ValueError("A media tube must supply at least one transfer.") - for step in ( - self.cold_incubation1, - self.heat_shock, - self.cold_incubation2, - self.recovery_incubation, - ): - if not isinstance(step, TemperatureStep): - raise TypeError("Incubations must be TemperatureStep records.") diff --git a/src/buildcompiler/protocols/models.py b/src/buildcompiler/protocols/models.py new file mode 100644 index 0000000..cc06d60 --- /dev/null +++ b/src/buildcompiler/protocols/models.py @@ -0,0 +1,436 @@ +"""Shared protocol data: samples, operations, placement and output handoffs. + +Planning leaves Sample.location unset. Allocation fills it on a new immutable +plan; both renderers then read that same plan. +""" + +from __future__ import annotations + +import math +import re +from collections.abc import Mapping +from dataclasses import dataclass, fields, replace +from typing import Literal + +from buildcompiler.domain.protocol_requests import MaterialRef + + +def positive(value: object, name: str) -> None: + """Validate finite positive volumes and rate parameters.""" + + if type(value) not in (int, float) or not math.isfinite(value) or value <= 0: + raise ValueError(f"{name} must be a finite positive number.") + + +def positive_integer(value: object, name: str) -> None: + """Validate counts without accepting booleans as integers.""" + + if type(value) is not int or value < 1: + raise ValueError(f"{name} must be a positive integer.") + + +class ConfigOverrides: + """Replace only explicitly supplied fields, including False and defaults.""" + + __slots__ = () + + def with_overrides(self, overrides: Mapping[str, object]): + """Return a validated copy, rejecting misspelled or unsupported options.""" + unknown = set(overrides) - {field.name for field in fields(self)} + if unknown: + raise ValueError(f"Unknown configuration fields: {sorted(unknown)}") + return replace(self, **overrides) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class WellGrid: + """Column-major well names and indices for a rectangular plate or rack.""" + + rows: int + columns: int + + def __post_init__(self) -> None: + if type(self.rows) is not int or not 1 <= self.rows <= 26: + raise ValueError("rows must be an integer between 1 and 26.") + if type(self.columns) is not int or self.columns < 1: + raise ValueError("columns must be a positive integer.") + + @property + def capacity(self) -> int: + """Number of wells in the grid.""" + + return self.rows * self.columns + + def name(self, index: int) -> str: + """Return the well name at a zero-based column-major index.""" + + if type(index) is not int or not 0 <= index < self.capacity: + raise ValueError(f"Well index {index} is outside this grid.") + return f"{chr(ord('A') + index % self.rows)}{index // self.rows + 1}" + + def index(self, name: str) -> int: + """Return the column-major index of a validated well name.""" + + match = re.fullmatch(r"([A-Z])([1-9][0-9]*)", name) + if not match: + raise ValueError(f"Invalid well name: {name!r}") + row = ord(match[1]) - ord("A") + column = int(match[2]) - 1 + if row >= self.rows or column >= self.columns: + raise ValueError(f"Well {name} is outside this grid.") + return column * self.rows + row + + +PLATE_96 = WellGrid(rows=8, columns=12) +BLOCK_24 = WellGrid(rows=4, columns=6) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class WellRef: + """A physical well qualified by its container within this protocol.""" + + container_id: str + well_name: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ContainerSpec: + """Labware definition and its parent module or deck slot.""" + + id: str + definition_id: str + parent: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Sample: + """One physical aliquot, with material identity, lineage and optional location.""" + + id: str + material: MaterialRef + parent_ids: tuple[str, ...] = () + replicate: int | None = None + initial_volume_ul: float | None = None + liquid_label: str | None = None + role: str = "material" + source_sample_id: str | None = None + contents: tuple[str, ...] = () + dilution: int | None = None + location: WellRef | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class SamplePoint: + """A sample reference with an optional vertical liquid-handling offset.""" + + sample_id: str + bottom_mm: float | None = None + top_mm: float | None = None + track_conical_height: bool = False + + def __post_init__(self) -> None: + if ( + sum( + ( + self.bottom_mm is not None, + self.top_mm is not None, + self.track_conical_height, + ) + ) + > 1 + ): + raise ValueError("A sample point requires one location policy.") + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Transfer: + """One liquid transfer, including mixing and tip reuse decisions.""" + + id: str + source: SamplePoint + destination: SamplePoint + volume_ul: float + aspiration_rate: float = 0.5 + dispense_rate: float = 1.0 + mix_before_ul: float = 0.0 + mix_repetitions: int = 3 + new_tip: bool = True + drop_tip: bool = True + blow_out: bool = True + touch_tip: bool = True + limit_to_pipette_capacity: bool = False + instrument: str = "pipette" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DropTip: + """Discard the tip currently attached to the named instrument.""" + + id: str + instrument: str = "pipette" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class PickUpTip: + """Attach a fresh tip to the named instrument.""" + + id: str + instrument: str = "pipette" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Mix: + """Mix an existing sample without changing its volume or tip.""" + + id: str + location: SamplePoint + volume_ul: float + repetitions: int + instrument: str = "pipette" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class Distribute: + """Fill a destination group from one source using an explicit tip policy.""" + + id: str + source: SamplePoint + destinations: tuple[SamplePoint, ...] + volume_ul: float + instrument: str = "large" + disposal_volume_ul: float = 0 + new_tip: Literal["once", "never"] = "once" + mix_before: tuple[int, float] | None = None + air_gap_ul: float | None = None + track_liquid_sample_id: str | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TemperatureStep: + """One temperature hold within an incubation program.""" + + celsius: float + minutes: float + + def __post_init__(self) -> None: + if ( + not math.isfinite(self.celsius) + or not math.isfinite(self.minutes) + or self.minutes < 0 + ): + raise ValueError( + "Temperature steps require finite temperatures and nonnegative durations." + ) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class TemperatureProgram: + """Temperature holds, repetition count and thermocycler volume setting.""" + + steps: tuple[TemperatureStep, ...] + repetitions: int + block_max_volume_ul: float + + +@dataclass(frozen=True, slots=True, kw_only=True) +class RunTemperatureProgram: + """Run an incubation program, optionally skipping it during simulation.""" + + id: str + program: TemperatureProgram + skip_during_simulation: bool = False + + +@dataclass(frozen=True, slots=True, kw_only=True) +class SetTemperature: + """Set a module or lid temperature without a timed hold.""" + + id: str + module: Literal["source", "reaction", "lid"] + celsius: float + skip_during_simulation: bool = False + + +@dataclass(frozen=True, slots=True, kw_only=True) +class LidAction: + """Open or close the thermocycler lid.""" + + id: str + action: Literal["open", "close"] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DeactivateSourceModule: + """Turn off source-module temperature control.""" + + id: str + + +@dataclass(frozen=True, slots=True, kw_only=True) +class OperatorAction: + """An instruction displayed to the operator during the protocol.""" + + id: str + instruction: str + + +Step = ( + Transfer + | PickUpTip + | Mix + | Distribute + | DropTip + | RunTemperatureProgram + | SetTemperature + | LidAction + | DeactivateSourceModule + | OperatorAction +) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class ProtocolPlan: + """Ordered operations and samples, before or after well allocation.""" + + id: str + request_id: str + samples: tuple[Sample, ...] + input_sample_ids: tuple[str, ...] + output_sample_ids: tuple[str, ...] + steps: tuple[Step, ...] + containers: tuple[ContainerSpec, ...] = () + + def __post_init__(self) -> None: + sample_ids = {sample.id for sample in self.samples} + if len(sample_ids) != len(self.samples): + raise ValueError("Sample ids must be unique within a plan.") + if len({step.id for step in self.steps}) != len(self.steps): + raise ValueError("Step ids must be unique within a plan.") + if not set(self.input_sample_ids + self.output_sample_ids) <= sample_ids: + raise ValueError("Plan inputs and outputs must reference declared samples.") + for sample in self.samples: + if not set(sample.parent_ids) <= sample_ids: + raise ValueError(f"Unknown parent for sample {sample.id}.") + for step in self.steps: + referenced = set() + if isinstance(step, Transfer): + referenced = {step.source.sample_id, step.destination.sample_id} + elif isinstance(step, Distribute): + referenced = { + step.source.sample_id, + *(d.sample_id for d in step.destinations), + } + if step.track_liquid_sample_id: + referenced.add(step.track_liquid_sample_id) + elif isinstance(step, Mix): + referenced = {step.location.sample_id} + if referenced - sample_ids: + raise ValueError(f"Unknown sample in operation {step.id}.") + + def with_locations( + self, containers: tuple[ContainerSpec, ...], locations: Mapping[str, WellRef] + ) -> ProtocolPlan: + """Bind every sample exactly once and return a plan ready to render.""" + if set(locations) != {s.id for s in self.samples}: + raise ValueError("Every sample must have exactly one placement.") + if len(set(locations.values())) != len(locations): + raise ValueError("Two samples cannot occupy the same location.") + container_ids = {c.id for c in containers} + if len(container_ids) != len(containers): + raise ValueError("Container ids must be unique.") + if any( + location.container_id not in container_ids + for location in locations.values() + ): + raise ValueError("Placements must reference declared containers.") + return replace( + self, + containers=containers, + samples=tuple(replace(s, location=locations[s.id]) for s in self.samples), + ) + + def output_manifest(self) -> OutputManifest: + """Return the located outputs for use as inputs to the next protocol.""" + samples = {s.id: s for s in self.samples} + return OutputManifest( + protocol_id=self.id, + samples=tuple(samples[sid] for sid in self.output_sample_ids), + ) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class OutputManifest: + """Planned output samples, retaining identities, lineage and physical wells.""" + + protocol_id: str + samples: tuple[Sample, ...] + + def __post_init__(self) -> None: + if len({s.id for s in self.samples}) != len(self.samples): + raise ValueError("Manifest sample ids must be unique.") + if any(s.location is None for s in self.samples): + raise ValueError("Manifest samples need a physical location.") + if len({s.location for s in self.samples}) != len(self.samples): + raise ValueError("Manifest sample locations must be unique.") + + def to_dict(self) -> dict: + """Serialize the manifest without implying that the protocol has run.""" + return { + "schema_version": "1.0", + "protocol_id": self.protocol_id, + "state": "planned", + "outputs": [ + { + "sample_id": s.id, + "material_identity": s.material.identity, + "label": s.material.label, + "parent_sample_ids": list(s.parent_ids), + "replicate": s.replicate, + "source_sample_id": s.source_sample_id, + "contents": list(s.contents), + "dilution": s.dilution, + "container_id": s.location.container_id, + "well_name": s.location.well_name, + } + for s in self.samples + ], + } + + def _require_single_container(self) -> None: + """Well-name-only exports cannot distinguish different source plates.""" + if len({s.location.container_id for s in self.samples}) > 1: + raise ValueError( + "This location format cannot represent multiple containers." + ) + + def plasmid_locations(self) -> dict[str, list[str]]: + """Group source wells by plasmid identity, preserving replicate order.""" + self._require_single_container() + result = {} + for sample in self.samples: + result.setdefault(sample.material.identity, []).append( + sample.location.well_name + ) + return result + + def bacterium_locations(self) -> dict[str, list[str]]: + """Export the contents of each culture well for downstream plating.""" + self._require_single_container() + return { + s.location.well_name: list(s.contents or (s.material.label,)) + for s in self.samples + } + + +def validate_slots(slots: tuple[str, ...]) -> None: + """Reject slot overlap with another resource, the thermocycler or trash.""" + if len(set(slots)) != len(slots) or not set(slots) <= { + "1", + "2", + "3", + "4", + "5", + "6", + "9", + }: + raise ValueError("Deck slots collide or overlap the thermocycler/trash.") diff --git a/src/buildcompiler/protocols/plans.py b/src/buildcompiler/protocols/plans.py deleted file mode 100644 index 008eee3..0000000 --- a/src/buildcompiler/protocols/plans.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Immutable logical plans with explicit input and output sample identities.""" - -from dataclasses import dataclass - -from buildcompiler.protocols.materials import Sample -from buildcompiler.protocols.steps import Distribute, Mix, Step, Transfer - - -@dataclass(frozen=True, slots=True, kw_only=True) -class ProtocolPlan: - id: str - request_id: str - samples: tuple[Sample, ...] - input_sample_ids: tuple[str, ...] - output_sample_ids: tuple[str, ...] - steps: tuple[Step, ...] - - def __post_init__(self) -> None: - sample_ids = {sample.id for sample in self.samples} - if len(sample_ids) != len(self.samples): - raise ValueError("Sample ids must be unique within a plan.") - if len({step.id for step in self.steps}) != len(self.steps): - raise ValueError("Step ids must be unique within a plan.") - if not set(self.input_sample_ids + self.output_sample_ids) <= sample_ids: - raise ValueError("Plan inputs and outputs must reference declared samples.") - for sample in self.samples: - if not set(sample.parent_ids) <= sample_ids: - raise ValueError(f"Unknown parent for sample {sample.id}.") - for step in self.steps: - referenced = set() - if isinstance(step, Transfer): - referenced = {step.source.sample_id, step.destination.sample_id} - elif isinstance(step, Distribute): - referenced = { - step.source.sample_id, - *(d.sample_id for d in step.destinations), - } - if step.track_liquid_sample_id: - referenced.add(step.track_liquid_sample_id) - elif isinstance(step, Mix): - referenced = {step.location.sample_id} - if referenced - sample_ids: - raise ValueError(f"Unknown sample in operation {step.id}.") diff --git a/src/buildcompiler/protocols/steps.py b/src/buildcompiler/protocols/steps.py deleted file mode 100644 index b2d6258..0000000 --- a/src/buildcompiler/protocols/steps.py +++ /dev/null @@ -1,129 +0,0 @@ -"""Serializable operations; no live robot or SBOL objects belong here.""" - -from dataclasses import dataclass -import math -from typing import Literal - -from buildcompiler.protocols.materials import SamplePoint - - -@dataclass(frozen=True, slots=True, kw_only=True) -class Transfer: - id: str - source: SamplePoint - destination: SamplePoint - volume_ul: float - aspiration_rate: float = 0.5 - dispense_rate: float = 1.0 - mix_before_ul: float = 0.0 - mix_repetitions: int = 3 - new_tip: bool = True - drop_tip: bool = True - blow_out: bool = True - touch_tip: bool = True - limit_to_pipette_capacity: bool = False - instrument: str = "pipette" - - -@dataclass(frozen=True, slots=True, kw_only=True) -class DropTip: - id: str - instrument: str = "pipette" - - -@dataclass(frozen=True, slots=True, kw_only=True) -class PickUpTip: - id: str - instrument: str = "pipette" - - -@dataclass(frozen=True, slots=True, kw_only=True) -class Mix: - id: str - location: SamplePoint - volume_ul: float - repetitions: int - instrument: str = "pipette" - - -@dataclass(frozen=True, slots=True, kw_only=True) -class Distribute: - id: str - source: SamplePoint - destinations: tuple[SamplePoint, ...] - volume_ul: float - instrument: str = "large" - disposal_volume_ul: float = 0 - new_tip: Literal["once", "never"] = "once" - mix_before: tuple[int, float] | None = None - air_gap_ul: float | None = None - track_liquid_sample_id: str | None = None - - -@dataclass(frozen=True, slots=True, kw_only=True) -class TemperatureStep: - celsius: float - minutes: float - - def __post_init__(self) -> None: - if ( - not math.isfinite(self.celsius) - or not math.isfinite(self.minutes) - or self.minutes < 0 - ): - raise ValueError( - "Temperature steps require finite temperatures and nonnegative durations." - ) - - -@dataclass(frozen=True, slots=True, kw_only=True) -class TemperatureProgram: - steps: tuple[TemperatureStep, ...] - repetitions: int - block_max_volume_ul: float - - -@dataclass(frozen=True, slots=True, kw_only=True) -class RunTemperatureProgram: - id: str - program: TemperatureProgram - skip_during_simulation: bool = False - - -@dataclass(frozen=True, slots=True, kw_only=True) -class SetTemperature: - id: str - module: Literal["source", "reaction", "lid"] - celsius: float - skip_during_simulation: bool = False - - -@dataclass(frozen=True, slots=True, kw_only=True) -class LidAction: - id: str - action: Literal["open", "close"] - - -@dataclass(frozen=True, slots=True, kw_only=True) -class DeactivateSourceModule: - id: str - - -@dataclass(frozen=True, slots=True, kw_only=True) -class OperatorAction: - id: str - instruction: str - - -Step = ( - Transfer - | PickUpTip - | Mix - | Distribute - | DropTip - | RunTemperatureProgram - | SetTemperature - | LidAction - | DeactivateSourceModule - | OperatorAction -) diff --git a/src/buildcompiler/protocols/validation.py b/src/buildcompiler/protocols/validation.py deleted file mode 100644 index 33719e6..0000000 --- a/src/buildcompiler/protocols/validation.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Small reusable configuration validators, independent of execution backends.""" - -import math -from collections.abc import Mapping -from dataclasses import fields, replace - - -def positive(value: object, name: str) -> None: - if type(value) not in (int, float) or not math.isfinite(value) or value <= 0: - raise ValueError(f"{name} must be a finite positive number.") - - -def positive_integer(value: object, name: str) -> None: - if type(value) is not int or value < 1: - raise ValueError(f"{name} must be a positive integer.") - - -class ConfigOverrides: - """Replace only explicitly supplied fields, including False and defaults.""" - - __slots__ = () - - def with_overrides(self, overrides: Mapping[str, object]): - unknown = set(overrides) - {field.name for field in fields(self)} - if unknown: - raise ValueError(f"Unknown configuration fields: {sorted(unknown)}") - return replace(self, **overrides) diff --git a/src/buildcompiler/robotutils.py b/src/buildcompiler/robotutils.py index e053b54..286d3cd 100644 --- a/src/buildcompiler/robotutils.py +++ b/src/buildcompiler/robotutils.py @@ -8,7 +8,7 @@ from pathlib import Path from typing import Any, Dict, List -from buildcompiler.adapters.native_protocols import compile_plating_payload +from buildcompiler.api.protocols import compile_plating_json def load_json_or_dict(value): @@ -172,7 +172,7 @@ def write_manual_plating_protocol(path, plate_id, plate_rows, advanced_params): def write_plating_protocol_script(path, plating_data, advanced_params): path = Path(path) - compiled = compile_plating_payload(plating_data, advanced_params=advanced_params) + compiled = compile_plating_json(plating_data, advanced_params=advanced_params) script = compiled.script path.write_text(script, encoding="utf-8") return path diff --git a/src/buildcompiler/stages/assembly_lvl1.py b/src/buildcompiler/stages/assembly_lvl1.py index 8447dcb..414bbe7 100644 --- a/src/buildcompiler/stages/assembly_lvl1.py +++ b/src/buildcompiler/stages/assembly_lvl1.py @@ -8,7 +8,7 @@ import sbol2 from buildcompiler.adapters.pudu import assembly_route_to_pudu_json -from buildcompiler.adapters.protocol_requests import assembly_request_from_route +from buildcompiler.protocols.inputs import assembly_request_from_route from buildcompiler.api.options import BuildOptions from buildcompiler.domain import ( BuildRequest, diff --git a/src/buildcompiler/stages/assembly_lvl2.py b/src/buildcompiler/stages/assembly_lvl2.py index 61901cb..0d2f28f 100644 --- a/src/buildcompiler/stages/assembly_lvl2.py +++ b/src/buildcompiler/stages/assembly_lvl2.py @@ -8,7 +8,7 @@ import sbol2 from buildcompiler.adapters.pudu import assembly_route_to_pudu_json -from buildcompiler.adapters.protocol_requests import assembly_request_from_route +from buildcompiler.protocols.inputs import assembly_request_from_route from buildcompiler.api.options import BuildOptions from buildcompiler.domain import ( BuildRequest, diff --git a/tests/automation/test_assembly_equivalence.py b/tests/automation/test_assembly_equivalence.py index 0de5889..0ba222a 100644 --- a/tests/automation/test_assembly_equivalence.py +++ b/tests/automation/test_assembly_equivalence.py @@ -11,7 +11,7 @@ ProtocolCompiler, assembly_request_from_json, ) -from buildcompiler.protocols.backends.opentrons.simulation import ( +from buildcompiler.protocols.backends.simulation import ( SimulationError, simulate_source, ) diff --git a/tests/automation/test_plating_equivalence.py b/tests/automation/test_plating_equivalence.py index 636fd8d..519b9ee 100644 --- a/tests/automation/test_plating_equivalence.py +++ b/tests/automation/test_plating_equivalence.py @@ -12,8 +12,8 @@ ProtocolCompiler, bacterium_manifest_from_json, ) -from buildcompiler.protocols.allocation.wells import PLATE_96 -from buildcompiler.protocols.backends.opentrons.simulation import simulate_source +from buildcompiler.protocols.backends.simulation import simulate_source +from buildcompiler.protocols.models import PLATE_96 pytestmark = pytest.mark.automation @@ -83,10 +83,8 @@ def test_plating_equivalence(pudu_repository, tmp_path, count, config, profile): ) assert actual.actions and actual.actions == expected.actions assert actual.sdk_calls == expected.sdk_calls - handoff = next( - a for a in compiled.artifacts.artifacts if a.name == "plating_layout.json" - ) - assert expected.generated_json["plating_layout.json"] == json.loads(handoff.content) + handoff = compiled.files["plating_layout.json"] + assert expected.generated_json["plating_layout.json"] == json.loads(handoff) assert actual.generated_json == {} print( f"{len(actual.actions)} actions; {len(actual.sdk_calls)} SDK calls; handoff equal" diff --git a/tests/automation/test_transformation_equivalence.py b/tests/automation/test_transformation_equivalence.py index a817148..f9ff68e 100644 --- a/tests/automation/test_transformation_equivalence.py +++ b/tests/automation/test_transformation_equivalence.py @@ -12,7 +12,7 @@ plasmid_manifest_from_json, transformation_request_from_json, ) -from buildcompiler.protocols.backends.opentrons.simulation import simulate_source +from buildcompiler.protocols.backends.simulation import simulate_source pytestmark = pytest.mark.automation diff --git a/tests/automation/test_workflow_equivalence.py b/tests/automation/test_workflow_equivalence.py index 34f4917..a3912d5 100644 --- a/tests/automation/test_workflow_equivalence.py +++ b/tests/automation/test_workflow_equivalence.py @@ -15,7 +15,7 @@ assembly_request_from_json, transformation_request_from_json, ) -from buildcompiler.protocols.backends.opentrons.simulation import simulate_source +from buildcompiler.protocols.backends.simulation import simulate_source pytestmark = pytest.mark.automation @@ -107,11 +107,7 @@ def compare(name, reference_source, compiled): plating, ) assert expected_plating["plating_layout.json"] == json.loads( - next( - a.content - for a in plating.artifacts.artifacts - if a.name == "plating_layout.json" - ) + plating.files["plating_layout.json"] ) assert [len(p.manifest.samples) for p in (assembly, transformation, plating)] == [ 4, @@ -119,12 +115,8 @@ def compare(name, reference_source, compiled): 32, ] assert { - s.source_sample_id - for s in transformation.allocation.protocol.samples - if s.role == "dna" + s.source_sample_id for s in transformation.plan.samples if s.role == "dna" } == {s.id for s in assembly.manifest.samples} assert { - s.source_sample_id - for s in plating.allocation.protocol.samples - if s.role == "bacteria" + s.source_sample_id for s in plating.plan.samples if s.role == "bacteria" } == {s.id for s in transformation.manifest.samples} diff --git a/tests/unit/adapters/test_native_protocols.py b/tests/unit/api/test_protocols.py similarity index 68% rename from tests/unit/adapters/test_native_protocols.py rename to tests/unit/api/test_protocols.py index a85a49c..4a9a9e7 100644 --- a/tests/unit/adapters/test_native_protocols.py +++ b/tests/unit/api/test_protocols.py @@ -2,29 +2,23 @@ import pytest -from buildcompiler.adapters.native_protocols import compile_plating_payload +from buildcompiler.api.protocols import compile_plating_json def test_legacy_plating_overrides_are_explicit_and_use_native_compilation(): - compiled = compile_plating_payload( + compiled = compile_plating_json( {"bacterium_locations": {"A2": "strain"}, "replicates": 2}, advanced_params={"replicates": 1, "number_dilutions": 1}, ) assert len(compiled.manifest.samples) == 1 assert "from pudu" not in compiled.script assert ".distribute(" in compiled.script - resolved = json.loads( - next( - a.content - for a in compiled.artifacts.artifacts - if a.name == "compilation.json" - ) - ) + resolved = json.loads(compiled.files["compilation.json"]) assert resolved["configuration"]["replicates"] == 1 def test_unsupported_legacy_parameters_fail_instead_of_being_ignored(): with pytest.raises(ValueError, match="Unsupported native plating parameters"): - compile_plating_payload( + compile_plating_json( {"bacterium_locations": {"A1": "strain"}}, advanced_params={"unknown": 1} ) diff --git a/tests/unit/protocols/test_compilation.py b/tests/unit/protocols/test_compilation.py index 7b1e0e9..430662d 100644 --- a/tests/unit/protocols/test_compilation.py +++ b/tests/unit/protocols/test_compilation.py @@ -15,9 +15,8 @@ ProtocolCompiler, assembly_request_from_json, ) -from buildcompiler.protocols.allocation.wells import BLOCK_24, PLATE_96 from buildcompiler.protocols.backends.markdown import render_markdown -from buildcompiler.protocols.steps import Transfer +from buildcompiler.protocols.models import BLOCK_24, PLATE_96, Transfer def payload(): @@ -53,7 +52,7 @@ def test_compile_is_deterministic_and_never_writes(tmp_path, monkeypatch): ] assert imports == ["opentrons"] with pytest.raises(dataclasses.FrozenInstanceError): - first.allocation.protocol.id = "changed" + first.plan.id = "changed" def test_config_overrides_preserve_explicit_default_and_false(): @@ -88,7 +87,7 @@ def test_order_identity_replicates_and_locations_survive_compilation(): request(), profile=OpentronsAssemblyProfile(thermocycler_starting_well=9), ) - plan = compiled.allocation.protocol + plan = compiled.plan samples = {sample.id: sample for sample in plan.samples} outputs = [samples[identity] for identity in plan.output_sample_ids] assert [output.replicate for output in outputs] == [1, 2] @@ -149,22 +148,18 @@ def test_manual_render_and_script_use_the_same_configuration(): compiled = ProtocolCompiler(assembly=AssemblyConfig(replicates=2)).compile( request() ) - transfers = [ - step - for step in compiled.allocation.protocol.steps - if isinstance(step, Transfer) - ] + transfers = [step for step in compiled.plan.steps if isinstance(step, Transfer)] assert len(transfers) == compiled.markdown.count("Transfer ") assert "replicate 1" in compiled.markdown and "replicate 2" in compiled.markdown assert "reactions/A1" in compiled.markdown and "reactions/B1" in compiled.markdown assert "75" in compiled.markdown assert "not an execution record" in compiled.markdown - assert render_markdown(compiled.allocation.protocol) + assert render_markdown(compiled.plan) def test_compiled_artifacts_require_explicit_write_and_refuse_overwrite(tmp_path): compiled = ProtocolCompiler().compile(request()) - paths = compiled.artifacts.write(tmp_path) + paths = compiled.write(tmp_path) assert {path.name for path in paths} == { "protocol.py", "compilation.json", @@ -177,7 +172,7 @@ def test_compiled_artifacts_require_explicit_write_and_refuse_overwrite(tmp_path == compiled.manifest.to_dict() ) with pytest.raises(FileExistsError): - compiled.artifacts.write(tmp_path) + compiled.write(tmp_path) def test_stage_to_compiled_protocol_preserves_serializable_requests(): diff --git a/tests/unit/protocols/test_transformation_plating.py b/tests/unit/protocols/test_transformation_plating.py index 4d3ae7a..83b4f3c 100644 --- a/tests/unit/protocols/test_transformation_plating.py +++ b/tests/unit/protocols/test_transformation_plating.py @@ -15,7 +15,7 @@ plasmid_manifest_from_json, transformation_request_from_json, ) -from buildcompiler.protocols.steps import Distribute, RunTemperatureProgram +from buildcompiler.protocols.models import Distribute, RunTemperatureProgram def request(): @@ -53,7 +53,7 @@ def test_source_replicates_lineage_and_explicit_locations(): profile=OpentronsTransformationProfile(thermocycler_starting_well=8), ) assert len(compiled.manifest.samples) == 6 - assert [p.location.well_name for p in compiled.manifest.placements] == [ + assert [p.location.well_name for p in compiled.manifest.samples] == [ "A2", "B2", "C2", @@ -61,7 +61,7 @@ def test_source_replicates_lineage_and_explicit_locations(): "E2", "F2", ] - samples = {s.id: s for s in compiled.allocation.protocol.samples} + samples = {s.id: s for s in compiled.plan.samples} first_sources = compiled.manifest.samples[0].parent_ids[1:-1] second_sources = compiled.manifest.samples[3].parent_ids[1:-1] assert [samples[s].source_sample_id for s in first_sources] == [ @@ -72,9 +72,7 @@ def test_source_replicates_lineage_and_explicit_locations(): upstream.samples[1].id, upstream.samples[3].id, ] - dna_locations = { - p.sample_id: p.location.well_name for p in compiled.allocation.placements - } + dna_locations = {p.id: p.location.well_name for p in compiled.plan.samples} assert [dna_locations[s] for s in first_sources] == ["D2", "F3"] assert [dna_locations[s] for s in second_sources] == ["A4", "G8"] @@ -104,11 +102,7 @@ def test_duplicate_physical_sources_are_rejected(): def test_transformation_thermal_steps_are_reviewable_but_simulation_guarded(): compiler = ProtocolCompiler() compiled = compiler.compile(request()) - programs = [ - s - for s in compiled.allocation.protocol.steps - if isinstance(s, RunTemperatureProgram) - ] + programs = [s for s in compiled.plan.steps if isinstance(s, RunTemperatureProgram)] assert len(programs) == 2 and all(p.skip_during_simulation for p in programs) assert "Omitted during Opentrons simulation" in compiled.markdown assert "if not protocol.is_simulating():" in compiled.script @@ -161,14 +155,14 @@ def test_plating_has_lineage_for_dilutions_and_each_replicate(): inputs=transformed.manifest, ) assert len(compiled.manifest.samples) == 8 - samples = {s.id: s for s in compiled.allocation.protocol.samples} + samples = {s.id: s for s in compiled.plan.samples} for output in compiled.manifest.samples: parent = samples[output.parent_ids[0]] assert parent.role == "dilution" and parent.dilution == output.dilution while parent.role == "dilution": parent = samples[parent.parent_ids[0]] assert parent.source_sample_id in {s.id for s in transformed.manifest.samples} - broth = [s for s in compiled.allocation.protocol.steps if isinstance(s, Distribute)] + broth = [s for s in compiled.plan.steps if isinstance(s, Distribute)] assert all(s.new_tip == "never" and s.source.track_conical_height for s in broth) @@ -205,13 +199,7 @@ def test_compilation_is_deterministic_and_has_no_file_or_sdk_effects( if isinstance(n, ast.ImportFrom) ] assert imports == ["opentrons"] - metadata = json.loads( - next( - a.content - for a in compiled.artifacts.artifacts - if a.name == "compilation.json" - ) - ) + metadata = json.loads(compiled.files["compilation.json"]) assert all("kind" in operation for operation in metadata["plan"]["steps"]) From 816280c72b1ea19ee595e3b04fb01ed23b705f88 Mon Sep 17 00:00:00 2001 From: Mike Arpaia Date: Thu, 24 Sep 2026 10:42:14 -0600 Subject: [PATCH 3/5] Add the ability to generate JSON protocols as well --- .github/workflows/ci.yml | 4 +- README.md | 2 +- docs/installation.rst | 15 +- docs/protocols.rst | 98 ++- pyproject.toml | 6 +- setup.py | 12 +- .../adapters/opentrons/simulation.py | 11 +- src/buildcompiler/adapters/protocols.py | 94 ++- src/buildcompiler/api/protocols.py | 22 +- src/buildcompiler/domain/protocol_requests.py | 16 + src/buildcompiler/protocols/__init__.py | 12 +- .../protocols/backends/__init__.py | 71 +- .../protocols/backends/_simulation_worker.py | 22 +- .../protocols/backends/markdown.py | 2 +- .../protocols/backends/opentrons_ot2_json.py | 736 ++++++++++++++++++ .../{opentrons.py => opentrons_ot2_python.py} | 57 +- .../protocols/backends/simulation.py | 73 ++ src/buildcompiler/protocols/compiler.py | 34 +- src/buildcompiler/protocols/inputs.py | 185 ----- .../protocols/methods/__init__.py | 83 +- .../protocols/methods/assembly.py | 142 +++- .../protocols/methods/plating.py | 105 ++- .../protocols/methods/transformation.py | 188 +++-- src/buildcompiler/protocols/models.py | 4 +- src/buildcompiler/stages/assembly_lvl1.py | 2 +- src/buildcompiler/stages/assembly_lvl2.py | 2 +- src/buildcompiler/stages/transformation.py | 4 +- tests/automation/README.md | 14 +- tests/automation/conftest.py | 137 +++- tests/automation/test_assembly_equivalence.py | 10 +- tests/automation/test_json_backend.py | 111 +++ tests/automation/test_plating_equivalence.py | 13 +- .../test_transformation_equivalence.py | 18 +- tests/automation/test_workflow_equivalence.py | 28 +- .../opentrons/test_simulation_boundary.py | 10 +- .../unit/adapters/test_protocol_boundaries.py | 127 ++- tests/unit/execution/test_executor.py | 31 +- tests/unit/protocols/test_compilation.py | 35 + tests/unit/protocols/test_opentrons_json.py | 198 +++++ .../protocols/test_transformation_plating.py | 58 ++ tests/unit/stages/test_assembly_lvl1.py | 4 + tests/unit/stages/test_assembly_lvl2.py | 4 + uv.lock | 30 +- 43 files changed, 2333 insertions(+), 497 deletions(-) create mode 100644 src/buildcompiler/protocols/backends/opentrons_ot2_json.py rename src/buildcompiler/protocols/backends/{opentrons.py => opentrons_ot2_python.py} (93%) delete mode 100644 src/buildcompiler/protocols/inputs.py create mode 100644 tests/automation/test_json_backend.py create mode 100644 tests/unit/protocols/test_opentrons_json.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74ef409..e772401 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - name: Install package run: | python -m pip install -U pip - python -m pip install -e ".[test]" + python -m pip install -e ".[test,automation]" python -m pip install ruff - name: Ruff check run: >- @@ -75,7 +75,7 @@ jobs: with: python-version: "3.10" - name: Install simulator and test dependencies - run: python -m pip install -e '.[test,simulation]' + run: python -m pip install -e '.[test,automation]' - name: Compare against pinned PUDU in the Opentrons simulator env: PUDU_REPOSITORY: ${{ github.workspace }}/.reference/PUDU diff --git a/README.md b/README.md index 55fd9ee..e8486ec 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ print(result.artifact_bundle.manifest) ``` `MANUAL` writes canonical JSON, a human-readable procedure, and a hash manifest. -`AUTOMATED` additionally generates PUDU Python protocols; setting +`AUTOMATED` additionally generates standalone OT-2 Python protocols and their handoff artifacts; setting `options.protocol.simulate = True` runs `opentrons_simulate` and treats a nonzero exit code as a protocol-stage failure. File-producing modes require an explicit `results_dir` and reject a nonempty directory unless `overwrite=True`. diff --git a/docs/installation.rst b/docs/installation.rst index dafdef2..8ba6b00 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -24,13 +24,20 @@ Install test dependencies: Optional automation dependencies -------------------------------- -Native protocol compilation and PUDU-compatible JSON generation are included in -the core package. To run the pinned simulator or equivalence tests, use Python -3.10 and install: +Python protocol compilation and structured handoffs are included in the core +package. The ``automation`` extra includes JSON protocol compilation, the +Opentrons simulator and inventory integration: .. code-block:: bash - python -m pip install -e ".[simulation,test]" + python -m pip install -e ".[automation]" + +To run the pinned simulator and equivalence tests, use Python 3.10 and add the +test dependencies: + +.. code-block:: bash + + python -m pip install -e ".[test,automation]" The equivalence suite additionally needs a PUDU reference checkout, as described in ``tests/automation/README.md``. Generated native scripts do not need PUDU. diff --git a/docs/protocols.rst b/docs/protocols.rst index 3bc3758..ee2959e 100644 --- a/docs/protocols.rst +++ b/docs/protocols.rst @@ -2,19 +2,19 @@ Native protocol compilation =========================== ``buildcompiler.protocols`` compiles assembly, transformation and plating requests -into review documents, structured handoffs and standalone robot scripts. Planning -and rendering require no robot SDK. Generated scripts import only Opentrons. +into review documents, structured handoffs and standalone OT-2 protocols. Planning +and rendering require no robot SDK. Python protocols import only Opentrons; JSON +protocols embed their labware definitions and require no Python entrypoint. Structure and ownership ----------------------- -The package has two small subpackages. Each method keeps its configuration, -hardware profile, planner and well allocation in one file:: +The package has two small subpackages. Each method keeps its input decoding, +configuration, hardware profile, planner and well allocation in one file:: protocols/ __init__.py compiler.py - inputs.py models.py methods/ __init__.py @@ -24,26 +24,74 @@ hardware profile, planner and well allocation in one file:: backends/ __init__.py markdown.py - opentrons.py + opentrons_ot2_python.py + opentrons_ot2_json.py simulation.py _simulation_worker.py ``domain.protocol_requests`` defines immutable requests shared with build stages. -``inputs.py`` converts existing JSON and indexed stage records into those requests. +Each method module converts its JSON or indexed stage records into those requests. +``MaterialRef.from_identity`` supplies shared identity and display-label handling. +``methods.__init__`` shares reaction-list validation, sequential well placement and +upstream well binding. Methods retain their field decoding, sample selection, +allocation policy and operation order. ``models.py`` holds ``Sample``, shared operations such as ``Transfer`` and ``Distribute``, ``ProtocolPlan`` and ``OutputManifest``. A sample keeps its material identity and parent sample IDs independently of its label and physical location. ``ProtocolCompiler.plan(request)`` calls the appropriate method planner and returns a plan with unassigned sample locations. ``compile(request, profile=...)`` also -allocates wells, then sends that same plan to both renderers. The Opentrons renderer -emits Python directly from the operations and checks tip use and pipette capacity. -There is no second command representation between the plan and the script. - -``CompiledProtocol`` contains ``plan``, ``script``, ``markdown``, ``manifest`` and a -read-only ``files`` mapping. ``write(directory)`` is the explicit filesystem +allocates wells, then sends that same plan to Markdown and the selected robot +backend. Both robot backends check tip use and pipette capacity. The Python backend +emits SDK calls; the JSON builder expands operations into validated vendor commands, +including refills, mixing, air gaps, trash movements and module waits. + +``CompiledProtocol`` exposes ``plan``, ``source``, ``backend``, ``protocol_filename``, +``markdown``, ``manifest`` and a read-only ``files`` mapping. ``script`` remains an +alias for the serialized source for existing callers. ``write(directory)`` is the explicit filesystem boundary. Compilation does not write files, simulate or mutate SBOL documents. -``backends.simulation.simulate_source`` runs the optional SDK in a subprocess. +``backends.simulation.analyze_source`` runs either format through the optional +Protocol Engine simulator in a subprocess. ``simulate_source`` retains the +Python SDK trace used by the original acceptance tests. + +Choosing the robot format +------------------------ + +Install ``.[automation]`` for JSON compilation, simulation and inventory integration. +This includes ``opentrons-shared-data==8.8.2`` for offline schemas and labware +definitions. The default backend remains ``opentrons_ot2_python``. +Select ``opentrons_ot2_json`` on ``ProtocolCompiler.compile`` or any of +``compile_assembly``, ``compile_transformation`` and ``compile_plating``: + +.. code-block:: python + + from buildcompiler.protocols import ProtocolCompiler + + compiled = ProtocolCompiler().compile( + request, + backend="opentrons_ot2_json", + ) + compiled.write("results/protocol") # Includes protocol.json. + +Both formats use the same allocated plan, review document and output manifest. +The JSON backend uses protocol schema 8, command schema 10 and labware schema 2, +qualified against Opentrons 8.8.2. It pins labware versions, pipette flow rates and +tip overlap to the Python API 2.21 behavior used by the hardware profiles. +Conical-source heights are computed from planned volume changes at each +distribution boundary, including disposal volumes consumed by refills. + +Simulation remains an explicit action, separate from compilation: + +.. code-block:: python + + from buildcompiler.protocols.backends.simulation import analyze_source + + analysis = analyze_source(compiled.source, format="json") + +The equivalent CLI is ``python -m opentrons.cli analyze protocol.json --check +--json-output analysis.json``. Use Python 3.10 with ``.[automation]`` for either +simulation entrypoint. ``opentrons.simulate.simulate`` does not accept modern JSON +protocols. Compiling stage results ----------------------- @@ -117,9 +165,9 @@ that format contains labels rather than material identities, the adapter assigns local import identities instead of claiming those labels are SBOL URIs. Native manifests retain identity and lineage throughout a workflow. -Each compilation returns ``protocol.py``, ``protocol.md``, ``manifest.json``, +Each compilation returns ``protocol.py`` or ``protocol.json``, plus ``protocol.md``, ``manifest.json``, ``compilation.json`` and a method-specific JSON handoff. ``compilation.json`` -records the resolved configuration, target profile and plan, including operation +records the selected backend, resolved configuration, target profile and plan, including operation kinds and sample locations. These files are also available in memory, for example ``compiled.files["manifest.json"]``. Writing refuses existing files unless ``overwrite=True`` is supplied explicitly. @@ -140,18 +188,22 @@ arbitrary instrument substitution. Transformation and plating each use one rack per pipette and reject batches that exhaust those racks. Assembly retains its explicit rack replacement schedule. -Transformation's temperature operations carry an explicit simulation condition: -they appear in the plan and review document but are skipped during ordinary -simulation. ``water_testing=True`` omits those operations altogether. +Transformation's temperature operations retain the Python simulation condition: +they appear in the plan and review document but are skipped during ordinary Python +simulation. JSON always contains the execution temperature program, and its +analysis exercises it. ``water_testing=True`` omits these operations in both formats. Validation and migration boundary ---------------------------------- -Install ``.[test,simulation]`` in Python 3.10 to run the pinned Opentrons 8.8.2 +Install ``.[test,automation]`` in Python 3.10 to run the pinned Opentrons 8.8.2 acceptance suite described in ``tests/automation/README.md``. The suite compares -the independent PUDU implementation with native output and tests the connected -three-stage handoff. It separately exercises transformation's temperature path -inside the simulator. Hardware and experimental outcomes are not tested. +the independent PUDU implementation with both native formats and tests the connected +three-stage handoff. Protocol Engine comparisons retain resource geometry, positions, +volumes, flow rates, tip boundaries and temperature programs. Transformation's full +temperature path is enabled in both Python references for these comparisons. +The original Python SDK comparisons remain in place. Hardware and experimental +outcomes are not tested. The compiler returns structured artifacts explicitly; it does not produce Excel workbooks or write handoff files during simulation. Legacy orchestration, notebook diff --git a/pyproject.toml b/pyproject.toml index a869d77..1e77bfb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,10 +35,6 @@ dependencies = [ ] [project.optional-dependencies] -simulation = [ - "opentrons==8.8.2 ; python_version == '3.10'", - "xlsxwriter>=3.2.5", -] test = [ "pytest>=7,<9", "pytest-cov[all]" @@ -52,6 +48,8 @@ dev = [ ] automation = [ "opentrons==8.8.2 ; python_version == '3.10'", + "opentrons-shared-data==8.8.2", + "xlsxwriter>=3.2.5", "SBOLInventory @ git+https://github.com/DRAGGON-Lab/SBOLInventory.git ; python_version >= '3.10'", ] notebook = [ diff --git a/setup.py b/setup.py index e62853e..716f284 100644 --- a/setup.py +++ b/setup.py @@ -37,10 +37,6 @@ "pydna", ], extras_require={ - "simulation": [ - "opentrons==8.8.2 ; python_version == '3.10'", - "xlsxwriter>=3.2.5", - ], "test": [ "pytest>=7,<9", "pytest-cov[all]", @@ -54,10 +50,18 @@ ], "automation": [ "opentrons==8.8.2 ; python_version == '3.10'", + "opentrons-shared-data==8.8.2", + "xlsxwriter>=3.2.5", "SBOLInventory @ " "git+https://github.com/DRAGGON-Lab/SBOLInventory.git ; " "python_version >= '3.10'", ], + "notebook": [ + "jupyterlab>=4,<5", + "nbclient>=0.10,<1", + "nbformat>=5,<6", + "ipykernel>=6,<8", + ], }, keywords=["SBOL", "genetic", "automation", "build", "synthetic biology"], classifiers=[ diff --git a/src/buildcompiler/adapters/opentrons/simulation.py b/src/buildcompiler/adapters/opentrons/simulation.py index d0f05af..1c0a121 100644 --- a/src/buildcompiler/adapters/opentrons/simulation.py +++ b/src/buildcompiler/adapters/opentrons/simulation.py @@ -7,7 +7,7 @@ from pathlib import Path from buildcompiler.api.options import ProtocolOptions -from buildcompiler.protocols.backends.simulation import simulate_source +from buildcompiler.protocols.backends.simulation import analyze_source class OptionalAutomationDependencyError(ImportError): @@ -38,14 +38,17 @@ def simulate( if importlib.util.find_spec("opentrons") is None: raise OptionalAutomationDependencyError( - "Install synbio-buildcompiler[simulation] in a Python 3.10 environment to use Opentrons simulation." + "Install synbio-buildcompiler[automation] in a Python 3.10 environment to use Opentrons simulation." ) source = Path(protocol_source) if not source.is_file(): raise FileNotFoundError(f"Protocol source does not exist: {source}") try: - trace = simulate_source(source.read_text(encoding="utf-8")) + trace = analyze_source( + source.read_text(encoding="utf-8"), + format="json" if source.suffix.lower() == ".json" else "python", + ) except RuntimeError as exc: raise ProtocolSimulationError(str(exc)) from exc @@ -56,7 +59,7 @@ def simulate( "protocol_source": str(protocol_source), "simulator_version": trace.simulator_version, "returncode": 0, - "command_count": len(trace.actions), + "command_count": len(trace.data["commands"]), "stderr": trace.stderr, }, ) diff --git a/src/buildcompiler/adapters/protocols.py b/src/buildcompiler/adapters/protocols.py index 157256e..fa983b9 100644 --- a/src/buildcompiler/adapters/protocols.py +++ b/src/buildcompiler/adapters/protocols.py @@ -6,12 +6,14 @@ import importlib.metadata import json import platform +from collections.abc import Mapping from dataclasses import dataclass, field from pathlib import Path from typing import Any from buildcompiler.api.options import ProtocolMode, ProtocolOptions from buildcompiler.domain.build_result import StageResult +from buildcompiler.protocols.compiler import CompiledProtocol, ProtocolCompiler @dataclass @@ -139,37 +141,34 @@ def _package_version(name: str) -> str | None: return None -def _generate_automated_protocol(*, stage: str, payload: object) -> str: - try: - from pudu.generate_protocol import generate_protocol - except ImportError as exc: - from buildcompiler.adapters.opentrons import OptionalAutomationDependencyError - - raise OptionalAutomationDependencyError( - "Install synbio-buildcompiler[automation] to generate automated protocols." - ) from exc - - protocol_type = { - "assembly_lvl1": "assembly", - "assembly_lvl2": "assembly", - "domestication": "assembly", - "transformation": "transformation", - "plating": "plating", - }.get(stage) - if protocol_type is None: +def _compile_protocol(result: StageResult) -> CompiledProtocol: + """Compile typed stage requests, with JSON fallback for older stage producers.""" + from buildcompiler.api.protocols import ( + compile_assembly, + compile_plating_json, + compile_transformation, + ) + from buildcompiler.protocols.methods.assembly import assembly_request_from_json + from buildcompiler.protocols.methods.transformation import ( + transformation_request_from_json, + ) + + stage = result.stage.value + payload = result.json_intermediate + entries = [payload] if isinstance(payload, Mapping) else payload + if stage in {"assembly_lvl1", "assembly_lvl2", "domestication"}: + if result.protocol_requests: + return compile_assembly(result) + request = assembly_request_from_json(entries, request_id=result.id) + elif stage == "transformation": + if result.protocol_requests: + return compile_transformation(result) + request = transformation_request_from_json(entries, request_id=result.id) + elif stage == "plating": + return compile_plating_json(payload) + else: raise ValueError(f"No automated protocol mapping exists for stage: {stage}") - kwargs: dict[str, object] = { - "protocol_data": payload, - "protocol_type": protocol_type, - "metadata": { - "protocolName": f"BuildCompiler {stage.replace('_', ' ').title()}", - "author": "BuildCompiler", - "description": "Generated from a canonical BuildCompiler protocol specification.", - }, - } - if protocol_type == "assembly": - kwargs["assembly_subtype"] = "SBOL" - return generate_protocol(**kwargs) + return ProtocolCompiler().compile(request) def _write_text(path: Path, content: str) -> ProtocolArtifact: @@ -185,10 +184,10 @@ def _write_text(path: Path, content: str) -> ProtocolArtifact: def build_protocol_bundle( *, stage_results: list[StageResult], options: ProtocolOptions ) -> ProtocolBundle: - """Build canonical JSON/manual artifacts at the orchestration boundary.""" + """Write requested stage artifacts and a manifest at the orchestration boundary.""" - payloads = [ - (result.stage.value, result.json_intermediate) + successful_results = [ + result for result in stage_results if result.json_intermediate is not None and result.status.value in {"success", "partial_success"} @@ -202,13 +201,13 @@ def build_protocol_bundle( "software": { "python": platform.python_version(), "synbio-buildcompiler": _package_version("synbio-buildcompiler"), - "pudupy": _package_version("pudupy"), "opentrons": _package_version("opentrons"), }, "artifacts": manifest_entries, } if options.mode == ProtocolMode.NONE: - for index, (stage, payload) in enumerate(payloads, start=1): + for index, result in enumerate(successful_results, start=1): + stage, payload = result.stage.value, result.json_intermediate key = f"{stage}_{index:03d}_json" content = _canonical_json(payload) artifacts[key] = ProtocolArtifact( @@ -231,7 +230,13 @@ def build_protocol_bundle( output_dir.mkdir(parents=True, exist_ok=True) stage_counts: dict[str, int] = {} - for stage, payload in payloads: + for result in successful_results: + stage, payload = result.stage.value, result.json_intermediate + compiled = ( + _compile_protocol(result) + if options.mode == ProtocolMode.AUTOMATED + else None + ) stage_counts[stage] = stage_counts.get(stage, 0) + 1 basename = f"{stage}_{stage_counts[stage]:03d}" json_content = _canonical_json(payload) @@ -240,15 +245,24 @@ def build_protocol_bundle( artifacts[f"{basename}_json"] = json_artifact manual_artifact = _write_text( output_dir / f"{basename}.md", - _manual_protocol(stage=stage, payload=payload), + compiled.markdown + if compiled is not None + else _manual_protocol(stage=stage, payload=payload), ) artifacts[f"{basename}_manual"] = manual_artifact - if options.mode == ProtocolMode.AUTOMATED: + if compiled is not None: script_artifact = _write_text( output_dir / f"{basename}.py", - _generate_automated_protocol(stage=stage, payload=payload), + compiled.source, ) artifacts[f"{basename}_automated"] = script_artifact + # Native protocols carry their expected handoffs as explicit artifacts. + for name, content in compiled.files.items(): + if name in {compiled.protocol_filename, "protocol.md"}: + continue + artifacts[f"{basename}_{Path(name).stem}"] = _write_text( + output_dir / f"{basename}_{name}", content + ) if options.simulate: from buildcompiler.adapters.opentrons import OpentronsSimulationAdapter @@ -256,7 +270,7 @@ def build_protocol_bundle( script_artifact.path, options=options, ) - log_content = "".join(simulation.logs) + log_content = "\n".join(simulation.logs) + "\n" log_artifact = _write_text( output_dir / f"{basename}.simulate.log", log_content ) diff --git a/src/buildcompiler/api/protocols.py b/src/buildcompiler/api/protocols.py index a876f56..c0b3c11 100644 --- a/src/buildcompiler/api/protocols.py +++ b/src/buildcompiler/api/protocols.py @@ -18,7 +18,8 @@ ProtocolCompiler, TransformationConfig, ) -from buildcompiler.protocols.inputs import bacterium_manifest_from_json +from buildcompiler.protocols.backends import ProtocolBackend +from buildcompiler.protocols.methods.plating import bacterium_manifest_from_json from buildcompiler.protocols.models import OutputManifest @@ -27,6 +28,7 @@ def compile_assembly( *, config: AssemblyConfig = AssemblyConfig(), profile: OpentronsAssemblyProfile = OpentronsAssemblyProfile(), + backend: ProtocolBackend = "opentrons_ot2_python", ) -> CompiledProtocol: """Allocate the stage's complete batch once and return in-memory artifacts.""" if result.status not in (StageStatus.SUCCESS, StageStatus.PARTIAL_SUCCESS): @@ -45,7 +47,9 @@ def compile_assembly( request = AssemblyRequest( id=result.id, reactions=reactions, source_stage_id=result.id ) - return ProtocolCompiler(assembly=config).compile(request, profile=profile) + return ProtocolCompiler(assembly=config).compile( + request, profile=profile, backend=backend + ) def compile_transformation( @@ -54,6 +58,7 @@ def compile_transformation( config: TransformationConfig = TransformationConfig(), profile: OpentronsTransformationProfile = OpentronsTransformationProfile(), inputs: OutputManifest | None = None, + backend: ProtocolBackend = "opentrons_ot2_python", ) -> CompiledProtocol: """Compile the complete transformation batch against an optional assembly manifest.""" if result.status not in (StageStatus.SUCCESS, StageStatus.PARTIAL_SUCCESS): @@ -75,7 +80,7 @@ def compile_transformation( ), ) return ProtocolCompiler(transformation=config).compile( - request, profile=profile, inputs=inputs + request, profile=profile, inputs=inputs, backend=backend ) @@ -85,6 +90,7 @@ def compile_plating( request_id: str = "plating", config: PlatingConfig = PlatingConfig(), profile: OpentronsPlatingProfile = OpentronsPlatingProfile(), + backend: ProtocolBackend = "opentrons_ot2_python", ) -> CompiledProtocol: """Compile plating for the exact physical samples in a transformation manifest.""" request = PlatingRequest( @@ -93,11 +99,13 @@ def compile_plating( sample_ids=tuple(sample.id for sample in inputs.samples), ) return ProtocolCompiler(plating=config).compile( - request, profile=profile, inputs=inputs + request, profile=profile, inputs=inputs, backend=backend ) -def compile_plating_json(payload, *, advanced_params=None): +def compile_plating_json( + payload, *, advanced_params=None, backend: ProtocolBackend = "opentrons_ot2_python" +): """Compile a JSON plating payload with explicitly supplied overrides.""" parameters = dict(payload) @@ -118,4 +126,6 @@ def compile_plating_json(payload, *, advanced_params=None): raise ValueError( f"Unsupported native plating parameters: {sorted(unknown)}. Use a supported target profile." ) - return compile_plating(inputs, request_id=name, config=config, profile=profile) + return compile_plating( + inputs, request_id=name, config=config, profile=profile, backend=backend + ) diff --git a/src/buildcompiler/domain/protocol_requests.py b/src/buildcompiler/domain/protocol_requests.py index 07b3bb5..ce77d40 100644 --- a/src/buildcompiler/domain/protocol_requests.py +++ b/src/buildcompiler/domain/protocol_requests.py @@ -1,6 +1,7 @@ """Immutable protocol inputs shared by build stages and protocol compilers.""" from dataclasses import dataclass +from urllib.parse import urlsplit @dataclass(frozen=True, slots=True, kw_only=True) @@ -14,6 +15,21 @@ def __post_init__(self) -> None: if not self.identity or not self.label: raise ValueError("Material identity and label must be nonempty.") + @classmethod + def from_identity(cls, identity: str, label: str | None = None) -> "MaterialRef": + """Keep the full identity and derive a display label when none is supplied.""" + if not isinstance(identity, str) or not identity: + raise ValueError("Material identities must be nonempty strings.") + if label is None: + segments = [part for part in urlsplit(identity).path.split("/") if part] + # Treat a trailing numeric path segment as an SBOL version for display. + label = ( + segments[-2] + if len(segments) > 1 and segments[-1].isdigit() + else (segments[-1] if segments else identity) + ) + return cls(identity=identity, label=label) + @dataclass(frozen=True, slots=True, kw_only=True) class AssemblyReaction: diff --git a/src/buildcompiler/protocols/__init__.py b/src/buildcompiler/protocols/__init__.py index e05a921..7321a57 100644 --- a/src/buildcompiler/protocols/__init__.py +++ b/src/buildcompiler/protocols/__init__.py @@ -8,24 +8,23 @@ TransformationReaction, TransformationRequest, ) +from buildcompiler.protocols.backends import ProtocolBackend from buildcompiler.protocols.compiler import CompiledProtocol, ProtocolCompiler -from buildcompiler.protocols.inputs import ( - assembly_request_from_json, - bacterium_manifest_from_json, - plasmid_manifest_from_json, - transformation_request_from_json, -) from buildcompiler.protocols.methods.assembly import ( AssemblyConfig, OpentronsAssemblyProfile, + assembly_request_from_json, ) from buildcompiler.protocols.methods.plating import ( OpentronsPlatingProfile, PlatingConfig, + bacterium_manifest_from_json, ) from buildcompiler.protocols.methods.transformation import ( OpentronsTransformationProfile, TransformationConfig, + plasmid_manifest_from_json, + transformation_request_from_json, ) from buildcompiler.protocols.models import OutputManifest, ProtocolPlan @@ -37,6 +36,7 @@ "MaterialRef", "OpentronsAssemblyProfile", "ProtocolCompiler", + "ProtocolBackend", "ProtocolPlan", "assembly_request_from_json", "TransformationConfig", diff --git a/src/buildcompiler/protocols/backends/__init__.py b/src/buildcompiler/protocols/backends/__init__.py index f0a0733..7953541 100644 --- a/src/buildcompiler/protocols/backends/__init__.py +++ b/src/buildcompiler/protocols/backends/__init__.py @@ -1 +1,70 @@ -"""Render shared plans and explicitly run the optional robot simulator.""" +"""Shared OT-2 target contracts and rendering utilities.""" + +from typing import Literal + +from buildcompiler.protocols.methods.assembly import OpentronsAssemblyProfile +from buildcompiler.protocols.methods.plating import OpentronsPlatingProfile +from buildcompiler.protocols.methods.transformation import ( + OpentronsTransformationProfile, +) +from buildcompiler.protocols.models import ( + PLATE_96, + Distribute, + PickUpTip, + ProtocolPlan, + Transfer, +) + +ProtocolBackend = Literal["opentrons_ot2_python", "opentrons_ot2_json"] + +# Colors are display metadata; all lookups use sample or material identities. +COLORS = ( + "#4040BF", + "#BF4040", + "#40BF40", + "#A640BF", + "#BFBF40", + "#BF7340", + "#40BFBF", + "#BF40A6", + "#73BF40", + "#4073BF", + "#BF8C40", + "#40BF73", + "#7340BF", + "#A6BF40", + "#BF5940", + "#40A6BF", + "#BF4073", + "#59BF40", + "#BFA640", + "#40BFA6", + "#8CBF40", + "#40BF59", + "#40BF8C", + "#BF40A6", +) + +TargetProfile = ( + OpentronsAssemblyProfile | OpentronsTransformationProfile | OpentronsPlatingProfile +) + + +def validate_tips(plan: ProtocolPlan, starts: dict[str, str | None]) -> None: + """Reject a batch that would exhaust a pipette's single on-deck rack.""" + counts = dict.fromkeys(starts, 0) + for step in plan.steps: + if ( + isinstance(step, PickUpTip) + or isinstance(step, Transfer) + and step.new_tip + or isinstance(step, Distribute) + and step.new_tip == "once" + ): + counts[step.instrument] += 1 + for instrument, count in counts.items(): + available = 96 - PLATE_96.index(starts[instrument] or "A1") + if count > available: + raise ValueError( + f"{instrument} requires {count} tips but only {available} remain." + ) diff --git a/src/buildcompiler/protocols/backends/_simulation_worker.py b/src/buildcompiler/protocols/backends/_simulation_worker.py index 311f5c0..d3530f4 100644 --- a/src/buildcompiler/protocols/backends/_simulation_worker.py +++ b/src/buildcompiler/protocols/backends/_simulation_worker.py @@ -1,12 +1,13 @@ """Private simulator worker, launched as a file so core imports never load the SDK. -The trace adapter is qualified against Opentrons 8.8.2. SDK calls are -observed, not substituted; the official simulate() entry point runs both sides. +Both entry points are qualified against Opentrons 8.8.2. The SDK trace observes +real calls through simulate(); engine analysis invokes the official analyzer. """ import functools import inspect import json +import runpy import sys from importlib.metadata import version from pathlib import Path @@ -140,7 +141,20 @@ def wrapper(*args, **kwargs): if __name__ == "__main__": - # Resolve the installed SDK, not the sibling opentrons.py source renderer. + # Resolve installed packages without exposing sibling backend modules. if sys.path[0] == str(Path(__file__).resolve().parent): del sys.path[0] - main() + if len(sys.argv) > 1 and sys.argv[1] == "analyze": + if version("opentrons") != "8.8.2": + raise RuntimeError("Protocol analysis requires opentrons==8.8.2.") + sys.argv = [ + "opentrons", + "analyze", + sys.argv[2], + "--check", + "--json-output", + "analysis.json", + ] + runpy.run_module("opentrons.cli", run_name="__main__") + else: + main() diff --git a/src/buildcompiler/protocols/backends/markdown.py b/src/buildcompiler/protocols/backends/markdown.py index 6371757..5d28fc3 100644 --- a/src/buildcompiler/protocols/backends/markdown.py +++ b/src/buildcompiler/protocols/backends/markdown.py @@ -89,7 +89,7 @@ def describe(sample_id: str) -> str: else: raise TypeError(f"Unsupported operation: {type(step).__name__}") if getattr(step, "skip_during_simulation", False): - action += " Omitted during Opentrons simulation." + action += " Omitted during Opentrons simulation of the Python artifact; included in JSON analysis." lines.append(f"{index}. {action}") lines.extend(("", "## Planned outputs", "")) for sample_id in plan.output_sample_ids: diff --git a/src/buildcompiler/protocols/backends/opentrons_ot2_json.py b/src/buildcompiler/protocols/backends/opentrons_ot2_json.py new file mode 100644 index 0000000..2eab833 --- /dev/null +++ b/src/buildcompiler/protocols/backends/opentrons_ot2_json.py @@ -0,0 +1,736 @@ +"""Lower allocated plans to self-contained OT-2 JSON protocols. + +Protocol schema 8 and command schema 10 are qualified with Opentrons 8.8.2. +The builder expands SDK conveniences into explicit commands; it never generates +or executes Python. Compilation needs shared-data, not the robot SDK. +""" + +from __future__ import annotations + +import json +import math +from dataclasses import dataclass +from functools import lru_cache +from importlib.metadata import version +from typing import Literal + +from buildcompiler.protocols.backends import COLORS, TargetProfile, validate_tips +from buildcompiler.protocols.methods.assembly import OpentronsAssemblyProfile +from buildcompiler.protocols.methods.plating import OpentronsPlatingProfile +from buildcompiler.protocols.methods.transformation import ( + OpentronsTransformationProfile, +) +from buildcompiler.protocols.models import ( + PLATE_96, + DeactivateSourceModule, + Distribute, + DropTip, + LidAction, + Mix, + OperatorAction, + PickUpTip, + ProtocolPlan, + RunTemperatureProgram, + Sample, + SamplePoint, + SetTemperature, + Transfer, + WellRef, +) + +# Explicit versions reproduce API 2.21 geometry instead of silently adopting +# the newest definitions in the installed labware library. +LABWARE_VERSIONS = { + "opentrons_24_aluminumblock_nest_1.5ml_snapcap": 1, + "nest_96_wellplate_100ul_pcr_full_skirt": 2, + "biorad_96_wellplate_200ul_pcr": 2, + "opentrons_24_tuberack_eppendorf_1.5ml_safelock_snapcap": 1, + "opentrons_15_tuberack_falcon_15ml_conical": 1, + "opentrons_96_tiprack_20ul": 1, + "opentrons_96_filtertiprack_20ul": 1, + "opentrons_96_filtertiprack_200ul": 1, +} + + +@lru_cache(maxsize=1) +def _validators() -> dict: + """Load the pinned offline schemas only when the JSON backend is selected.""" + try: + from jsonschema import Draft7Validator + from opentrons_shared_data import load_shared_data + except ImportError as exc: + raise ImportError( + "Install synbio-buildcompiler[automation] to compile JSON protocols." + ) from exc + if version("opentrons-shared-data") != "8.8.2": + raise RuntimeError("JSON compilation requires opentrons-shared-data==8.8.2.") + schemas = { + name: json.loads(load_shared_data(path)) + for name, path in { + "protocol": "protocol/schemas/8.json", + "command": "command/schemas/10.json", + "labware": "labware/schemas/2.json", + "liquid": "liquid/schemas/1.json", + }.items() + } + result = {name: Draft7Validator(schema) for name, schema in schemas.items()} + # Select the command's branch directly: validating every oneOf branch for + # each pipetting movement is both slow and produces unhelpful errors. + command_schema = schemas["command"] + # Vendor schemas allow extra keys in command parameters. Tighten our authoring + # boundary so a misspelled optional field cannot silently change behavior. + for definition in command_schema["definitions"].values(): + if definition.get("type") == "object" and "properties" in definition: + definition.setdefault("additionalProperties", False) + for reference in command_schema["oneOf"]: + definition = command_schema["definitions"][reference["$ref"].split("/")[-1]] + command_type = definition["properties"]["commandType"]["enum"][0] + result[command_type] = Draft7Validator({**command_schema, "oneOf": [reference]}) + return result + + +@dataclass(slots=True) +class _Pipette: + """Mutable state confined to one compilation, including its tip schedule.""" + + capacity: float + flow_rate: float + racks: tuple[str, ...] + slots: tuple[str, ...] + next_tip: int + attached: bool = False + volume: float = 0 + active_batch: int = 0 + + +@dataclass(frozen=True, slots=True) +class _Point: + """A physical well with a vertical offset, resolved before command emission.""" + + well: WellRef + origin: Literal["bottom", "top"] = "bottom" + z: float = 1 + + def params(self) -> dict: + """Return the location fields shared by liquid-handling commands.""" + return { + "labwareId": self.well.container_id, + "wellName": self.well.well_name, + "wellLocation": { + "origin": self.origin, + "offset": {"x": 0, "y": 0, "z": self.z}, + }, + } + + +class OpentronsProtocolBuilder: + """Build and validate commands for the supported OT-2 hardware profiles.""" + + def __init__(self, plan: ProtocolPlan, profile: TargetProfile) -> None: + self.plan = plan + self.profile = profile + self.samples = {sample.id: sample for sample in plan.samples} + self.commands: list[dict] = [] + self.definitions: dict[str, dict] = {} + self.labware: dict[str, dict] = {} + self.liquids: dict[str, dict] = {} + self.sample_liquids: dict[str, str] = {} + self.volumes: dict[WellRef, float] = {} + self.pipettes: dict[str, _Pipette] = {} + self.validators = _validators() + + def _emit(self, command_type: str, **params) -> None: + """Validate each command at the boundary to the vendor wire format.""" + command = { + "commandType": command_type, + "key": f"command-{len(self.commands):05d}", + "params": params, + } + self.validators[command_type].validate(command) + self.commands.append(command) + + def _load_labware(self, name: str, load_name: str, parent: str) -> None: + """Embed the qualified definition and bind its stable resource identifier.""" + from opentrons_shared_data.labware import load_definition + + if name in self.labware: + raise ValueError(f"Duplicate labware identifier: {name}") + if load_name not in LABWARE_VERSIONS: + raise ValueError(f"Unqualified OT-2 labware definition: {load_name}") + definition = load_definition(load_name, LABWARE_VERSIONS[load_name]) + self.validators["labware"].validate(definition) + self.definitions[f"opentrons/{load_name}/{definition['version']}"] = definition + self.labware[name] = definition + location = ( + {"moduleId": parent} + if parent in ("source_module", "reaction_module") + else "offDeck" + if parent == "offDeck" + else {"slotName": parent} + ) + self._emit( + "loadLabware", + labwareId=name, + loadName=load_name, + namespace="opentrons", + version=definition["version"], + location=location, + ) + + def _well(self, sample: Sample) -> WellRef: + """Reject unallocated samples and references absent from loaded labware.""" + well = sample.location + if well is None: + raise ValueError(f"Sample {sample.id} has not been allocated.") + if well.well_name not in self.labware[well.container_id]["wells"]: + raise ValueError(f"Unknown well: {well}") + return well + + def _point(self, point: SamplePoint) -> _Point: + """Resolve conical source height once per operation, before its refills.""" + well = self._well(self.samples[point.sample_id]) + if point.top_mm is not None: + return _Point(well, "top", point.top_mm) + if point.bottom_mm is not None: + return _Point(well, "bottom", point.bottom_mm) + if point.track_conical_height: + definition = self.labware[well.container_id]["wells"][well.well_name] + volume = self.volumes.get(well) + maximum = definition["totalLiquidVolume"] + if volume is not None and volume >= maximum * 0.2: + height = max(volume / maximum * (definition["depth"] - 10) - 10, 3) + return _Point(well, "bottom", height) + return _Point(well) + + def _liquid( + self, + sample: Sample, + liquid_id: str, + *, + name: str, + color: str, + description: str = "", + ) -> None: + """Define display metadata once and register the initial well volume.""" + if liquid_id not in self.liquids: + liquid = { + "displayName": name, + "description": description, + "displayColor": color, + } + self.validators["liquid"].validate(liquid) + self.liquids[liquid_id] = liquid + self.sample_liquids[sample.id] = liquid_id + self._load_liquid(self._well(sample), liquid_id, sample.initial_volume_ul) + + def _load_liquid(self, well: WellRef, liquid: str, volume: float) -> None: + """Record a liquid annotation and its tracked starting volume.""" + self._emit( + "loadLiquid", + labwareId=well.container_id, + liquidId=liquid, + volumeByWell={well.well_name: volume}, + ) + self.volumes[well] = volume + + def _load_pipette( + self, + name: str, + model: str, + mount: str, + racks: tuple[str, ...], + slots: tuple[str, ...], + initial_tip: str | None, + ) -> None: + """Pin tip overlap and flow rates to the Python API 2.21 behavior.""" + self._emit( + "loadPipette", + pipetteId=name, + pipetteName=model, + mount=mount, + tipOverlapNotAfterVersion="v3", + liquidPresenceDetection=False, + ) + small = model == "p20_single_gen2" + self.pipettes[name] = _Pipette( + capacity=20 if small else 200, + flow_rate=7.56 if small else 92.86, + racks=racks, + slots=slots, + next_tip=PLATE_96.index(initial_tip or "A1"), + ) + + def _setup(self) -> None: + """Load the deck and liquids in the same order as the Python backend.""" + profile = self.profile + containers = {c.id: c for c in self.plan.containers} + + def load(name: str) -> None: + """Resolve one allocated container's hardware declaration.""" + container = containers[name] + self._load_labware(name, container.definition_id, container.parent) + + if isinstance( + profile, (OpentronsAssemblyProfile, OpentronsTransformationProfile) + ): + self._emit( + "loadModule", + moduleId="source_module", + model="temperatureModuleV1", + location={"slotName": profile.temperature_module_position}, + ) + load("sources") + self._emit( + "loadModule", + moduleId="reaction_module", + model="thermocyclerModuleV1", + location={"slotName": "7"}, + ) + load("reactions") + if isinstance(profile, OpentronsAssemblyProfile): + count = sum(isinstance(s, Transfer) and s.new_tip for s in self.plan.steps) + rack_count = (PLATE_96.index(profile.initial_tip) + count + 95) // 96 + slots = profile.tiprack_positions + racks = tuple(f"tips_{i}" for i in range(rack_count)) + for i, rack in enumerate(racks): + self._load_labware( + rack, + "opentrons_96_tiprack_20ul", + slots[i] if i < len(slots) else "offDeck", + ) + self._load_pipette( + "pipette", + "p20_single_gen2", + profile.pipette_position, + racks, + slots, + profile.initial_tip, + ) + else: + if isinstance(profile, OpentronsTransformationProfile): + if "dna_plate" in containers: + load("dna_plate") + load("tube_rack") + starts = { + "small": profile.initial_tip_p20, + "large": profile.initial_tip_p300, + } + slots = (profile.tiprack_p20_position, profile.tiprack_p200_position) + small_rack = "opentrons_96_tiprack_20ul" + elif isinstance(profile, OpentronsPlatingProfile): + starts = { + "small": profile.initial_small_tip, + "large": profile.initial_large_tip, + } + slots = (profile.small_tiprack_position, profile.large_tiprack_position) + small_rack = "opentrons_96_filtertiprack_20ul" + else: + raise TypeError(f"Unsupported profile: {type(profile).__name__}") + validate_tips(self.plan, starts) + self._load_labware("small_tips", small_rack, slots[0]) + self._load_labware( + "large_tips", "opentrons_96_filtertiprack_200ul", slots[1] + ) + for name, model, mount, slot in ( + ("small", "p20_single_gen2", "left", slots[0]), + ("large", "p300_single_gen2", "right", slots[1]), + ): + self._load_pipette( + name, model, mount, (f"{name}_tips",), (slot,), starts[name] + ) + if isinstance(profile, OpentronsPlatingProfile): + load("tube_rack") + + dna_liquids = {} + bacteria_index = 0 + for index, sample_id in enumerate(self.plan.input_sample_ids): + sample = self.samples[sample_id] + liquid_id = f"liquid_{index}" + if isinstance(profile, OpentronsAssemblyProfile): + label = sample.liquid_label or sample.material.label + self._liquid( + sample, + liquid_id, + name=label, + description=label, + color=COLORS[index % len(COLORS)], + ) + elif isinstance(profile, OpentronsTransformationProfile): + if sample.role == "dna": + liquid_id, color_index = dna_liquids.setdefault( + sample.material.identity, (liquid_id, len(dna_liquids)) + ) + self._liquid( + sample, + liquid_id, + name=sample.material.label, + description=f"{sample.material.label} DNA construct", + color=COLORS[color_index % len(COLORS)], + ) + else: + self._liquid( + sample, + liquid_id, + name=sample.liquid_label, + color=COLORS[(sample.replicate - 1) % len(COLORS)], + ) + elif sample.role == "broth": + self._liquid( + sample, + liquid_id, + name="liquid_broth", + description="Liquid broth for dilutions", + color="#D2B48C", + ) + else: + self._liquid( + sample, + liquid_id, + name="transformed_bacteria", + description=sample.liquid_label, + color=COLORS[bacteria_index % len(COLORS)], + ) + bacteria_index += 1 + if isinstance(profile, OpentronsPlatingProfile): + for name in sorted( + (n for n in containers if n.startswith(("dilution_", "agar_"))), + key=lambda n: (not n.startswith("dilution_"), n), + ): + load(name) + for sample in self.plan.samples: + self._well(sample) + + def _require_tip(self, instrument: str) -> _Pipette: + """Check that the instrument exists and owns an attached tip.""" + pipette = self.pipettes[instrument] + if not pipette.attached: + raise ValueError(f"An attached tip is required on {instrument}.") + return pipette + + def _pickup(self, instrument: str) -> None: + """Consume the next tip, pausing for manual rack swaps when needed.""" + pipette = self.pipettes[instrument] + if pipette.attached: + raise ValueError(f"A tip is already attached to {instrument}.") + rack, well = divmod(pipette.next_tip, 96) + if rack >= len(pipette.racks): + raise ValueError(f"No tips remain for {instrument}.") + batch = rack // len(pipette.slots) + if batch != pipette.active_batch: + size = len(pipette.slots) + for old in pipette.racks[ + pipette.active_batch * size : (pipette.active_batch + 1) * size + ]: + self._emit( + "moveLabware", + labwareId=old, + newLocation="offDeck", + strategy="manualMoveWithPause", + ) + for index in range( + batch * size, min((batch + 1) * size, len(pipette.racks)) + ): + self._emit( + "moveLabware", + labwareId=pipette.racks[index], + newLocation={"slotName": pipette.slots[index % size]}, + strategy="manualMoveWithPause", + ) + pipette.active_batch = batch + self._emit( + "pickUpTip", + pipetteId=instrument, + **_Point( + WellRef( + container_id=pipette.racks[rack], well_name=PLATE_96.name(well) + ), + "top", + 0, + ).params(), + ) + pipette.next_tip += 1 + pipette.attached = True + + def _drop(self, instrument: str) -> None: + """Preserve the SDK's alternating fixed-trash drop positions.""" + pipette = self._require_tip(instrument) + self._emit( + "moveToAddressableAreaForDropTip", + pipetteId=instrument, + addressableAreaName="fixedTrash", + alternateDropLocation=True, + ) + self._emit("dropTipInPlace", pipetteId=instrument) + pipette.attached = False + pipette.volume = 0 + + def _pipette( + self, + action: Literal["aspirate", "dispense"], + instrument: str, + point: _Point, + volume: float, + *, + rate: float = 1, + push_out: float | None = None, + air: bool = False, + ) -> None: + """Check tip capacity and update volume accounting for one movement.""" + pipette = self._require_tip(instrument) + after = pipette.volume + (volume if action == "aspirate" else -volume) + if ( + not math.isfinite(volume) + or volume < 0 + or not -1e-8 <= after <= pipette.capacity + 1e-8 + ): + raise ValueError( + f"Operation exceeds supported {instrument} pipette capacity." + ) + params = { + "pipetteId": instrument, + **point.params(), + "volume": volume, + "flowRate": pipette.flow_rate * rate, + } + if push_out is not None: + params["pushOut"] = push_out + self._emit(action, **params) + pipette.volume = max(0, after) + if not air and point.well in self.volumes: + self.volumes[point.well] += -volume if action == "aspirate" else volume + + def _mix( + self, instrument: str, point: _Point, volume: float, repetitions: int + ) -> None: + """Suppress push-out until the final dispense, as API 2.21 mix does.""" + for repetition in range(repetitions): + self._pipette("aspirate", instrument, point, volume) + self._pipette( + "dispense", + instrument, + point, + volume, + push_out=0 if repetition < repetitions - 1 else None, + ) + + def _blow_out(self, instrument: str, point: _Point | None = None) -> None: + """Empty a tip at its current well position or at fixed trash.""" + pipette = self._require_tip(instrument) + if point is None: + self._emit( + "moveToAddressableAreaForDropTip", + pipetteId=instrument, + addressableAreaName="fixedTrash", + offset={"x": 0, "y": 0, "z": 0}, + ) + self._emit( + "blowOutInPlace", pipetteId=instrument, flowRate=pipette.flow_rate + ) + else: + self._emit( + "blowout", + pipetteId=instrument, + **point.params(), + flowRate=pipette.flow_rate, + ) + if point.well in self.volumes: + self.volumes[point.well] += pipette.volume + pipette.volume = 0 + + def _air_gap(self, instrument: str, well: WellRef, volume: float) -> None: + """API 2.21 acquires air five millimetres above the last visited well.""" + point = _Point(well, "top", 5) + self._emit("moveToWell", pipetteId=instrument, **point.params()) + self._pipette("aspirate", instrument, point, volume, air=True) + + def _distribute(self, step: Distribute) -> None: + """Expand uniform destinations into refills, retaining one tip per group.""" + pipette = self.pipettes[step.instrument] + air = step.air_gap_ul or 0 + usable = pipette.capacity - step.disposal_volume_ul - air + if step.volume_ul <= 0 or step.volume_ul > usable or not step.destinations: + raise ValueError( + "Distribution exceeds pipette capacity or has no destinations." + ) + if step.new_tip == "once": + self._pickup(step.instrument) + elif step.new_tip != "never": + raise ValueError(f"Unsupported distribution tip policy: {step.new_tip}") + self._require_tip(step.instrument) + source = self._point(step.source) + destinations = [self._point(p) for p in step.destinations] + # Accumulate in destination order. Floor division can choose a different + # refill boundary for fractional volumes near the pipette's capacity. + groups: list[tuple[list[_Point], float]] = [] + group: list[_Point] = [] + total = 0.0 + for destination in destinations: + if ( + total + step.disposal_volume_ul + air + step.volume_ul + > pipette.capacity + ): + groups.append((group, total)) + group, total = [], 0.0 + group.append(destination) + total += step.volume_ul + groups.append((group, total)) + for group, total in groups: + if step.mix_before and pipette.volume == 0: + repetitions, volume = step.mix_before + self._mix(step.instrument, source, volume, repetitions) + self._pipette( + "aspirate", + step.instrument, + source, + total + step.disposal_volume_ul, + ) + if air: + self._air_gap(step.instrument, source.well, air) + for index, destination in enumerate(group): + self._pipette( + "dispense", step.instrument, destination, step.volume_ul + air + ) + if air and index < len(group) - 1: + self._air_gap(step.instrument, destination.well, air) + if step.disposal_volume_ul: + self._blow_out(step.instrument) + if step.new_tip == "once": + self._drop(step.instrument) + if step.track_liquid_sample_id: + for destination in destinations: + self._load_liquid( + destination.well, + self.sample_liquids[step.track_liquid_sample_id], + step.volume_ul, + ) + + def _operations(self) -> None: + """Lower all planned operations, including the full thermal program.""" + for step in self.plan.steps: + if isinstance(step, Transfer): + if step.new_tip: + self._pickup(step.instrument) + source, destination = ( + self._point(step.source), + self._point(step.destination), + ) + volume = ( + min(step.volume_ul, self.pipettes[step.instrument].capacity) + if step.limit_to_pipette_capacity + else step.volume_ul + ) + if step.mix_before_ul: + self._mix( + step.instrument, + source, + step.mix_before_ul, + step.mix_repetitions, + ) + self._pipette( + "aspirate", + step.instrument, + source, + volume, + rate=step.aspiration_rate, + ) + self._pipette( + "dispense", + step.instrument, + destination, + volume, + rate=step.dispense_rate, + ) + if step.blow_out: + self._blow_out(step.instrument, destination) + if step.touch_tip: + self._emit( + "touchTip", + pipetteId=step.instrument, + **_Point(destination.well, "top", -14).params(), + radius=0.5, + speed=20, + ) + if step.drop_tip: + self._drop(step.instrument) + elif isinstance(step, PickUpTip): + self._pickup(step.instrument) + elif isinstance(step, DropTip): + self._drop(step.instrument) + elif isinstance(step, Mix): + self._mix( + step.instrument, + self._point(step.location), + step.volume_ul, + step.repetitions, + ) + elif isinstance(step, Distribute): + self._distribute(step) + elif isinstance(step, LidAction): + self._emit(f"thermocycler/{step.action}Lid", moduleId="reaction_module") + elif isinstance(step, SetTemperature): + commands = { + "source": ("temperatureModule", "Temperature", "source_module"), + "reaction": ("thermocycler", "BlockTemperature", "reaction_module"), + "lid": ("thermocycler", "LidTemperature", "reaction_module"), + } + module, target, resource = commands[step.module] + params = {"moduleId": resource, "celsius": step.celsius} + if step.module == "reaction": + params["holdTimeSeconds"] = 0 + self._emit(f"{module}/setTarget{target}", **params) + self._emit(f"{module}/waitFor{target}", moduleId=resource) + elif isinstance(step, DeactivateSourceModule): + self._emit("temperatureModule/deactivate", moduleId="source_module") + elif isinstance(step, RunTemperatureProgram): + program = step.program + self._emit( + "thermocycler/runExtendedProfile", + moduleId="reaction_module", + profileElements=[ + { + "steps": [ + {"celsius": s.celsius, "holdSeconds": s.minutes * 60} + for s in program.steps + ], + "repetitions": program.repetitions, + } + ], + blockMaxVolumeUl=program.block_max_volume_ul, + ) + elif isinstance(step, OperatorAction): + self._emit("comment", message=step.instruction) + else: + raise TypeError( + f"Unsupported protocol operation: {type(step).__name__}" + ) + if any(p.attached for p in self.pipettes.values()): + raise ValueError("Protocol ends with an attached tip.") + + def build(self) -> dict: + """Return a validated document; builders are single-use compilation objects.""" + if self.commands: + raise RuntimeError("This protocol builder has already been used.") + self._setup() + self._operations() + document = { + "$otSharedSchema": "#/protocol/schemas/8", + "schemaVersion": 8, + "metadata": {"protocolName": self.plan.id, "author": "BuildCompiler"}, + "robot": {"model": "OT-2 Standard", "deckId": "ot2_standard"}, + "labwareDefinitionSchemaId": "opentronsLabwareSchemaV2", + "labwareDefinitions": self.definitions, + "liquidSchemaId": "opentronsLiquidSchemaV1", + "liquids": self.liquids, + "commandSchemaId": "opentronsCommandSchemaV10", + "commands": self.commands, + "commandAnnotationSchemaId": "opentronsCommandAnnotationSchemaV1", + "commandAnnotations": [], + } + self.validators["protocol"].validate(document) + return document + + +def render_json(plan: ProtocolPlan, *, profile: TargetProfile) -> str: + """Serialize a deterministic, standalone protocol without running a simulator.""" + document = OpentronsProtocolBuilder(plan, profile).build() + return json.dumps(document, indent=2, sort_keys=True, allow_nan=False) + "\n" diff --git a/src/buildcompiler/protocols/backends/opentrons.py b/src/buildcompiler/protocols/backends/opentrons_ot2_python.py similarity index 93% rename from src/buildcompiler/protocols/backends/opentrons.py rename to src/buildcompiler/protocols/backends/opentrons_ot2_python.py index 9e99865..cb27819 100644 --- a/src/buildcompiler/protocols/backends/opentrons.py +++ b/src/buildcompiler/protocols/backends/opentrons_ot2_python.py @@ -3,6 +3,7 @@ import keyword import math +from buildcompiler.protocols.backends import COLORS, TargetProfile, validate_tips from buildcompiler.protocols.methods.assembly import OpentronsAssemblyProfile from buildcompiler.protocols.methods.plating import OpentronsPlatingProfile from buildcompiler.protocols.methods.transformation import ( @@ -25,38 +26,6 @@ Transfer, ) -# Colors are display metadata; all lookups use sample or material identities. -COLORS = ( - "#4040BF", - "#BF4040", - "#40BF40", - "#A640BF", - "#BFBF40", - "#BF7340", - "#40BFBF", - "#BF40A6", - "#73BF40", - "#4073BF", - "#BF8C40", - "#40BF73", - "#7340BF", - "#A6BF40", - "#BF5940", - "#40A6BF", - "#BF4073", - "#59BF40", - "#BFA640", - "#40BFA6", - "#8CBF40", - "#40BF59", - "#40BF8C", - "#BF40A6", -) - -TargetProfile = ( - OpentronsAssemblyProfile | OpentronsTransformationProfile | OpentronsPlatingProfile -) - def _well(sample: Sample, point: SamplePoint | None = None) -> str: """Format a bound well and its optional aspiration or dispense offset.""" @@ -119,26 +88,6 @@ def _assembly_tips( return setup, pickups -def _validate_tips(plan: ProtocolPlan, starts: dict[str, str | None]) -> None: - """Reject a batch that would exhaust a pipette's single on-deck rack.""" - counts = dict.fromkeys(starts, 0) - for step in plan.steps: - if ( - isinstance(step, PickUpTip) - or isinstance(step, Transfer) - and step.new_tip - or isinstance(step, Distribute) - and step.new_tip == "once" - ): - counts[step.instrument] += 1 - for instrument, count in counts.items(): - available = 96 - PLATE_96.index(starts[instrument] or "A1") - if count > available: - raise ValueError( - f"{instrument} requires {count} tips but only {available} remain." - ) - - def _instruments( small_slot: str, large_slot: str, starts: dict[str, str | None], *, filtered: bool ) -> list[str]: @@ -396,7 +345,7 @@ def load(name: str) -> None: load("dna_plate") load("tube_rack") starts = {"small": profile.initial_tip_p20, "large": profile.initial_tip_p300} - _validate_tips(plan, starts) + validate_tips(plan, starts) lines.extend( _instruments( profile.tiprack_p20_position, @@ -440,7 +389,7 @@ def load(name: str) -> None: "small": profile.initial_small_tip, "large": profile.initial_large_tip, } - _validate_tips(plan, starts) + validate_tips(plan, starts) lines.extend( _instruments( profile.small_tiprack_position, diff --git a/src/buildcompiler/protocols/backends/simulation.py b/src/buildcompiler/protocols/backends/simulation.py index 0cd9d57..81f3fba 100644 --- a/src/buildcompiler/protocols/backends/simulation.py +++ b/src/buildcompiler/protocols/backends/simulation.py @@ -7,6 +7,7 @@ import tempfile from dataclasses import dataclass from pathlib import Path +from typing import Literal class SimulationError(RuntimeError): @@ -25,6 +26,78 @@ class SimulationResult: stderr: str +@dataclass(frozen=True, slots=True, kw_only=True) +class ProtocolAnalysis: + """Protocol Engine results for either a Python or a JSON robot artifact.""" + + simulator_version: str + data: dict + generated_json: dict[str, object] + stdout: str + stderr: str + + +def analyze_source( + source: str, + *, + format: Literal["python", "json"] = "python", + python: str | Path = sys.executable, + python_paths: tuple[str | Path, ...] = (), + timeout: float = 120, +) -> ProtocolAnalysis: + """Run the real Protocol Engine analyzer, with failures treated as errors. + + JSON schema 8 requires this entry point. Python sources must be trusted; + the child process isolates SDK state and files, not arbitrary code execution. + """ + if format not in ("python", "json"): + raise ValueError(f"Unsupported protocol format: {format!r}") + worker = Path(__file__).with_name("_simulation_worker.py") + with tempfile.TemporaryDirectory(prefix="buildcompiler-analysis-") as temporary: + directory = Path(temporary) + filename = "protocol.json" if format == "json" else "protocol.py" + (directory / filename).write_text(source, encoding="utf-8") + environment = os.environ.copy() + environment["OT_API_CONFIG_DIR"] = str(directory / "config") + environment["PYTHONDONTWRITEBYTECODE"] = "1" + if python_paths: + environment["PYTHONPATH"] = os.pathsep.join( + str(Path(p).resolve()) for p in python_paths + ) + try: + process = subprocess.run( + [str(python), str(worker), "analyze", filename], + cwd=directory, + env=environment, + capture_output=True, + text=True, + timeout=timeout, + check=False, + ) + except (OSError, subprocess.TimeoutExpired) as exc: + raise SimulationError(f"Could not run Opentrons analysis: {exc}") from exc + result = directory / "analysis.json" + if process.returncode or not result.exists(): + raise SimulationError( + f"Opentrons analysis failed ({process.returncode}):\n{process.stderr}\n{process.stdout}" + ) + data = json.loads(result.read_text(encoding="utf-8")) + if data["result"] != "ok" or data["errors"]: + raise SimulationError(f"Opentrons analysis failed: {data['errors']}") + generated = { + path.name: json.loads(path.read_text(encoding="utf-8")) + for path in directory.glob("*.json") + if path.name not in ("protocol.json", "analysis.json") + } + return ProtocolAnalysis( + simulator_version="8.8.2", + data=data, + generated_json=generated, + stdout=process.stdout, + stderr=process.stderr, + ) + + def simulate_source( source: str, *, diff --git a/src/buildcompiler/protocols/compiler.py b/src/buildcompiler/protocols/compiler.py index 036528f..2532d2c 100644 --- a/src/buildcompiler/protocols/compiler.py +++ b/src/buildcompiler/protocols/compiler.py @@ -1,4 +1,4 @@ -"""Compile requests into plans, review documents and standalone robot scripts.""" +"""Compile requests into plans, review documents and standalone robot protocols.""" import json from collections.abc import Mapping @@ -12,8 +12,9 @@ ProtocolRequest, TransformationRequest, ) +from buildcompiler.protocols.backends import ProtocolBackend, TargetProfile from buildcompiler.protocols.backends.markdown import render_markdown -from buildcompiler.protocols.backends.opentrons import TargetProfile, render_python +from buildcompiler.protocols.backends.opentrons_ot2_python import render_python from buildcompiler.protocols.methods.assembly import ( AssemblyConfig, OpentronsAssemblyProfile, @@ -50,6 +51,19 @@ class CompiledProtocol: markdown: str manifest: OutputManifest files: Mapping[str, str] + backend: ProtocolBackend = "opentrons_ot2_python" + + @property + def protocol_filename(self) -> str: + """Name of the robot artifact in this bundle.""" + return ( + "protocol.json" if self.backend == "opentrons_ot2_json" else "protocol.py" + ) + + @property + def source(self) -> str: + """Serialized robot protocol, in the selected backend's format.""" + return self.script def __post_init__(self) -> None: for name in self.files: @@ -113,8 +127,11 @@ def compile( *, profile: TargetProfile | None = None, inputs: OutputManifest | None = None, + backend: ProtocolBackend = "opentrons_ot2_python", ) -> CompiledProtocol: """Return an allocated plan and files, leaving writing and simulation explicit.""" + if backend not in ("opentrons_ot2_python", "opentrons_ot2_json"): + raise ValueError(f"Unsupported protocol backend: {backend!r}") plan = self.plan(request, inputs=inputs) if isinstance(request, AssemblyRequest): profile = profile or OpentronsAssemblyProfile() @@ -148,12 +165,20 @@ def compile( plan = allocate_plating(plan, profile=profile, inputs=inputs) handoff_name = "plating_layout.json" handoff = plating_layout(plan, dilution_factor=config.dilution_factor) - script = render_python(plan, profile=profile) + if backend == "opentrons_ot2_json": + from buildcompiler.protocols.backends.opentrons_ot2_json import render_json + + script = render_json(plan, profile=profile) + protocol_filename = "protocol.json" + else: + script = render_python(plan, profile=profile) + protocol_filename = "protocol.py" markdown = render_markdown(plan) manifest = plan.output_manifest() metadata = { "schema_version": "1.0", "method": method, + "backend": backend, "configuration": asdict(config), "target_profile": asdict(profile), "plan": { @@ -168,8 +193,9 @@ def compile( script=script, markdown=markdown, manifest=manifest, + backend=backend, files={ - "protocol.py": script, + protocol_filename: script, "protocol.md": markdown, "manifest.json": _json(manifest.to_dict()), "compilation.json": _json(metadata), diff --git a/src/buildcompiler/protocols/inputs.py b/src/buildcompiler/protocols/inputs.py deleted file mode 100644 index fa5593c..0000000 --- a/src/buildcompiler/protocols/inputs.py +++ /dev/null @@ -1,185 +0,0 @@ -"""Decode protocol inputs and stage records without importing robot dependencies.""" - -from collections.abc import Mapping, Sequence -from urllib.parse import urlsplit - -from buildcompiler.domain import IndexedBackbone, IndexedPlasmid, IndexedReagent -from buildcompiler.domain.protocol_requests import ( - AssemblyReaction, - AssemblyRequest, - MaterialRef, - TransformationReaction, - TransformationRequest, -) -from buildcompiler.protocols.models import PLATE_96, OutputManifest, Sample, WellRef - - -def material_ref(identity: str, label: str | None = None) -> MaterialRef: - """Keep the full identity and derive a display label when none is supplied.""" - - if not isinstance(identity, str) or not identity: - raise ValueError("Material identities must be nonempty strings.") - if label is None: - segments = [part for part in urlsplit(identity).path.split("/") if part] - label = ( - segments[-2] - if len(segments) > 1 and segments[-1].isdigit() - else (segments[-1] if segments else identity) - ) - return MaterialRef(identity=identity, label=label) - - -def assembly_request_from_json( - payload: Sequence[Mapping[str, object]], - *, - request_id: str, - source_stage_id: str | None = None, -) -> AssemblyRequest: - """Decode products and ordered components into an assembly batch.""" - if isinstance(payload, (str, bytes)) or not isinstance(payload, Sequence): - raise TypeError("Assembly JSON must be a sequence of reaction objects.") - reactions = [] - for index, entry in enumerate(payload): - if not isinstance(entry, Mapping): - raise TypeError("Each assembly entry must be an object.") - required = {"Product", "Backbone", "PartsList", "Restriction Enzyme"} - if missing := required - entry.keys(): - raise ValueError(f"Assembly {index} is missing {sorted(missing)}.") - parts = entry["PartsList"] - if not isinstance(parts, (list, tuple)): - raise TypeError("PartsList must be an ordered list.") - reactions.append( - AssemblyReaction( - id=f"{request_id}/reaction/{index}", - product=material_ref(entry["Product"]), - backbone=material_ref(entry["Backbone"]), - parts=tuple(material_ref(part) for part in parts), - restriction_enzyme=material_ref(entry["Restriction Enzyme"]), - ) - ) - return AssemblyRequest( - id=request_id, reactions=tuple(reactions), source_stage_id=source_stage_id - ) - - -def transformation_request_from_json( - payload: Sequence[Mapping[str, object]], - *, - request_id: str, - source_stage_id: str | None = None, -) -> TransformationRequest: - """Decode strains, chassis and ordered plasmids into a transformation batch.""" - - if isinstance(payload, (str, bytes)) or not isinstance(payload, Sequence): - raise TypeError("Transformation JSON must be a sequence of reaction objects.") - reactions = [] - for index, entry in enumerate(payload): - if not isinstance(entry, Mapping): - raise TypeError("Each transformation entry must be an object.") - if missing := {"Strain", "Chassis", "Plasmids"} - entry.keys(): - raise ValueError(f"Transformation {index} is missing {sorted(missing)}.") - if not isinstance(entry["Plasmids"], (list, tuple)): - raise TypeError("Plasmids must be an ordered list.") - reactions.append( - TransformationReaction( - id=f"{request_id}/reaction/{index}", - strain=material_ref(entry["Strain"]), - chassis=material_ref(entry["Chassis"]), - plasmids=tuple(material_ref(p) for p in entry["Plasmids"]), - ) - ) - return TransformationRequest( - id=request_id, reactions=tuple(reactions), source_stage_id=source_stage_id - ) - - -def plasmid_manifest_from_json( - payload: Mapping[str, list[str]], *, protocol_id: str = "imported-assembly" -) -> OutputManifest: - """Import identity-to-well mappings as distinct physical source replicates.""" - - samples = [] - for index, (identity, wells) in enumerate(payload.items()): - if not isinstance(wells, (list, tuple)) or not wells: - raise ValueError("Each plasmid requires a nonempty list of source wells.") - for replicate, well in enumerate(wells, 1): - PLATE_96.index(well) - sample = Sample( - id=f"{protocol_id}/{index}/{replicate}", - material=material_ref(identity), - replicate=replicate, - location=WellRef(container_id="source_plate", well_name=well), - ) - samples.append(sample) - return OutputManifest(protocol_id=protocol_id, samples=tuple(samples)) - - -def bacterium_manifest_from_json( - payload: Mapping, *, protocol_id: str = "imported-transformation" -) -> OutputManifest: - """Import culture-well labels, assigning local identities when none are present.""" - - locations = payload.get("bacterium_locations") - if not isinstance(locations, Mapping) or not locations: - raise ValueError("Plating JSON requires nonempty bacterium_locations.") - samples = [] - for index, (well, contents) in enumerate(locations.items()): - PLATE_96.index(well) - if isinstance(contents, str): - labels = (contents,) - elif ( - isinstance(contents, (list, tuple)) - and contents - and all(isinstance(v, str) for v in contents) - ): - labels = tuple(contents) - else: - raise ValueError( - "Bacterium contents must be a string or nonempty list of strings." - ) - # The legacy format has labels only: do not pretend they are SBOL identities. - sample = Sample( - id=f"{protocol_id}/{index}", - material=MaterialRef( - identity=f"urn:buildcompiler:imported:{protocol_id}:{index}", - label=", ".join(labels), - ), - contents=labels, - liquid_label=str(contents), - location=WellRef(container_id="source_plate", well_name=well), - ) - samples.append(sample) - return OutputManifest(protocol_id=protocol_id, samples=tuple(samples)) - - -def assembly_request_from_route( - *, - stage_id: str, - products: Sequence[IndexedPlasmid], - parts: Sequence[IndexedPlasmid], - backbone: IndexedBackbone, - restriction_enzyme: IndexedReagent, -) -> AssemblyRequest: - """Use actual produced identities and preserve the selected component order.""" - - def material(record) -> MaterialRef: - """Keep indexed identities and choose the most specific display label.""" - return MaterialRef( - identity=record.identity, - label=record.name or record.display_id or record.identity, - ) - - return AssemblyRequest( - id=stage_id, - source_stage_id=stage_id, - reactions=tuple( - AssemblyReaction( - id=f"{stage_id}/product/{index}", - product=material(product), - backbone=material(backbone), - parts=tuple(material(part) for part in parts), - restriction_enzyme=material(restriction_enzyme), - ) - for index, product in enumerate(products) - ), - ) diff --git a/src/buildcompiler/protocols/methods/__init__.py b/src/buildcompiler/protocols/methods/__init__.py index e1937a8..d7f990d 100644 --- a/src/buildcompiler/protocols/methods/__init__.py +++ b/src/buildcompiler/protocols/methods/__init__.py @@ -1 +1,82 @@ -"""Method-specific configurations, planners and well allocation.""" +"""Lab methods and their shared input validation and well binding. + +Each method module keeps its input decoders, configuration and hardware profile +beside its planner and allocator. Planners describe samples and operations without +well locations; allocators return a new plan with those locations filled in. +The helpers here share input-shape validation and well binding while each method +chooses its sample order, replicate expansion and liquid-handling steps. +""" + +from collections.abc import Iterable, Iterator, Mapping, Sequence + +from buildcompiler.protocols.models import OutputManifest, Sample, WellGrid, WellRef + + +def reaction_entries( + payload: object, *, method: str, required: set[str] +) -> Iterator[tuple[int, Mapping[str, object]]]: + """Validate the reaction-list envelope and yield entries in their input order. + + Methods still decode their own fields. Validation happens entry by entry so + errors in an earlier reaction are reported before inspecting later ones. + """ + if isinstance(payload, (str, bytes)) or not isinstance(payload, Sequence): + raise TypeError( + f"{method.capitalize()} JSON must be a sequence of reaction objects." + ) + for index, entry in enumerate(payload): + if not isinstance(entry, Mapping): + raise TypeError(f"Each {method} entry must be an object.") + if missing := required - entry.keys(): + raise ValueError( + f"{method.capitalize()} {index} is missing {sorted(missing)}." + ) + yield index, entry + + +def sequential_wells( + sample_ids: Iterable[str], + *, + container_id: str, + grid: WellGrid, + start: int = 0, +) -> dict[str, WellRef]: + """Place samples in the supplied order, rejecting positions outside the grid. + + The method chooses the sample order and starting offset. Existing plan + validation guarantees unique sample IDs and checks the combined placements. + """ + return { + sample_id: WellRef(container_id=container_id, well_name=grid.name(index)) + for index, sample_id in enumerate(sample_ids, start) + } + + +def bind_source_wells( + samples: Iterable[Sample], + inputs: OutputManifest, + *, + container_id: str, + grid: WellGrid, +) -> dict[str, WellRef]: + """Bind local samples to one upstream plate, preserving their source wells. + + Only the selected samples participate in the single-plate check. The new + container ID describes the plate's role in this protocol; source sample IDs + identify the physical aliquots even when material identities are shared. + """ + upstream = {sample.id: sample.location for sample in inputs.samples} + containers = set() + locations = {} + for sample in samples: + if sample.source_sample_id not in upstream: + raise ValueError(f"Missing upstream location for sample {sample.id}.") + source = upstream[sample.source_sample_id] + grid.index(source.well_name) + containers.add(source.container_id) + locations[sample.id] = WellRef( + container_id=container_id, well_name=source.well_name + ) + if len(containers) != 1: + raise ValueError("Mapped samples must come from one source plate.") + return locations diff --git a/src/buildcompiler/protocols/methods/assembly.py b/src/buildcompiler/protocols/methods/assembly.py index 1358dee..7da974d 100644 --- a/src/buildcompiler/protocols/methods/assembly.py +++ b/src/buildcompiler/protocols/methods/assembly.py @@ -1,9 +1,21 @@ -"""Assembly method: configuration, planning and well allocation.""" +"""Plan assembly reactions from shared stocks, then place them on the deck. + +Sources have a deterministic stock order, while reactions and their components +retain request order. Keeping those orders separate makes shared stocks reusable +without changing the sequence of additions within a reaction. +""" import math +from collections.abc import Mapping, Sequence from dataclasses import dataclass -from buildcompiler.domain.protocol_requests import AssemblyRequest, MaterialRef +from buildcompiler.domain import IndexedBackbone, IndexedPlasmid, IndexedReagent +from buildcompiler.domain.protocol_requests import ( + AssemblyReaction, + AssemblyRequest, + MaterialRef, +) +from buildcompiler.protocols.methods import reaction_entries, sequential_wells from buildcompiler.protocols.models import ( BLOCK_24, PLATE_96, @@ -22,7 +34,6 @@ TemperatureProgram, TemperatureStep, Transfer, - WellRef, positive, positive_integer, validate_slots, @@ -31,7 +42,12 @@ @dataclass(frozen=True, slots=True, kw_only=True) class AssemblyConfig(ConfigOverrides): - """Reaction volumes, replicate count and assembly incubation programs.""" + """Reaction volumes in microliters, replicate count and incubation programs. + + Rates are pipette speed multipliers. Water testing retains liquid-handling + steps but omits temperature control. Program block volumes are explicit + thermocycler settings, independent of the calculated reaction volume. + """ volume_total_reaction: float = 20 volume_part: float = 2 @@ -107,7 +123,12 @@ def __post_init__(self) -> None: @dataclass(frozen=True, slots=True, kw_only=True) class OpentronsAssemblyProfile: - """Assembly deck slots, pipette mount and starting well/tip offsets.""" + """Assembly deck slots, pipette mount and starting well/tip offsets. + + Reaction offsets are zero-based, column-major indices; tips use well names. + Tiprack positions are the slots available at once. The renderer schedules + rack replacements when the batch needs more tips than fit on deck. + """ api_level: str = "2.21" temperature_module_position: str = "1" @@ -145,8 +166,78 @@ def max_volume_ul(self) -> float: LIGASE = MaterialRef(identity="urn:buildcompiler:ligase", label="T4 DNA Ligase") +def assembly_request_from_json( + payload: Sequence[Mapping[str, object]], + *, + request_id: str, + source_stage_id: str | None = None, +) -> AssemblyRequest: + """Decode products and ordered components into an assembly batch.""" + reactions = [] + for index, entry in reaction_entries( + payload, + method="assembly", + required={"Product", "Backbone", "PartsList", "Restriction Enzyme"}, + ): + parts = entry["PartsList"] + if not isinstance(parts, (list, tuple)): + raise TypeError("PartsList must be an ordered list.") + reactions.append( + AssemblyReaction( + id=f"{request_id}/reaction/{index}", + product=MaterialRef.from_identity(entry["Product"]), + backbone=MaterialRef.from_identity(entry["Backbone"]), + parts=tuple(MaterialRef.from_identity(part) for part in parts), + restriction_enzyme=MaterialRef.from_identity( + entry["Restriction Enzyme"] + ), + ) + ) + return AssemblyRequest( + id=request_id, reactions=tuple(reactions), source_stage_id=source_stage_id + ) + + +def assembly_request_from_route( + *, + stage_id: str, + products: Sequence[IndexedPlasmid], + parts: Sequence[IndexedPlasmid], + backbone: IndexedBackbone, + restriction_enzyme: IndexedReagent, +) -> AssemblyRequest: + """Use actual produced identities and preserve the selected component order.""" + + def material(record) -> MaterialRef: + """Keep indexed identities and choose the most specific display label.""" + return MaterialRef( + identity=record.identity, + label=record.name or record.display_id or record.identity, + ) + + return AssemblyRequest( + id=stage_id, + source_stage_id=stage_id, + reactions=tuple( + AssemblyReaction( + id=f"{stage_id}/product/{index}", + product=material(product), + backbone=material(backbone), + parts=tuple(material(part) for part in parts), + restriction_enzyme=material(restriction_enzyme), + ) + for index, product in enumerate(products) + ), + ) + + def plan_assembly(request: AssemblyRequest, *, config: AssemblyConfig) -> ProtocolPlan: - """Expand ordered reactions into a logical plan with stable sample lineage.""" + """Expand each requested reaction into complete, consecutive replicates. + + Each stock material has one input sample shared by all reactions that use + it. Output parent IDs describe the DNA components; reagent additions remain + explicit transfers in the plan. Locations are assigned separately. + """ enzymes = {} parts = {} for reaction in request.reactions: @@ -156,7 +247,9 @@ def plan_assembly(request: AssemblyRequest, *, config: AssemblyConfig) -> Protoc if previous is not None and previous != material: raise ValueError(f"Conflicting labels for {material.identity}.") parts[material.identity] = material - # Sort labels for stable source positions; retain identities for lookups. + # Deduplicate stocks by identity, then sort within each reagent group for + # stable source positions. Identical display labels still denote distinct + # stocks when their identities differ. ordered = ( WATER, BUFFER, @@ -202,6 +295,8 @@ def plan_assembly(request: AssemblyRequest, *, config: AssemblyConfig) -> Protoc + config.volume_t4_dna_ligase + config.volume_restriction_enzyme ) + # The backbone consumes the same per-component volume as each part; + # water fills the balance after all components and reagents are counted. water_volume = ( config.volume_total_reaction - fixed_volume @@ -211,6 +306,8 @@ def plan_assembly(request: AssemblyRequest, *, config: AssemblyConfig) -> Protoc raise ValueError( f"Reaction {reaction.id} has no remaining volume for water." ) + # Tuple order is execution order; the final flag requests source mixing. + # Component order follows the request, independently of stock positions. additions = ( (WATER, water_volume, False), (BUFFER, config.volume_t4_dna_ligase_buffer, True), @@ -240,7 +337,9 @@ def plan_assembly(request: AssemblyRequest, *, config: AssemblyConfig) -> Protoc drop_tip=addition_index != len(additions) - 1, ) ) - # Finish mixing with the tip retained from the final addition. + # Retain the final addition's tip for these explicit movements. + # Their bottom offsets and finishing actions differ from an SDK + # mix call; the renderer caps each movement to pipette capacity. for mix_index in range(int(config.volume_total_reaction / 10)): steps.append( Transfer( @@ -291,7 +390,12 @@ def plan_assembly(request: AssemblyRequest, *, config: AssemblyConfig) -> Protoc def allocate_assembly( plan: ProtocolPlan, *, profile: OpentronsAssemblyProfile ) -> ProtocolPlan: - """Assign sources and reactions in column-major order on their plates.""" + """Assign sources and reactions in column-major order on separate labware. + + Source order comes from stock sorting; output order comes from reaction and + replicate expansion. The starting offset reserves earlier reaction wells + without shifting sources or changing sample identities. + """ if len(plan.input_sample_ids) > BLOCK_24.capacity: raise ValueError("Assembly source block capacity exceeded.") if ( @@ -299,18 +403,16 @@ def allocate_assembly( > PLATE_96.capacity ): raise ValueError("Assembly reaction plate capacity exceeded.") - locations = { - sample_id: WellRef(container_id="sources", well_name=BLOCK_24.name(i)) - for i, sample_id in enumerate(plan.input_sample_ids) - } + locations = sequential_wells( + plan.input_sample_ids, container_id="sources", grid=BLOCK_24 + ) locations.update( - { - sample_id: WellRef( - container_id="reactions", - well_name=PLATE_96.name(i + profile.thermocycler_starting_well), - ) - for i, sample_id in enumerate(plan.output_sample_ids) - } + sequential_wells( + plan.output_sample_ids, + container_id="reactions", + grid=PLATE_96, + start=profile.thermocycler_starting_well, + ) ) return plan.with_locations( ( diff --git a/src/buildcompiler/protocols/methods/plating.py b/src/buildcompiler/protocols/methods/plating.py index 9d050d1..61ff680 100644 --- a/src/buildcompiler/protocols/methods/plating.py +++ b/src/buildcompiler/protocols/methods/plating.py @@ -1,9 +1,17 @@ -"""Plating method: configuration, planning and well allocation.""" +"""Plan dilution chains and plated replicates, then allocate their wells. + +Each selected upstream culture remains a distinct source. Dilutions form a +lineage chain from that source, while plated replicates branch from each dilution. +Dilution wells and agar spots are allocated independently because only the latter +multiply with the plating replicate count. +""" from collections import Counter +from collections.abc import Mapping from dataclasses import dataclass from buildcompiler.domain.protocol_requests import MaterialRef, PlatingRequest +from buildcompiler.protocols.methods import bind_source_wells from buildcompiler.protocols.models import ( PLATE_96, ConfigOverrides, @@ -29,7 +37,12 @@ @dataclass(frozen=True, slots=True, kw_only=True) class PlatingConfig(ConfigOverrides): - """Dilution volumes, spotting replicates and liquid-handling rates.""" + """Volumes in microliters, spots per dilution and pipette speed multipliers. + + ``volume_total_reaction`` declares the starting volume of each source culture; + dilution volumes are derived separately from the transfer and dilution factor. + ``max_colonies`` bounds planned spots across all sources and dilutions. + """ volume_total_reaction: float = 20 volume_bacteria_transfer: float = 2 @@ -61,6 +74,8 @@ def __post_init__(self) -> None: ) if self.dilution_factor <= 1 or self.dilution_volume <= 1: raise ValueError("Dilution factor and dilution volume must exceed one.") + # The first dilution must supply its own spots and, when present, seed + # the second dilution. Each dilution supplies all of its plated replicates. required = self.volume_colony * self.replicates if self.number_dilutions == 2: required += self.volume_bacteria_transfer @@ -90,7 +105,12 @@ def mix_volume(self) -> float: @dataclass(frozen=True, slots=True, kw_only=True) class OpentronsPlatingProfile: - """Plating source labware, deck slots and starting tip/tube positions.""" + """Plating source labware, deck slots and starting tip/tube positions. + + The source labware definition is explicit: a manifest's well names do not + identify its physical plate model. ``lb_tube_position`` is a zero-based, + column-major rack index; starting tips use well names. + """ api_level: str = "2.21" thermocycler_labware: str = "biorad_96_wellplate_200ul_pcr" @@ -134,10 +154,54 @@ def __post_init__(self) -> None: ) +def bacterium_manifest_from_json( + payload: Mapping, *, protocol_id: str = "imported-transformation" +) -> OutputManifest: + """Import culture-well labels, assigning local identities when none are present.""" + + locations = payload.get("bacterium_locations") + if not isinstance(locations, Mapping) or not locations: + raise ValueError("Plating JSON requires nonempty bacterium_locations.") + samples = [] + for index, (well, contents) in enumerate(locations.items()): + PLATE_96.index(well) + if isinstance(contents, str): + labels = (contents,) + elif ( + isinstance(contents, (list, tuple)) + and contents + and all(isinstance(v, str) for v in contents) + ): + labels = tuple(contents) + else: + raise ValueError( + "Bacterium contents must be a string or nonempty list of strings." + ) + # The legacy format has labels only: do not pretend they are SBOL identities. + sample = Sample( + id=f"{protocol_id}/{index}", + material=MaterialRef( + identity=f"urn:buildcompiler:imported:{protocol_id}:{index}", + label=", ".join(labels), + ), + contents=labels, + liquid_label=str(contents), + location=WellRef(container_id="source_plate", well_name=well), + ) + samples.append(sample) + return OutputManifest(protocol_id=protocol_id, samples=tuple(samples)) + + def plan_plating( request: PlatingRequest, *, config: PlatingConfig, inputs: tuple[Sample, ...] ) -> ProtocolPlan: - """Build dilution intermediates and plated replicates with explicit lineage.""" + """Build dilution intermediates and plated replicates with explicit lineage. + + Request sample IDs select exact upstream aliquots in the requested order; + cultures sharing a material identity are still handled separately. All broth + fills precede culture transfers. Each culture's dilution chain is completed + before its spots are dispensed, with tip ownership explicit in the steps. + """ available = {s.id: s for s in inputs} if set(request.sample_ids) - available.keys(): @@ -171,6 +235,8 @@ def plan_plating( ) sources.append(source) parent = source + # Each dilution points to its immediate culture parent and the broth. + # Spots branch from that dilution, so replicates share its preparation. for dilution in range(1, config.number_dilutions + 1): diluted = Sample( id=f"{request.id}/dilution/{index}/{dilution}", @@ -197,11 +263,15 @@ def plan_plating( LidAction(id="open-lid", action="open"), PickUpTip(id="broth-tip", instrument="large"), ] + # Fill one dilution level across all cultures before moving to the next. + # This matches plate allocation even though lineage was built per culture. ordered_dilutions = [ dilutions[index, dilution] for dilution in range(1, config.number_dilutions + 1) for index in range(len(sources)) ] + # Distribution groups share the explicitly acquired broth tip. Source height + # is resolved at runtime as the broth volume changes between groups. for start in range(0, len(ordered_dilutions), 8): steps.append( Distribute( @@ -220,7 +290,7 @@ def plan_plating( steps.append(DropTip(id="broth-tip-drop", instrument="large")) def transfer(id, source, destination, volume, *, agar=False): - """Keep the current tip and use the agar offset only for spotting.""" + """Use shared rates and caller-owned tips, adding offset/blow-out for agar.""" return Transfer( id=id, source=SamplePoint(sample_id=source.id), @@ -294,12 +364,17 @@ def transfer(id, source, destination, volume, *, agar=False): def allocate_plating( plan: ProtocolPlan, *, profile: OpentronsPlatingProfile, inputs: OutputManifest ) -> ProtocolPlan: - """Place each dilution in a plate half or on its own plate when needed.""" + """Preserve source wells and pack new samples by role and dilution level. + + Each pair of dilution levels shares one plate when both fit in a half plate; + otherwise each level gets a separate plate. Dilution and agar plate counts + can differ because agar wells also expand with the number of replicates. + """ - upstream = {p.id: p.location for p in inputs.samples} selected = [s for s in plan.samples if s.role == "bacteria"] - if len({upstream[s.source_sample_id].container_id for s in selected}) != 1: - raise ValueError("The plating target accepts one source plate.") + sources = bind_source_wells( + selected, inputs, container_id="reactions", grid=PLATE_96 + ) containers = [ ContainerSpec( id="reactions", @@ -324,9 +399,7 @@ def allocate_plating( if sample.initial_volume_ul > 15000: raise ValueError("Broth loading exceeds tube capacity.") elif sample.role == "bacteria": - well = upstream[sample.source_sample_id].well_name - PLATE_96.index(well) - location = WellRef(container_id="reactions", well_name=well) + location = sources[sample.id] elif sample.role in ("dilution", "agar"): per_dilution = counts[sample.role, sample.dilution] if per_dilution > 96: @@ -338,6 +411,7 @@ def allocate_plating( index = indices[sample.role, sample.dilution] indices[sample.role, sample.dilution] += 1 if sample.dilution == 2 and not two_plates: + # Column-major index 48 is A7, the start of the second half. index += 48 name = f"{sample.role}_{plate}" if name not in {c.id for c in containers}: @@ -356,7 +430,11 @@ def allocate_plating( def plating_layout(plan: ProtocolPlan, *, dilution_factor: float) -> dict: - """Export agar wells by tracing each spot back to its source culture.""" + """Export agar wells with their original culture wells and cumulative ratios. + + Lineage resolves the source even for spots taken from later dilutions. Stable + grouping by dilution retains the request's culture and replicate order. + """ samples = {s.id: s for s in plan.samples} locations = {s.id: s.location for s in plan.samples} @@ -365,6 +443,7 @@ def plating_layout(plan: ProtocolPlan, *, dilution_factor: float) -> dict: # Group by dilution while preserving source and replicate order. for sample in sorted(outputs, key=lambda s: s.dilution): source = samples[sample.parent_ids[0]] + # The first parent follows the culture chain; the other parent is broth. while source.role == "dilution": source = samples[source.parent_ids[0]] location = locations[sample.id] diff --git a/src/buildcompiler/protocols/methods/transformation.py b/src/buildcompiler/protocols/methods/transformation.py index 6a7b6ab..67ecd00 100644 --- a/src/buildcompiler/protocols/methods/transformation.py +++ b/src/buildcompiler/protocols/methods/transformation.py @@ -1,11 +1,26 @@ -"""Transformation method: configuration, planning and well allocation.""" +"""Plan transformation batches with explicit source aliquots and tube usage. + +The planner first expands samples and their lineage, then emits cell additions, +DNA additions and incubation/recovery operations. The allocator binds those +samples to wells, preserving upstream DNA locations when a manifest is supplied. +""" from collections import Counter +from collections.abc import Mapping, Sequence from dataclasses import dataclass from itertools import groupby from math import ceil -from buildcompiler.domain.protocol_requests import MaterialRef, TransformationRequest +from buildcompiler.domain.protocol_requests import ( + MaterialRef, + TransformationReaction, + TransformationRequest, +) +from buildcompiler.protocols.methods import ( + bind_source_wells, + reaction_entries, + sequential_wells, +) from buildcompiler.protocols.models import ( BLOCK_24, PLATE_96, @@ -31,7 +46,13 @@ @dataclass(frozen=True, slots=True, kw_only=True) class TransformationConfig(ConfigOverrides): - """Transfer volumes, source tube volumes, replicates and incubations.""" + """Source loading, transfer volumes in microliters, and incubation settings. + + ``volume_dna`` is the declared loading of each DNA source, whereas + ``transfer_volume_dna`` is drawn for each output. ``replicates`` applies + separately to every set of upstream source locations. Rates are pipette + speed multipliers; temperature-step durations are in minutes. + """ volume_dna: float = 20 transfer_volume_dna: float = 2 @@ -79,7 +100,12 @@ def __post_init__(self) -> None: @dataclass(frozen=True, slots=True, kw_only=True) class OpentronsTransformationProfile: - """Transformation deck slots and source, reaction and tip offsets.""" + """Transformation deck slots and source, reaction and tip offsets. + + DNA and reaction offsets are zero-based, column-major indices; tips use well + names. ``initial_dna_well`` applies only to fresh sources in the source block. + With an input manifest, DNA keeps its existing wells on ``dna_plate_position``. + """ api_level: str = "2.21" temperature_module_position: str = "1" @@ -111,14 +137,69 @@ def __post_init__(self) -> None: ) +def transformation_request_from_json( + payload: Sequence[Mapping[str, object]], + *, + request_id: str, + source_stage_id: str | None = None, +) -> TransformationRequest: + """Decode strains, chassis and ordered plasmids into a transformation batch.""" + + reactions = [] + for index, entry in reaction_entries( + payload, method="transformation", required={"Strain", "Chassis", "Plasmids"} + ): + if not isinstance(entry["Plasmids"], (list, tuple)): + raise TypeError("Plasmids must be an ordered list.") + reactions.append( + TransformationReaction( + id=f"{request_id}/reaction/{index}", + strain=MaterialRef.from_identity(entry["Strain"]), + chassis=MaterialRef.from_identity(entry["Chassis"]), + plasmids=tuple(MaterialRef.from_identity(p) for p in entry["Plasmids"]), + ) + ) + return TransformationRequest( + id=request_id, reactions=tuple(reactions), source_stage_id=source_stage_id + ) + + +def plasmid_manifest_from_json( + payload: Mapping[str, list[str]], *, protocol_id: str = "imported-assembly" +) -> OutputManifest: + """Import identity-to-well mappings as distinct physical source replicates.""" + + samples = [] + for index, (identity, wells) in enumerate(payload.items()): + if not isinstance(wells, (list, tuple)) or not wells: + raise ValueError("Each plasmid requires a nonempty list of source wells.") + for replicate, well in enumerate(wells, 1): + PLATE_96.index(well) + sample = Sample( + id=f"{protocol_id}/{index}/{replicate}", + material=MaterialRef.from_identity(identity), + replicate=replicate, + location=WellRef(container_id="source_plate", well_name=well), + ) + samples.append(sample) + return OutputManifest(protocol_id=protocol_id, samples=tuple(samples)) + + def plan_transformation( request: TransformationRequest, *, config: TransformationConfig, inputs: tuple[Sample, ...] | None = None, ) -> ProtocolPlan: - """Expand source and method replicates, grouping transfers by source tube.""" + """Expand reactions into outputs and record every contributing source. + + For each reaction, source sets follow manifest order and each set produces + ``config.replicates`` outputs. Multiple plasmids are paired by source index, + not combined as a Cartesian product. Without upstream samples, each plasmid + gets one fresh source. Allocation later supplies all physical locations. + """ + # Stock sorting makes source IDs stable without reordering requested outputs. plasmids = {p.identity: p for r in request.reactions for p in r.plasmids} chassis = {r.chassis.identity: r.chassis for r in request.reactions} ordered_plasmids = sorted(plasmids.values(), key=lambda p: (p.label, p.identity)) @@ -142,6 +223,8 @@ def plan_transformation( location_replicates = 1 samples = [] dna = {} + # Give each aliquot a local ID while retaining the upstream ID for handoffs. + # A material identity alone cannot distinguish its physical source replicates. for index, plasmid in enumerate(ordered_plasmids): dna[plasmid.identity] = [] for location_index in range(location_replicates): @@ -160,6 +243,8 @@ def plan_transformation( ) samples.append(sample) dna[plasmid.identity].append(sample) + # Count whole transfers per tube. Any remainder stays in that tube; ceil + # below allocates another tube when the last group needs fewer transfers. per_cell_tube = int( config.tube_volume_competent_cell // config.transfer_volume_competent_cell ) @@ -198,10 +283,14 @@ def plan_transformation( samples.append(sample) media.append(sample) input_ids = tuple(s.id for s in samples) + # Record source/output pairs before emitting operations so all cell additions + # precede DNA additions while both phases retain the same output order. outputs = [] cell_transfers = [] dna_transfers = [] cell_counts = Counter() + # Cell consumption is tracked per chassis across interleaved reactions. + # Media consumption instead follows the global output index. for reaction in request.reactions: for location_index in range(location_replicates): for replicate in range(config.replicates): @@ -230,6 +319,8 @@ def plan_transformation( cell_transfers.append((cell, output)) dna_transfers.extend((parent, output) for parent in parents) steps = [LidAction(id="open-lid", action="open")] + # Water testing removes temperature commands from the plan. Ordinary plans + # retain them with a simulation guard, so review documents still show them. if not config.water_testing: steps.extend( ( @@ -247,8 +338,8 @@ def plan_transformation( ), ) ) - # Combine consecutive transfers from the same tube without reordering - # reactions. A chassis can recur later after another chassis was handled. + # groupby combines consecutive uses of one physical tube, not all uses of + # a chassis. Sorting first would change reaction order and tip boundaries. for index, (source_id, group) in enumerate( groupby(cell_transfers, key=lambda pair: pair[0].id) ): @@ -279,7 +370,8 @@ def plan_transformation( drop_tip=False, ) ) - # Reuse the DNA tip for the finishing movements, then discard it. + # The DNA transfer owns the tip through both finishing movements. Only + # the second movement performs touch-tip and cleanup before the next DNA. for movement in range(2): steps.append( Transfer( @@ -357,7 +449,27 @@ def allocate_transformation( profile: OpentronsTransformationProfile, inputs: OutputManifest | None = None, ) -> ProtocolPlan: - """Bind mapped DNA wells or allocate fresh sources, tubes and reactions.""" + """Return a located plan using fresh DNA stocks or one upstream DNA plate. + + Manifest wells are rebound to this protocol's DNA plate without moving the + aliquots within it. Cell and media tubes share a separate rack; reactions + occupy consecutive wells beginning at the configured reaction offset. + """ + + # Preserve plan order within each physical resource. In particular, cell and + # media tubes share a rack and must be allocated together. + dna, tubes, reactions = [], [], [] + for sample in plan.samples: + if sample.role == "dna": + dna.append(sample) + elif sample.role in ("cells", "media"): + if sample.initial_volume_ul > 1500: + raise ValueError("Reagent loading exceeds tube capacity.") + tubes.append(sample) + elif sample.role == "reaction": + reactions.append(sample) + else: + raise ValueError(f"Unknown transformation sample role {sample.role!r}.") containers = [ ContainerSpec( @@ -371,8 +483,9 @@ def allocate_transformation( parent="reaction_module", ), ] - upstream = {} if inputs is not None: + # This target reserves one plate for the complete DNA handoff, including + # any manifest samples not selected by the current request. if len({p.location.container_id for p in inputs.samples}) != 1: raise ValueError("The transformation target accepts one source DNA plate.") containers.append( @@ -382,7 +495,16 @@ def allocate_transformation( parent=profile.dna_plate_position, ) ) - upstream = {p.id: p.location for p in inputs.samples} + locations = bind_source_wells( + dna, inputs, container_id="dna_plate", grid=PLATE_96 + ) + else: + locations = sequential_wells( + (s.id for s in dna), + container_id="sources", + grid=BLOCK_24, + start=profile.initial_dna_well, + ) containers.append( ContainerSpec( id="tube_rack", @@ -390,38 +512,16 @@ def allocate_transformation( parent=profile.tube_rack_position, ) ) - locations = {} - dna_index, tube_index, reaction_index = ( - profile.initial_dna_well, - 0, - profile.thermocycler_starting_well, + # Tube placement always begins at rack A1; only reactions have an offset here. + locations.update( + sequential_wells((s.id for s in tubes), container_id="tube_rack", grid=BLOCK_24) + ) + locations.update( + sequential_wells( + (s.id for s in reactions), + container_id="reactions", + grid=PLATE_96, + start=profile.thermocycler_starting_well, + ) ) - for sample in plan.samples: - if sample.role == "dna": - if inputs is None: - location = WellRef( - container_id="sources", well_name=BLOCK_24.name(dna_index) - ) - dna_index += 1 - else: - # The plate changes its deck role, but the source well and - # upstream sample identity remain the same. - well = upstream[sample.source_sample_id].well_name - PLATE_96.index(well) - location = WellRef(container_id="dna_plate", well_name=well) - elif sample.role in ("cells", "media"): - location = WellRef( - container_id="tube_rack", well_name=BLOCK_24.name(tube_index) - ) - tube_index += 1 - if sample.initial_volume_ul > 1500: - raise ValueError("Reagent loading exceeds tube capacity.") - elif sample.role == "reaction": - location = WellRef( - container_id="reactions", well_name=PLATE_96.name(reaction_index) - ) - reaction_index += 1 - else: - raise ValueError(f"Unknown transformation sample role {sample.role!r}.") - locations[sample.id] = location return plan.with_locations(tuple(containers), locations) diff --git a/src/buildcompiler/protocols/models.py b/src/buildcompiler/protocols/models.py index cc06d60..8490cf2 100644 --- a/src/buildcompiler/protocols/models.py +++ b/src/buildcompiler/protocols/models.py @@ -234,7 +234,7 @@ class TemperatureProgram: @dataclass(frozen=True, slots=True, kw_only=True) class RunTemperatureProgram: - """Run an incubation program, optionally skipping it during simulation.""" + """Run a program, optionally skipping it during Python protocol simulation.""" id: str program: TemperatureProgram @@ -243,7 +243,7 @@ class RunTemperatureProgram: @dataclass(frozen=True, slots=True, kw_only=True) class SetTemperature: - """Set a module or lid temperature without a timed hold.""" + """Set a temperature; the simulation guard applies to Python protocols only.""" id: str module: Literal["source", "reaction", "lid"] diff --git a/src/buildcompiler/stages/assembly_lvl1.py b/src/buildcompiler/stages/assembly_lvl1.py index 414bbe7..dce2fce 100644 --- a/src/buildcompiler/stages/assembly_lvl1.py +++ b/src/buildcompiler/stages/assembly_lvl1.py @@ -8,7 +8,6 @@ import sbol2 from buildcompiler.adapters.pudu import assembly_route_to_pudu_json -from buildcompiler.protocols.inputs import assembly_request_from_route from buildcompiler.api.options import BuildOptions from buildcompiler.domain import ( BuildRequest, @@ -19,6 +18,7 @@ StageStatus, ) from buildcompiler.inventory import CompatibilitySelector, Inventory +from buildcompiler.protocols.methods.assembly import assembly_request_from_route from buildcompiler.sbol import AssemblyJob, AssemblyService diff --git a/src/buildcompiler/stages/assembly_lvl2.py b/src/buildcompiler/stages/assembly_lvl2.py index 0d2f28f..ed4549c 100644 --- a/src/buildcompiler/stages/assembly_lvl2.py +++ b/src/buildcompiler/stages/assembly_lvl2.py @@ -8,7 +8,6 @@ import sbol2 from buildcompiler.adapters.pudu import assembly_route_to_pudu_json -from buildcompiler.protocols.inputs import assembly_request_from_route from buildcompiler.api.options import BuildOptions from buildcompiler.domain import ( BuildRequest, @@ -19,6 +18,7 @@ ) from buildcompiler.inventory import CompatibilitySelector, Inventory from buildcompiler.inventory.compatibility import Lvl2Route +from buildcompiler.protocols.methods.assembly import assembly_request_from_route from buildcompiler.sbol import AssemblyJob, AssemblyService diff --git a/src/buildcompiler/stages/transformation.py b/src/buildcompiler/stages/transformation.py index 2692eec..e5206d9 100644 --- a/src/buildcompiler/stages/transformation.py +++ b/src/buildcompiler/stages/transformation.py @@ -14,8 +14,10 @@ StageResult, StageStatus, ) +from buildcompiler.protocols.methods.transformation import ( + transformation_request_from_json, +) from buildcompiler.sbol.transformation import TransformationJob, TransformationService -from buildcompiler.protocols.inputs import transformation_request_from_json class TransformationStage: diff --git a/tests/automation/README.md b/tests/automation/README.md index 68cab2d..8f74367 100644 --- a/tests/automation/README.md +++ b/tests/automation/README.md @@ -1,19 +1,23 @@ # Protocol equivalence acceptance -These tests run the independent PUDU reference and BuildCompiler's generated standalone Python through `opentrons.simulate.simulate`. They compare ordered structured robot commands, resource and liquid definitions, temperature-program arguments, touch-tip parameters, and JSON handoffs. Only informational `comment` commands are excluded. Unknown trace values and simulator failures fail the run. +These tests compare the independent PUDU reference with both BuildCompiler OT-2 backends. All three run through the real Protocol Engine simulator using `opentrons analyze --check`. The original Python SDK comparisons through `opentrons.simulate.simulate` also remain in place. Unknown trace values and simulator failures fail the run. + +Engine comparisons preserve command order, all execution parameters, resource and liquid definitions, loaded labware geometry, resolved positions, temperature programs and tip handling. Generated resource IDs are replaced by stable identifiers, equivalent top/bottom well offsets are normalized, and floating-point coordinates are rounded to nine decimal places. Informational comments and result bookkeeping such as timestamps, task IDs and module serial numbers are excluded. Physical results such as tip geometry, movement positions and liquid volumes are retained. Traces are written to each test's temporary directory. Use Python 3.10 and the reference revision recorded in [pudu_reference.json](pudu_reference.json): ```bash -python -m pip install -e '.[test,simulation]' +python -m pip install -e '.[test,automation]' PUDU_REPOSITORY=../PUDU BUILDCOMPILER_REQUIRE_EQUIVALENCE=1 \ python -m pytest tests/automation -q --basetemp=/tmp/buildcompiler-equivalence ``` The fixture verifies the source hashes. `BUILDCOMPILER_REQUIRE_EQUIVALENCE=1` makes missing dependencies or a missing reference checkout fail instead of skip. CI checks out the pinned reference, runs this command, and uploads the traces. Normal core tests require neither PUDU nor Opentrons. -Coverage includes assembly source ordering, replicates, offsets, rates, water testing and rack replacement; transformation source-location replicates, multiple plasmids, interleaved chassis, independent cell/media tube boundaries, both pipettes, offsets and thermal behavior; plating dilution counts, replicates, two agar plates, offsets and changing conical-source heights. A connected workflow checks assembly → transformation → plating using each implementation's own handoffs and verifies native sample lineage. A changed-flow negative control establishes that the comparator detects a behavioral difference. +Coverage includes assembly source ordering, replicates, offsets, rates, water testing and rack replacement; transformation source-location replicates, multiple plasmids, interleaved chassis, independent cell/media tube boundaries, both pipettes, offsets and thermal behavior; plating dilution counts, replicates, two agar plates, offsets and changing conical-source heights. A connected workflow checks assembly → transformation → plating using each implementation's own handoffs and verifies native sample lineage. Negative controls change flow rates, positions, tip wells, temperatures and module generations to establish that the engine comparator detects behavioral differences. An invalid-volume control must fail analysis. + +PUDU forces transformation into water-testing mode during ordinary Python simulation. JSON protocols retain the complete execution program. For every engine comparison, the Python entrypoints override only `ProtocolContext.is_simulating` to exercise that same temperature path. Explicit water testing still removes temperatures from all three implementations. The original SDK comparisons separately cover ordinary simulation and the forced thermal path. -PUDU forces transformation into water-testing mode during simulation. Two additional transformation cases override only `ProtocolContext.is_simulating` in both entrypoints to exercise the ordinary temperature-program path and explicit water-testing override through the real simulator. These are separate from normal simulation parity. +A separate native Python/JSON comparison checks fractional refill boundaries. PUDU's reagent-tube counter rejects that fractional input, so this extra case establishes equivalence to the Python SDK rather than claiming three-way reference coverage. -The generated scripts do not write files. Their expected handoffs are compiler artifacts compared against PUDU's simulation-generated JSON. This verifies the covered simulator behavior, not physical hardware or experimental outcomes. +Generated protocols do not write files. Their expected handoffs are compiler artifacts compared against PUDU's simulation-generated JSON, and the two native bundles must have identical plans, manifests and handoffs. This verifies the covered simulator behavior, not physical hardware or experimental outcomes. diff --git a/tests/automation/conftest.py b/tests/automation/conftest.py index ab2cbec..01fb037 100644 --- a/tests/automation/conftest.py +++ b/tests/automation/conftest.py @@ -8,6 +8,141 @@ import pytest +from buildcompiler.protocols.backends.simulation import analyze_source + + +def engine_actions(analysis): + """Compare physical semantics while removing generated IDs and float noise. + + Keep command order, every execution parameter, loaded geometry and resolved + positions. Only informational comments, IDs and nonphysical result bookkeeping + are omitted. A bottom-relative and top-relative offset may denote one point. + """ + data = analysis.data + identifiers = {} + definitions = {} + liquids = {liquid["id"]: liquid for liquid in data["liquids"]} + counts = {kind: 0 for kind in ("moduleId", "labwareId", "pipetteId")} + for command in data["commands"]: + for kind, load in ( + ("moduleId", "loadModule"), + ("labwareId", "loadLabware"), + ("pipetteId", "loadPipette"), + ): + if command["commandType"] == load: + result = command["result"] + identifiers[result[kind]] = f"{kind}/{counts[kind]}" + counts[kind] += 1 + if kind == "labwareId": + definitions[result[kind]] = result["definition"] + + def normalize(value): + """Replace resource IDs recursively and retain sub-nanometre precision.""" + if isinstance(value, str): + return identifiers.get(value, value) + if isinstance(value, float): + return round(value, 9) + if isinstance(value, list): + return [normalize(item) for item in value] + if isinstance(value, dict): + return {key: normalize(item) for key, item in value.items()} + return value + + actions = [] + for command in data["commands"]: + assert command["status"] == "succeeded", command + kind = command["commandType"] + if kind == "comment": + continue + params = dict(command["params"]) + if kind in ("loadModule", "loadLabware", "loadPipette"): + params.pop( + { + "loadModule": "moduleId", + "loadLabware": "labwareId", + "loadPipette": "pipetteId", + }[kind], + None, + ) + if "liquidId" in params: + params["liquidId"] = { + key: value + for key, value in liquids[params["liquidId"]].items() + if key != "id" + } + if "wellLocation" in params: + location = dict(params["wellLocation"]) + offset = dict(location["offset"]) + if location["origin"] == "bottom": + offset["z"] -= definitions[params["labwareId"]]["wells"][ + params["wellName"] + ]["depth"] + location["origin"] = "top" + params["wellLocation"] = {**location, "offset": offset} + # Physical results catch differences in geometry and trash positioning + # that equal-looking command parameters alone would miss. + result = { + key: value + for key, value in command["result"].items() + if key + in ( + "position", + "volume", + "tipVolume", + "tipLength", + "tipDiameter", + "definition", + "model", + ) + } + actions.append( + normalize({"commandType": kind, "params": params, "result": result}) + ) + assert actions + return actions + + +@pytest.fixture +def compare_json_backend(pudu_repository, tmp_path): + """Run PUDU, native Python and native JSON through the same real engine.""" + + def compare(reference, python_protocol, json_protocol): + """Require the same plan, handoffs and physical trace in all three runs.""" + assert python_protocol.plan == json_protocol.plan + assert python_protocol.manifest == json_protocol.manifest + for name in python_protocol.files.keys() - {"protocol.py", "compilation.json"}: + assert python_protocol.files[name] == json_protocol.files[name] + + def thermal_source(source): + """Exercise execution's thermal path even while the engine simulates.""" + return ( + source.replace("def run(", "def _original_run(", 1) + + "\ndef run(protocol):\n protocol.is_simulating = lambda: False\n _original_run(protocol)\n" + ) + + traces = {} + for label, source, format, paths in ( + ("pudu", thermal_source(reference), "python", (pudu_repository / "src",)), + ("python", thermal_source(python_protocol.source), "python", ()), + ("json", json_protocol.source, "json", ()), + ): + analysis = analyze_source(source, format=format, python_paths=paths) + assert analysis.simulator_version == "8.8.2" + traces[label] = engine_actions(analysis) + ( + tmp_path + / f"{python_protocol.plan.id.replace('/', '_')}-{label}-engine.json" + ).write_text(json.dumps(traces[label], indent=2, sort_keys=True)) + if label != "pudu": + assert not analysis.generated_json + assert traces["python"] == traces["pudu"] + assert traces["json"] == traces["pudu"] + print( + f"{len(traces['json'])} Protocol Engine commands equal across all three backends" + ) + + return compare + @pytest.fixture(scope="session") def pudu_repository(): @@ -21,7 +156,7 @@ def pudu_repository(): ): if required: pytest.fail("Acceptance requires Opentrons and a PUDU reference checkout.") - pytest.skip("Install the simulation extra and supply PUDU_REPOSITORY.") + pytest.skip("Install the automation extra and supply PUDU_REPOSITORY.") reference = json.loads(Path(__file__).with_name("pudu_reference.json").read_text()) for name, expected in reference["sha256"].items(): path = repository / name diff --git a/tests/automation/test_assembly_equivalence.py b/tests/automation/test_assembly_equivalence.py index 0ba222a..7a93d36 100644 --- a/tests/automation/test_assembly_equivalence.py +++ b/tests/automation/test_assembly_equivalence.py @@ -94,13 +94,21 @@ def reference_script(payload, config, profile): ], ) def test_assembly_simulator_equivalence( - pudu_repository, tmp_path, count, config, profile + pudu_repository, tmp_path, count, config, profile, compare_json_backend ): payload = assemblies(count) compiled = ProtocolCompiler(assembly=config).compile( assembly_request_from_json(payload, request_id="acceptance"), profile=profile, ) + json_protocol = ProtocolCompiler(assembly=config).compile( + assembly_request_from_json(payload, request_id="acceptance"), + profile=profile, + backend="opentrons_ot2_json", + ) + compare_json_backend( + reference_script(payload, config, profile), compiled, json_protocol + ) expected = simulate_source( reference_script(payload, config, profile), python_paths=(pudu_repository / "src",), diff --git a/tests/automation/test_json_backend.py b/tests/automation/test_json_backend.py new file mode 100644 index 0000000..b08c26d --- /dev/null +++ b/tests/automation/test_json_backend.py @@ -0,0 +1,111 @@ +"""Negative controls for Protocol Engine equivalence and failure reporting.""" + +import copy +import json + +import pytest + +from buildcompiler.adapters.opentrons import OpentronsSimulationAdapter +from buildcompiler.api import ProtocolOptions +from buildcompiler.protocols import ( + ProtocolCompiler, + TransformationConfig, + assembly_request_from_json, + transformation_request_from_json, +) +from buildcompiler.protocols.backends.simulation import SimulationError, analyze_source + +from conftest import engine_actions + +pytestmark = pytest.mark.automation + + +@pytest.fixture(scope="module") +def json_reference(pudu_repository): + """Analyze one valid document once for the mutation tests.""" + request = assembly_request_from_json( + [ + { + "Product": "product", + "Backbone": "backbone", + "PartsList": ["part"], + "Restriction Enzyme": "enzyme", + } + ], + request_id="negative-controls", + ) + compiled = ProtocolCompiler().compile(request, backend="opentrons_ot2_json") + return json.loads(compiled.source), engine_actions( + analyze_source(compiled.source, format="json") + ) + + +@pytest.mark.parametrize( + "mutation", ["rate", "position", "tip", "temperature", "hardware"] +) +def test_engine_comparator_detects_behavior_changes(json_reference, mutation): + document, original = json_reference + changed = copy.deepcopy(document) + kind = { + "rate": "aspirate", + "position": "aspirate", + "tip": "pickUpTip", + "temperature": "temperatureModule/setTargetTemperature", + "hardware": "loadModule", + }[mutation] + params = next(c["params"] for c in changed["commands"] if c["commandType"] == kind) + if mutation == "rate": + params["flowRate"] *= 0.5 + elif mutation == "position": + params["wellLocation"]["offset"]["z"] += 1 + elif mutation == "tip": + params["wellName"] = "H12" + elif mutation == "temperature": + params["celsius"] += 1 + else: + params["model"] = "temperatureModuleV2" + analysis = analyze_source(json.dumps(changed), format="json") + assert engine_actions(analysis) != original + + +def test_engine_rejects_invalid_json_execution(json_reference): + document, _ = json_reference + changed = copy.deepcopy(document) + next(c["params"] for c in changed["commands"] if c["commandType"] == "aspirate")[ + "volume" + ] = 1000 + with pytest.raises(SimulationError, match="analysis failed"): + analyze_source(json.dumps(changed), format="json") + + +def test_simulation_adapter_accepts_json_protocol_files(json_reference, tmp_path): + document, _ = json_reference + path = tmp_path / "protocol.json" + path.write_text(json.dumps(document)) + result = OpentronsSimulationAdapter().simulate( + path, options=ProtocolOptions(simulate=True) + ) + assert result.ran + assert result.metadata["simulator_version"] == "8.8.2" + assert result.metadata["command_count"] >= len(document["commands"]) + + +def test_fractional_refills_match_the_python_sdk(pudu_repository): + """Check the native backends where the reference's integer tube counter fails.""" + request = transformation_request_from_json( + [{"Strain": "strain", "Chassis": "cell", "Plasmids": ["product"]}], + request_id="fractional-refills", + ) + compiler = ProtocolCompiler( + transformation=TransformationConfig( + replicates=31, + tube_volume_competent_cell=500, + transfer_volume_competent_cell=20 / 3, + water_testing=True, + ) + ) + python_protocol = compiler.compile(request) + json_protocol = compiler.compile(request, backend="opentrons_ot2_json") + expected = analyze_source(python_protocol.source) + actual = analyze_source(json_protocol.source, format="json") + assert engine_actions(actual) == engine_actions(expected) diff --git a/tests/automation/test_plating_equivalence.py b/tests/automation/test_plating_equivalence.py index 519b9ee..1a8a1a3 100644 --- a/tests/automation/test_plating_equivalence.py +++ b/tests/automation/test_plating_equivalence.py @@ -64,7 +64,9 @@ def reference_script(payload, config, profile): "conical-height-threshold", ], ) -def test_plating_equivalence(pudu_repository, tmp_path, count, config, profile): +def test_plating_equivalence( + pudu_repository, tmp_path, count, config, profile, compare_json_backend +): payload = plating_data(count) inputs = bacterium_manifest_from_json(payload) compiled = ProtocolCompiler(plating=config).compile( @@ -72,6 +74,15 @@ def test_plating_equivalence(pudu_repository, tmp_path, count, config, profile): inputs=inputs, profile=profile, ) + json_protocol = ProtocolCompiler(plating=config).compile( + PlatingRequest(id="plating", sample_ids=tuple(s.id for s in inputs.samples)), + inputs=inputs, + profile=profile, + backend="opentrons_ot2_json", + ) + compare_json_backend( + reference_script(payload, config, profile), compiled, json_protocol + ) expected = simulate_source( reference_script(payload, config, profile), python_paths=(pudu_repository / "src",), diff --git a/tests/automation/test_transformation_equivalence.py b/tests/automation/test_transformation_equivalence.py index f9ff68e..c94adb0 100644 --- a/tests/automation/test_transformation_equivalence.py +++ b/tests/automation/test_transformation_equivalence.py @@ -128,13 +128,29 @@ def force_thermal_path(source): ], ) def test_transformation_equivalence( - pudu_repository, tmp_path, payload, config, profile, locations, thermal + pudu_repository, + tmp_path, + payload, + config, + profile, + locations, + thermal, + compare_json_backend, ): compiled = ProtocolCompiler(transformation=config).compile( transformation_request_from_json(payload, request_id="transformation"), profile=profile, inputs=plasmid_manifest_from_json(locations) if locations is not None else None, ) + json_protocol = ProtocolCompiler(transformation=config).compile( + transformation_request_from_json(payload, request_id="transformation"), + profile=profile, + backend="opentrons_ot2_json", + inputs=plasmid_manifest_from_json(locations) if locations is not None else None, + ) + compare_json_backend( + reference_script(payload, config, profile, locations), compiled, json_protocol + ) expected_source = reference_script(payload, config, profile, locations) actual_source = compiled.script if thermal: diff --git a/tests/automation/test_workflow_equivalence.py b/tests/automation/test_workflow_equivalence.py index a3912d5..604fb57 100644 --- a/tests/automation/test_workflow_equivalence.py +++ b/tests/automation/test_workflow_equivalence.py @@ -20,7 +20,9 @@ pytestmark = pytest.mark.automation -def test_connected_workflow_equivalence(pudu_repository, tmp_path): +def test_connected_workflow_equivalence( + pudu_repository, tmp_path, compare_json_backend +): assemblies = [ { "Product": f"https://example.org/plasmid_{i}/1", @@ -45,7 +47,8 @@ def test_connected_workflow_equivalence(pudu_repository, tmp_path): ) reference_preamble = "metadata={'apiLevel':'2.21'}\ndef run(protocol):\n" - def compare(name, reference_source, compiled): + def compare(name, reference_source, compiled, json_compiled): + compare_json_backend(reference_source, compiled, json_compiled) expected = simulate_source( reference_source, python_paths=(pudu_repository / "src",) ) @@ -62,12 +65,17 @@ def compare(name, reference_source, compiled): assembly = compiler.compile( assembly_request_from_json(assemblies, request_id="assembly") ) + json_assembly = compiler.compile( + assembly_request_from_json(assemblies, request_id="assembly"), + backend="opentrons_ot2_json", + ) expected_assembly = compare( "assembly", "from pudu.assembly import SBOLLoopAssembly\n" + reference_preamble + f" SBOLLoopAssembly(assemblies={assemblies!r}, replicates=2, output_xlsx=False).run(protocol)\n", assembly, + json_assembly, ) assert ( expected_assembly["transformation_input.json"] @@ -77,12 +85,18 @@ def compare(name, reference_source, compiled): transformation_request_from_json(transformations, request_id="transformation"), inputs=assembly.manifest, ) + json_transformation = compiler.compile( + transformation_request_from_json(transformations, request_id="transformation"), + inputs=json_assembly.manifest, + backend="opentrons_ot2_json", + ) expected_transformation = compare( "transformation", "from pudu.transformation import HeatShockTransformation\n" + reference_preamble + f" HeatShockTransformation(transformation_data={transformations!r}, plasmid_locations={expected_assembly['transformation_input.json']!r}, replicates=2).run(protocol)\n", transformation, + json_transformation, ) assert expected_transformation["plating_input.json"] == { "bacterium_locations": transformation.manifest.bacterium_locations() @@ -99,12 +113,22 @@ def compare(name, reference_source, compiled): inputs=transformation.manifest, profile=plating_profile, ) + json_plating = compiler.compile( + PlatingRequest( + id="plating", + sample_ids=tuple(s.id for s in json_transformation.manifest.samples), + ), + inputs=json_transformation.manifest, + profile=plating_profile, + backend="opentrons_ot2_json", + ) expected_plating = compare( "plating", "from pudu.plating import Plating\n" + reference_preamble + f" Plating(plating_data={expected_transformation['plating_input.json']!r}, replicates=2, thermocycler_labware={plating_profile.thermocycler_labware!r}).run(protocol)\n", plating, + json_plating, ) assert expected_plating["plating_layout.json"] == json.loads( plating.files["plating_layout.json"] diff --git a/tests/unit/adapters/opentrons/test_simulation_boundary.py b/tests/unit/adapters/opentrons/test_simulation_boundary.py index ed5efcc..5dbbb01 100644 --- a/tests/unit/adapters/opentrons/test_simulation_boundary.py +++ b/tests/unit/adapters/opentrons/test_simulation_boundary.py @@ -46,9 +46,9 @@ def test_simulate_captures_evidence(monkeypatch, tmp_path): monkeypatch.setattr(importlib.util, "find_spec", lambda _: object()) monkeypatch.setattr( simulation, - "simulate_source", - lambda source: SimpleNamespace( - actions=[{"command": "test"}], + "analyze_source", + lambda source, **kwargs: SimpleNamespace( + data={"commands": [{"command": "test"}]}, simulator_version="8.8.2", stdout="ok\n", stderr="", @@ -71,10 +71,10 @@ def test_simulation_failure_is_not_reported_as_success(monkeypatch, tmp_path): protocol.write_text("broken", encoding="utf-8") monkeypatch.setattr(importlib.util, "find_spec", lambda _: object()) - def fail(source): + def fail(source, **kwargs): raise RuntimeError("Opentrons simulation failed with exit code 2: invalid") - monkeypatch.setattr(simulation, "simulate_source", fail) + monkeypatch.setattr(simulation, "analyze_source", fail) with pytest.raises(ProtocolSimulationError, match="exit code 2"): OpentronsSimulationAdapter().simulate( diff --git a/tests/unit/adapters/test_protocol_boundaries.py b/tests/unit/adapters/test_protocol_boundaries.py index 67b88e0..be4a638 100644 --- a/tests/unit/adapters/test_protocol_boundaries.py +++ b/tests/unit/adapters/test_protocol_boundaries.py @@ -1,5 +1,6 @@ import sys -import types +import hashlib +import json import pytest @@ -72,51 +73,105 @@ def test_protocol_bundle_writes_json_manual_and_deterministic_manifest(tmp_path) assert all("sha256" in item for item in bundle.manifest["artifacts"]) -def test_automated_bundle_generates_pudu_script_lazily(monkeypatch, tmp_path): - calls = [] - - def generate_protocol(**kwargs): - calls.append(kwargs) - return "from opentrons import protocol_api\n" - - pudu_module = types.ModuleType("pudu") - generate_module = types.ModuleType("pudu.generate_protocol") - generate_module.generate_protocol = generate_protocol - monkeypatch.setitem(sys.modules, "pudu", pudu_module) - monkeypatch.setitem(sys.modules, "pudu.generate_protocol", generate_module) - stage_result = StageResult( - id="assembly-result", - stage=BuildStage.ASSEMBLY_LVL1, +@pytest.mark.parametrize( + ("stage", "payload", "handoff"), + [ + ( + BuildStage.ASSEMBLY_LVL1, + { + "Product": "product", + "Backbone": "backbone", + "PartsList": ["part"], + "Restriction Enzyme": "BsaI", + }, + "transformation_input.json", + ), + ( + BuildStage.ASSEMBLY_LVL2, + [ + { + "Product": "product", + "Backbone": "backbone", + "PartsList": ["part"], + "Restriction Enzyme": "BsaI", + } + ], + "transformation_input.json", + ), + ( + BuildStage.DOMESTICATION, + { + "Product": "product", + "Backbone": "backbone", + "PartsList": ["insert"], + "Restriction Enzyme": "BsaI", + "Generated Insert Sequence": "ACGT", + }, + "transformation_input.json", + ), + ( + BuildStage.TRANSFORMATION, + {"Strain": "strain", "Chassis": "chassis", "Plasmids": ["plasmid"]}, + "plating_input.json", + ), + ( + BuildStage.PLATING, + {"bacterium_locations": {"A1": ["strain"]}}, + "plating_layout.json", + ), + ], +) +def test_automated_bundle_uses_native_compiler_without_pudu_or_sdk( + monkeypatch, tmp_path, stage, payload, handoff +): + monkeypatch.setitem(sys.modules, "pudu", None) + monkeypatch.setitem(sys.modules, "pudu.generate_protocol", None) + monkeypatch.setitem(sys.modules, "opentrons", None) + result = StageResult( + id="stage-result", + stage=stage, status=StageStatus.SUCCESS, - json_intermediate={"Product": "product", "PartsList": ["part"]}, - ) - - bundle = build_protocol_bundle( - stage_results=[stage_result], - options=ProtocolOptions(mode=ProtocolMode.AUTOMATED, results_dir=tmp_path), + json_intermediate=payload, ) + options = ProtocolOptions(mode=ProtocolMode.AUTOMATED, results_dir=tmp_path) + + bundle = build_protocol_bundle(stage_results=[result], options=options) + + basename = f"{stage.value}_001" + script = (tmp_path / f"{basename}.py").read_text() + compile(script, f"{basename}.py", "exec") + assert "def run(" in script + assert "pudu" not in script + assert bundle.artifacts[f"{basename}_automated"].metadata["written"] is True + assert json.loads((tmp_path / f"{basename}_{handoff}").read_text()) + compilation = json.loads((tmp_path / f"{basename}_compilation.json").read_text()) + assert compilation["backend"] == "opentrons_ot2_python" + for artifact in bundle.manifest["artifacts"]: + assert ( + artifact["sha256"] + == hashlib.sha256((tmp_path / artifact["path"]).read_bytes()).hexdigest() + ) - assert (tmp_path / "assembly_lvl1_001.py").exists() - assert bundle.artifacts["assembly_lvl1_001_automated"].metadata["written"] is True - assert calls[0]["protocol_type"] == "assembly" - assert calls[0]["assembly_subtype"] == "SBOL" + before = {path.name: path.read_bytes() for path in tmp_path.iterdir()} + with pytest.raises(FileExistsError): + build_protocol_bundle(stage_results=[result], options=options) + options.overwrite = True + repeated = build_protocol_bundle(stage_results=[result], options=options) + assert repeated.manifest == bundle.manifest + assert before == {path.name: path.read_bytes() for path in tmp_path.iterdir()} -def test_automated_bundle_missing_pudu_fails_explicitly(monkeypatch, tmp_path): - monkeypatch.setitem(sys.modules, "pudu", None) - monkeypatch.setitem(sys.modules, "pudu.generate_protocol", None) - stage_result = StageResult( +def test_automated_bundle_rejects_incomplete_input_before_writing(tmp_path): + result = StageResult( id="transformation-result", stage=BuildStage.TRANSFORMATION, status=StageStatus.SUCCESS, json_intermediate={"Strain": "strain", "Plasmids": ["plasmid"]}, ) - with pytest.raises(ImportError, match="automation"): + with pytest.raises(ValueError, match="Chassis"): build_protocol_bundle( - stage_results=[stage_result], - options=ProtocolOptions( - mode=ProtocolMode.AUTOMATED, - results_dir=tmp_path, - ), + stage_results=[result], + options=ProtocolOptions(mode=ProtocolMode.AUTOMATED, results_dir=tmp_path), ) + assert not any(tmp_path.iterdir()) diff --git a/tests/unit/execution/test_executor.py b/tests/unit/execution/test_executor.py index 75b86b8..2d3a4a9 100644 --- a/tests/unit/execution/test_executor.py +++ b/tests/unit/execution/test_executor.py @@ -1,3 +1,7 @@ +import json + +import pytest + from buildcompiler.api import BuildOptions, ProtocolMode from buildcompiler.domain import ( BuildRequest, @@ -13,6 +17,7 @@ from buildcompiler.execution import BuildContext, FullBuildExecutor from buildcompiler.inventory import Inventory from buildcompiler.planning import BuildPlan +from buildcompiler.protocols.methods.assembly import assembly_request_from_json from buildcompiler.sbol import SbolResolver @@ -493,10 +498,22 @@ def run(self, product, *, source_document, target_document): assert result.stage_results[-1].status == StageStatus.FAILED -def test_protocol_mode_writes_bundle_and_exposes_target_and_all_products(tmp_path): +@pytest.mark.parametrize("mode", [ProtocolMode.MANUAL, ProtocolMode.AUTOMATED]) +def test_protocol_mode_writes_bundle_and_exposes_target_and_all_products( + tmp_path, mode +): options = BuildOptions() - options.protocol.mode = ProtocolMode.MANUAL + options.protocol.mode = mode options.protocol.results_dir = tmp_path + payload = { + "Product": "design", + "Backbone": "backbone", + "PartsList": ["part"], + "Restriction Enzyme": "BsaI", + } + protocol_request = assembly_request_from_json( + [{**payload, "Product": "https://x/plasmid/protocol"}], request_id="assembly" + ) assembly = FakeStage( lambda request: StageResult( id="assembly", @@ -504,7 +521,8 @@ def test_protocol_mode_writes_bundle_and_exposes_target_and_all_products(tmp_pat status=StageStatus.SUCCESS, request_ids=[request.id], products=[plasmid("https://x/plasmid/protocol")], - json_intermediate={"Product": "product", "PartsList": []}, + json_intermediate=payload, + protocol_requests=(protocol_request,), ) ) blocked = FakeStage( @@ -546,3 +564,10 @@ def test_protocol_mode_writes_bundle_and_exposes_target_and_all_products(tmp_pat assert result.artifact_bundle.status == "success" assert result.target_products == result.all_products assert (tmp_path / "protocol_manifest.json").exists() + if mode == ProtocolMode.AUTOMATED: + assert (tmp_path / "assembly_lvl1_001.py").exists() + handoff = json.loads( + (tmp_path / "assembly_lvl1_001_transformation_input.json").read_text() + ) + assert "https://x/plasmid/protocol" in handoff + assert "design" not in handoff diff --git a/tests/unit/protocols/test_compilation.py b/tests/unit/protocols/test_compilation.py index 430662d..a472871 100644 --- a/tests/unit/protocols/test_compilation.py +++ b/tests/unit/protocols/test_compilation.py @@ -14,8 +14,10 @@ OpentronsAssemblyProfile, ProtocolCompiler, assembly_request_from_json, + transformation_request_from_json, ) from buildcompiler.protocols.backends.markdown import render_markdown +from buildcompiler.protocols.methods import sequential_wells from buildcompiler.protocols.models import BLOCK_24, PLATE_96, Transfer @@ -37,6 +39,39 @@ def request(): return assembly_request_from_json(payload(), request_id="example") +@pytest.mark.parametrize( + "decode", [assembly_request_from_json, transformation_request_from_json] +) +@pytest.mark.parametrize( + "data,error,message", + [ + ({}, TypeError, "sequence of reaction objects"), + ("[]", TypeError, "sequence of reaction objects"), + ([None], TypeError, "entry must be an object"), + ([{}], ValueError, "0 is missing"), + ], +) +def test_reaction_decoders_reject_invalid_envelopes(decode, data, error, message): + with pytest.raises(error, match=message): + decode(data, request_id="invalid") + + +@pytest.mark.parametrize("grid,last_well", [(PLATE_96, "H12"), (BLOCK_24, "D6")]) +def test_sequential_wells_respect_remaining_capacity(grid, last_well): + locations = sequential_wells( + ("last",), container_id="plate", grid=grid, start=grid.capacity - 1 + ) + assert locations["last"].well_name == last_well + assert locations["last"].container_id == "plate" + with pytest.raises(ValueError, match="outside this grid"): + sequential_wells( + ("last", "overflow"), + container_id="plate", + grid=grid, + start=grid.capacity - 1, + ) + + def test_compile_is_deterministic_and_never_writes(tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) compiler = ProtocolCompiler() diff --git a/tests/unit/protocols/test_opentrons_json.py b/tests/unit/protocols/test_opentrons_json.py new file mode 100644 index 0000000..47d0dc8 --- /dev/null +++ b/tests/unit/protocols/test_opentrons_json.py @@ -0,0 +1,198 @@ +"""Backend selection and offline JSON compilation without the robot SDK.""" + +import json +import subprocess +import sys +from dataclasses import replace + +import pytest + +from buildcompiler.api.protocols import ( + compile_assembly, + compile_plating, + compile_transformation, +) +from buildcompiler.domain import BuildStage, StageResult, StageStatus +from buildcompiler.protocols import ( + AssemblyConfig, + OpentronsAssemblyProfile, + ProtocolCompiler, + TransformationConfig, + assembly_request_from_json, + transformation_request_from_json, +) +from buildcompiler.protocols.backends.opentrons_ot2_json import ( + OpentronsProtocolBuilder, + render_json, +) +from buildcompiler.protocols.models import PickUpTip, Transfer + + +@pytest.fixture(autouse=True) +def shared_data(): + """Only the optional data package is needed; core imports remain independent.""" + pytest.importorskip("opentrons_shared_data") + + +def assembly_request(): + """A small batch for testing compiler contracts.""" + return assembly_request_from_json( + [ + { + "Product": "product", + "Backbone": "backbone", + "PartsList": ["part"], + "Restriction Enzyme": "enzyme", + } + ], + request_id="assembly", + ) + + +def transformation_request(): + """A transformation consuming the assembly's identified product.""" + return transformation_request_from_json( + [{"Strain": "strain", "Chassis": "cell", "Plasmids": ["product"]}], + request_id="transformation", + ) + + +def test_json_is_deterministic_offline_and_writes_the_correct_artifact( + tmp_path, monkeypatch +): + monkeypatch.chdir(tmp_path) + compiler = ProtocolCompiler() + first = compiler.compile(assembly_request(), backend="opentrons_ot2_json") + assert first == compiler.compile(assembly_request(), backend="opentrons_ot2_json") + assert not list(tmp_path.iterdir()) + assert "opentrons" not in sys.modules + document = json.loads(first.source) + assert document["robot"]["model"] == "OT-2 Standard" + assert document["schemaVersion"] == 8 + assert document["commandSchemaId"] == "opentronsCommandSchemaV10" + for command in document["commands"]: + if command["commandType"] == "loadLabware": + p = command["params"] + assert ( + f"{p['namespace']}/{p['loadName']}/{p['version']}" + in document["labwareDefinitions"] + ) + assert first.protocol_filename == "protocol.json" + paths = first.write(tmp_path) + assert (tmp_path / "protocol.json").read_text() == first.source + assert not (tmp_path / "protocol.py").exists() + assert first.protocol_filename in {path.name for path in paths} + with pytest.raises(FileExistsError): + first.write(tmp_path) + assert ( + json.loads(first.files["compilation.json"])["backend"] == "opentrons_ot2_json" + ) + + +@pytest.mark.parametrize("backend", ["opentrons_ot2_python", "opentrons_ot2_json"]) +def test_stage_helpers_accept_either_backend_through_a_complete_workflow(backend): + assembled = compile_assembly( + StageResult( + id="assembly", + stage=BuildStage.ASSEMBLY_LVL1, + status=StageStatus.SUCCESS, + protocol_requests=(assembly_request(),), + ), + backend=backend, + ) + transformed = compile_transformation( + StageResult( + id="transformation", + stage=BuildStage.TRANSFORMATION, + status=StageStatus.SUCCESS, + protocol_requests=(transformation_request(),), + ), + inputs=assembled.manifest, + backend=backend, + ) + plated = compile_plating(transformed.manifest, backend=backend) + for compiled in (assembled, transformed, plated): + assert compiled.backend == backend + assert compiled.source == compiled.files[compiled.protocol_filename] + assert compiled.manifest.samples + + +def test_unknown_backend_fails_explicitly(): + with pytest.raises(ValueError, match="Unsupported protocol backend"): + ProtocolCompiler().compile(assembly_request(), backend="unknown") + + +def test_json_retains_execution_temperatures_and_respects_water_testing(): + for water_testing in (False, True): + compiled = ProtocolCompiler( + transformation=TransformationConfig(water_testing=water_testing) + ).compile(transformation_request(), backend="opentrons_ot2_json") + programs = [ + c + for c in json.loads(compiled.source)["commands"] + if c["commandType"] == "thermocycler/runExtendedProfile" + ] + assert len(programs) == (0 if water_testing else 2) + + +def test_json_rejects_capacity_and_invalid_tip_ownership(): + compiler = ProtocolCompiler() + profile = OpentronsAssemblyProfile() + plan = compiler.compile(assembly_request()).plan + transfer = next(step for step in plan.steps if isinstance(step, Transfer)) + with pytest.raises(ValueError, match="attached tip"): + render_json( + replace(plan, steps=(replace(transfer, new_tip=False),)), profile=profile + ) + with pytest.raises(ValueError, match="already attached"): + render_json( + replace(plan, steps=(PickUpTip(id="first"), transfer)), profile=profile + ) + with pytest.raises(ValueError, match="pipette capacity"): + ProtocolCompiler(assembly=AssemblyConfig(volume_total_reaction=50)).compile( + assembly_request(), backend="opentrons_ot2_json" + ) + + +def test_authoring_boundary_rejects_unknown_optional_command_parameters(): + from jsonschema import ValidationError + + builder = OpentronsProtocolBuilder( + ProtocolCompiler().compile(assembly_request()).plan, OpentronsAssemblyProfile() + ) + with pytest.raises(ValidationError): + builder._emit( + "pickUpTip", + pipetteId="pipette", + labwareId="tips", + wellName="A1", + wellLocaton={}, + ) + + +def test_missing_json_dependency_does_not_break_python_compilation(): + code = """ +import importlib.abc +import sys +class NoSharedData(importlib.abc.MetaPathFinder): + def find_spec(self, fullname, path, target=None): + if fullname.split('.')[0] == 'opentrons_shared_data': + raise ImportError('intentionally unavailable') +sys.meta_path.insert(0, NoSharedData()) +from buildcompiler.protocols import ProtocolCompiler, assembly_request_from_json +request = assembly_request_from_json( + [{'Product': 'product', 'Backbone': 'backbone', 'PartsList': ['part'], 'Restriction Enzyme': 'enzyme'}], + request_id='boundary') +assert ProtocolCompiler().compile(request).protocol_filename == 'protocol.py' +try: + ProtocolCompiler().compile(request, backend='opentrons_ot2_json') +except ImportError as error: + assert 'automation' in str(error) +else: + raise AssertionError('JSON compilation silently ignored a missing dependency') +assert 'opentrons' not in sys.modules +""" + process = subprocess.run( + [sys.executable, "-c", code], capture_output=True, text=True + ) + assert process.returncode == 0, process.stderr diff --git a/tests/unit/protocols/test_transformation_plating.py b/tests/unit/protocols/test_transformation_plating.py index 83b4f3c..6a6683e 100644 --- a/tests/unit/protocols/test_transformation_plating.py +++ b/tests/unit/protocols/test_transformation_plating.py @@ -99,6 +99,64 @@ def test_duplicate_physical_sources_are_rejected(): plasmid_manifest_from_json({"first": ["A1"], "second": ["A1"]}) +def test_transformation_requires_one_plate_for_the_complete_handoff(): + upstream = plasmid_manifest_from_json( + { + "https://example.org/first/1": ["A1"], + "https://example.org/second/1": ["B1"], + "unused": ["C1"], + } + ) + unused = upstream.samples[-1] + upstream = dataclasses.replace( + upstream, + samples=( + *upstream.samples[:-1], + dataclasses.replace( + unused, + location=dataclasses.replace(unused.location, container_id="other"), + ), + ), + ) + with pytest.raises(ValueError, match="one source DNA plate"): + ProtocolCompiler().compile(request(), inputs=upstream) + + +def test_plating_binds_selected_aliquots_by_id_and_preserves_their_wells(): + upstream = sources() + # Keep two aliquots of the same material on one plate; the unselected + # material occupies another plate and must not affect the selected batch. + upstream = dataclasses.replace( + upstream, + samples=( + *upstream.samples[:2], + *( + dataclasses.replace( + sample, + location=dataclasses.replace(sample.location, container_id="other"), + ) + for sample in upstream.samples[2:] + ), + ), + ) + selected = tuple(s.id for s in reversed(upstream.samples[:2])) + compiled = ProtocolCompiler().compile( + PlatingRequest(id="selected", sample_ids=selected), inputs=upstream + ) + cultures = [s for s in compiled.plan.samples if s.role == "bacteria"] + assert tuple(s.source_sample_id for s in cultures) == selected + assert [s.location.well_name for s in cultures] == ["A4", "D2"] + assert {s.location.container_id for s in cultures} == {"reactions"} + with pytest.raises(ValueError, match="one source plate"): + ProtocolCompiler().compile( + PlatingRequest( + id="mixed-plates", + sample_ids=(upstream.samples[0].id, upstream.samples[-1].id), + ), + inputs=upstream, + ) + + def test_transformation_thermal_steps_are_reviewable_but_simulation_guarded(): compiler = ProtocolCompiler() compiled = compiler.compile(request()) diff --git a/tests/unit/stages/test_assembly_lvl1.py b/tests/unit/stages/test_assembly_lvl1.py index 364c29a..fc890f3 100644 --- a/tests/unit/stages/test_assembly_lvl1.py +++ b/tests/unit/stages/test_assembly_lvl1.py @@ -148,6 +148,10 @@ def test_assembly_lvl1_success_returns_products_json_and_indexes_generated(): part.identity for part in result.protocol_requests[0].reactions[0].parts ] == result.json_intermediate["PartsList"] assert result.sbol_document is not None + assert ( + result.protocol_artifacts["assembly_spec"] + == result.json_intermediate["Parameters"] + ) assert inv.find_lvl1_region_plasmids("https://example.org/designs/d1") assert result.logs assert result.warnings and isinstance(result.warnings[0], BuildWarning) diff --git a/tests/unit/stages/test_assembly_lvl2.py b/tests/unit/stages/test_assembly_lvl2.py index ab6a8c9..26c3aa4 100644 --- a/tests/unit/stages/test_assembly_lvl2.py +++ b/tests/unit/stages/test_assembly_lvl2.py @@ -130,6 +130,10 @@ def test_assembly_lvl2_success_routes_and_indexes_generated_product(): part.identity for part in result.protocol_requests[0].reactions[0].parts ] == result.json_intermediate["PartsList"] assert result.protocol_artifacts["selected_route"] is not None + assert ( + result.protocol_artifacts["assembly_spec"] + == result.json_intermediate["Parameters"] + ) assert inv.find_lvl1_region_plasmids(module.identity) diff --git a/uv.lock b/uv.lock index f7c7bfd..e9f8aa3 100644 --- a/uv.lock +++ b/uv.lock @@ -1669,9 +1669,9 @@ name = "opentrons-shared-data" version = "8.8.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jsonschema", marker = "python_full_version < '3.11'" }, - { name = "pydantic", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/b4/6d66d9b106a2c81f4c91e18e224bc55908df1f10711cb03e9934e65cde36/opentrons_shared_data-8.8.2.tar.gz", hash = "sha256:b07a3343d5986cb1d49bb0005642425067955b0b8282d208a5f42ab69f1bb9a4", size = 658731 } wheels = [ @@ -1837,10 +1837,10 @@ name = "pydantic" version = "2.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-types", marker = "python_full_version < '3.11'" }, - { name = "pydantic-core", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, - { name = "typing-inspection", marker = "python_full_version < '3.11'" }, + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750 } wheels = [ @@ -1852,7 +1852,7 @@ name = "pydantic-core" version = "2.46.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262 } wheels = [ @@ -2669,7 +2669,9 @@ dependencies = [ [package.optional-dependencies] automation = [ { name = "opentrons", marker = "python_full_version < '3.11'" }, + { name = "opentrons-shared-data" }, { name = "sbolinventory" }, + { name = "xlsxwriter" }, ] dev = [ { name = "build" }, @@ -2684,10 +2686,6 @@ notebook = [ { name = "nbclient" }, { name = "nbformat" }, ] -simulation = [ - { name = "opentrons", marker = "python_full_version < '3.11'" }, - { name = "xlsxwriter" }, -] test = [ { name = "pytest" }, { name = "pytest-cov" }, @@ -2707,7 +2705,7 @@ requires-dist = [ { name = "nbclient", marker = "extra == 'notebook'", specifier = ">=0.10,<1" }, { name = "nbformat", marker = "extra == 'notebook'", specifier = ">=5,<6" }, { name = "opentrons", marker = "python_full_version == '3.10.*' and extra == 'automation'", specifier = "==8.8.2" }, - { name = "opentrons", marker = "python_full_version == '3.10.*' and extra == 'simulation'", specifier = "==8.8.2" }, + { name = "opentrons-shared-data", marker = "extra == 'automation'", specifier = "==8.8.2" }, { name = "pydna" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7,<9" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=7,<9" }, @@ -2717,9 +2715,9 @@ requires-dist = [ { name = "sbol2" }, { name = "sbolinventory", marker = "python_full_version >= '3.10' and extra == 'automation'", git = "https://github.com/DRAGGON-Lab/SBOLInventory.git" }, { name = "twine", marker = "extra == 'dev'", specifier = ">=5.0" }, - { name = "xlsxwriter", marker = "extra == 'simulation'", specifier = ">=3.2.5" }, + { name = "xlsxwriter", marker = "extra == 'automation'", specifier = ">=3.2.5" }, ] -provides-extras = ["simulation", "test", "dev", "automation", "notebook"] +provides-extras = ["test", "dev", "automation", "notebook"] [package.metadata.requires-dev] dev = [{ name = "ruff", specifier = ">=0.14.0" }] @@ -2859,7 +2857,7 @@ name = "typing-inspection" version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928 } wheels = [ From 27f392a196c5f5efe9d6d9c74d18da351b6a25b0 Mon Sep 17 00:00:00 2001 From: Mike Arpaia Date: Thu, 24 Sep 2026 11:38:56 -0600 Subject: [PATCH 4/5] Update the API reference --- docs/API_Reference.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/API_Reference.rst b/docs/API_Reference.rst index 47d0f68..fd4f100 100644 --- a/docs/API_Reference.rst +++ b/docs/API_Reference.rst @@ -23,6 +23,14 @@ buildcompiler.api.options :undoc-members: :show-inheritance: +buildcompiler.api.protocols +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: buildcompiler.api.protocols + :members: + :undoc-members: + :show-inheritance: + buildcompiler.api.serialization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -262,6 +270,22 @@ buildcompiler.domain.plasmid :undoc-members: :show-inheritance: +buildcompiler.domain.protocol +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: buildcompiler.domain.protocol + :members: + :undoc-members: + :show-inheritance: + +buildcompiler.domain.protocol_requests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: buildcompiler.domain.protocol_requests + :members: + :undoc-members: + :show-inheritance: + buildcompiler.domain.reagent ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From d3e5d421fcb743e65bf7d6b52d4d8b2cd280552b Mon Sep 17 00:00:00 2001 From: Mike Arpaia Date: Thu, 24 Sep 2026 12:40:30 -0600 Subject: [PATCH 5/5] Fix tests --- .github/workflows/ci.yml | 4 +- docs/API_Reference.rst | 75 +++++++++++++++++++ docs/development.rst | 7 +- docs/protocols.rst | 2 +- pyproject.toml | 23 +++++- setup.py | 2 +- src/buildcompiler/adapters/pudu/__init__.py | 4 +- .../adapters/pudu/assembly_json.py | 2 +- .../adapters/pudu/transformation_json.py | 1 - src/buildcompiler/api/__init__.py | 26 +++---- src/buildcompiler/api/compiler.py | 12 ++- src/buildcompiler/execution/executor.py | 4 +- src/buildcompiler/inventory/__init__.py | 4 +- src/buildcompiler/inventory/indexing.py | 2 +- src/buildcompiler/inventory/inventory.py | 1 - src/buildcompiler/inventory/selector.py | 7 +- src/buildcompiler/planning/__init__.py | 4 +- src/buildcompiler/planning/combinatorial.py | 7 +- src/buildcompiler/planning/validation.py | 2 +- src/buildcompiler/protocols/__init__.py | 18 ++--- src/buildcompiler/sbol/__init__.py | 2 +- src/buildcompiler/sbol/assembly.py | 8 +- src/buildcompiler/sbol/domestication.py | 11 ++- src/buildcompiler/sbol/resolver.py | 3 +- src/buildcompiler/sbol/synbiohub.py | 6 +- src/buildcompiler/stages/assembly_lvl2.py | 6 +- tests/automation/test_json_backend.py | 3 +- tests/conftest.py | 2 +- .../integration/test_full_build_happy_path.py | 2 +- .../test_legacy_offline_fixtures.py | 1 - .../test_missing_lvl1_then_domestication.py | 2 +- .../test_offline_quickstart_notebook.py | 1 - .../opentrons/test_simulation_boundary.py | 2 +- .../unit/adapters/test_protocol_boundaries.py | 2 +- tests/unit/api/test_synbiohub_factory.py | 7 +- tests/unit/planning/test_combinatorial.py | 5 +- .../planning/test_domestication_planner.py | 8 +- tests/unit/planning/test_validation.py | 3 +- tests/unit/protocols/test_opentrons_json.py | 2 +- tests/unit/sbol/test_assembly_service.py | 2 +- tests/unit/sbol/test_resolver.py | 2 +- tests/unit/stages/test_assembly_lvl2.py | 2 +- tests/unit/stages/test_transformation.py | 4 +- tests/unit/test_core_imports.py | 14 ++-- tools/update_api_reference.py | 3 +- uv.lock | 47 ++++++------ 46 files changed, 233 insertions(+), 124 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e772401..9be0f79 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: core: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: python-version: ["3.10", "3.11"] steps: @@ -21,8 +22,7 @@ jobs: - name: Install package run: | python -m pip install -U pip - python -m pip install -e ".[test,automation]" - python -m pip install ruff + python -m pip install -e ".[dev,automation]" - name: Ruff check run: >- ruff check diff --git a/docs/API_Reference.rst b/docs/API_Reference.rst index fd4f100..bfb3084 100644 --- a/docs/API_Reference.rst +++ b/docs/API_Reference.rst @@ -39,6 +39,81 @@ buildcompiler.api.serialization :undoc-members: :show-inheritance: +Protocols +--------- + +buildcompiler.protocols.backends.markdown +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: buildcompiler.protocols.backends.markdown + :members: + :undoc-members: + :show-inheritance: + +buildcompiler.protocols.backends.opentrons_ot2_json +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: buildcompiler.protocols.backends.opentrons_ot2_json + :members: + :undoc-members: + :show-inheritance: + +buildcompiler.protocols.backends.opentrons_ot2_python +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: buildcompiler.protocols.backends.opentrons_ot2_python + :members: + :undoc-members: + :show-inheritance: + +buildcompiler.protocols.backends.simulation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: buildcompiler.protocols.backends.simulation + :members: + :undoc-members: + :show-inheritance: + +buildcompiler.protocols.compiler +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: buildcompiler.protocols.compiler + :members: + :undoc-members: + :show-inheritance: + +buildcompiler.protocols.methods.assembly +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: buildcompiler.protocols.methods.assembly + :members: + :undoc-members: + :show-inheritance: + +buildcompiler.protocols.methods.plating +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: buildcompiler.protocols.methods.plating + :members: + :undoc-members: + :show-inheritance: + +buildcompiler.protocols.methods.transformation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: buildcompiler.protocols.methods.transformation + :members: + :undoc-members: + :show-inheritance: + +buildcompiler.protocols.models +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: buildcompiler.protocols.models + :members: + :undoc-members: + :show-inheritance: + Stages ------ diff --git a/docs/development.rst b/docs/development.rst index 2c8f139..c2122db 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -8,7 +8,9 @@ Install the package and test dependencies: .. code-block:: bash - python -m pip install -e ".[test]" + python -m pip install -e ".[dev]" + +The ``dev`` extra pins the same Ruff version used by CI. Run the focused tests for the documented PUDU path: @@ -32,7 +34,8 @@ Build docs locally: .. code-block:: bash python -m pip install -r docs/requirements.txt - sphinx-build -b html docs docs/_build/html + python tools/update_api_reference.py + sphinx-build -b html -W --keep-going docs docs/_build/html Agent handoff ------------- diff --git a/docs/protocols.rst b/docs/protocols.rst index ee2959e..2d63e0c 100644 --- a/docs/protocols.rst +++ b/docs/protocols.rst @@ -55,7 +55,7 @@ Protocol Engine simulator in a subprocess. ``simulate_source`` retains the Python SDK trace used by the original acceptance tests. Choosing the robot format ------------------------- +------------------------- Install ``.[automation]`` for JSON compilation, simulation and inventory integration. This includes ``opentrons-shared-data==8.8.2`` for offline schemas and labware diff --git a/pyproject.toml b/pyproject.toml index 1e77bfb..623c020 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ test = [ dev = [ "pytest>=7,<9", "pytest-cov[all]", - "ruff>=0.14.0", + "ruff==0.16.8", "build>=1.2", "twine>=5.0", ] @@ -73,7 +73,26 @@ where = ["src"] [dependency-groups] dev = [ - "ruff>=0.14.0", + "ruff==0.16.8", +] + +[tool.ruff] +required-version = "==0.16.8" + +[tool.ruff.lint.flake8-bugbear] +# These constructors return frozen records whose default fields are immutable. +extend-immutable-calls = [ + "buildcompiler.protocols.AssemblyConfig", + "buildcompiler.protocols.TransformationConfig", + "buildcompiler.protocols.PlatingConfig", + "buildcompiler.protocols.OpentronsAssemblyProfile", + "buildcompiler.protocols.OpentronsTransformationProfile", + "buildcompiler.protocols.OpentronsPlatingProfile", + "buildcompiler.protocols.methods.assembly.AssemblyConfig", + "buildcompiler.protocols.methods.transformation.TransformationConfig", + "buildcompiler.protocols.methods.plating.PlatingConfig", + "buildcompiler.protocols.models.TemperatureStep", + "buildcompiler.protocols.models.TemperatureProgram", ] diff --git a/setup.py b/setup.py index 716f284..0a39679 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ "dev": [ "pytest>=7,<9", "pytest-cov[all]", - "ruff>=0.14.0", + "ruff==0.16.8", "build>=1.2", "twine>=5.0", ], diff --git a/src/buildcompiler/adapters/pudu/__init__.py b/src/buildcompiler/adapters/pudu/__init__.py index 242d8f4..0dbcb23 100644 --- a/src/buildcompiler/adapters/pudu/__init__.py +++ b/src/buildcompiler/adapters/pudu/__init__.py @@ -23,9 +23,9 @@ "domestication_artifacts_to_pudu_json", "legacy_assembly_route_to_pudu_json", "legacy_assembly_routes_to_pudu_json", - "write_assembly_pudu_input_json", "plasmid_locations_to_pudu_json", + "plating_to_pudu_json", "transformation_to_pudu_json", "transformations_to_pudu_json", - "plating_to_pudu_json", + "write_assembly_pudu_input_json", ] diff --git a/src/buildcompiler/adapters/pudu/assembly_json.py b/src/buildcompiler/adapters/pudu/assembly_json.py index 02b00dc..00aad4a 100644 --- a/src/buildcompiler/adapters/pudu/assembly_json.py +++ b/src/buildcompiler/adapters/pudu/assembly_json.py @@ -1,7 +1,7 @@ """In-memory adapter for compiler-level PUDU assembly JSON payloads.""" -from collections.abc import Sequence import json +from collections.abc import Sequence from pathlib import Path from typing import Any diff --git a/src/buildcompiler/adapters/pudu/transformation_json.py b/src/buildcompiler/adapters/pudu/transformation_json.py index 4678895..15017b0 100644 --- a/src/buildcompiler/adapters/pudu/transformation_json.py +++ b/src/buildcompiler/adapters/pudu/transformation_json.py @@ -4,7 +4,6 @@ from buildcompiler.domain import IndexedPlasmid, TransformationProtocolSpec - PUDU_96_WELL_ORDER = tuple( f"{row}{column}" for column in range(1, 13) for row in "ABCDEFGH" ) diff --git a/src/buildcompiler/api/__init__.py b/src/buildcompiler/api/__init__.py index 909d7a1..0d193f4 100644 --- a/src/buildcompiler/api/__init__.py +++ b/src/buildcompiler/api/__init__.py @@ -1,5 +1,10 @@ """Public API contracts, options, and compiler facade for BuildCompiler.""" +from buildcompiler.api.protocols import ( + compile_assembly, + compile_plating, + compile_transformation, +) from buildcompiler.errors import ( BuildCompilerError, SynBioHubAuthenticationError, @@ -10,11 +15,6 @@ SynBioHubResponseError, ) from buildcompiler.inventory import index_collections -from buildcompiler.api.protocols import ( - compile_assembly, - compile_transformation, - compile_plating, -) from .compiler import ( BuildCompiler, @@ -62,9 +62,10 @@ ) __all__ = [ + "SCHEMA_VERSION", + "ApprovalOptions", "BuildCompiler", "BuildCompilerError", - "ApprovalOptions", "BuildOptions", "CombinatorialOptions", "DomesticationOptions", @@ -76,8 +77,6 @@ "ReagentOptions", "ReportingOptions", "SelectionOptions", - "TransformationOptions", - "SCHEMA_VERSION", "SerializationError", "SynBioHubAuthenticationError", "SynBioHubConfigurationError", @@ -85,17 +84,17 @@ "SynBioHubNetworkError", "SynBioHubResourceError", "SynBioHubResponseError", + "TransformationOptions", "assembly_lvl1", "assembly_lvl2", - "domestication", - "full_build", - "index_collections", - "transformation", "compile_assembly", - "compile_transformation", "compile_plating", + "compile_transformation", "deserialize_build_plan", + "domestication", "dumps_json_dto", + "full_build", + "index_collections", "serialize_approval", "serialize_backbone", "serialize_build_plan", @@ -111,4 +110,5 @@ "serialize_unsupported_planning_record", "serialize_warning", "to_json_dto", + "transformation", ] diff --git a/src/buildcompiler/api/compiler.py b/src/buildcompiler/api/compiler.py index de11568..1215998 100644 --- a/src/buildcompiler/api/compiler.py +++ b/src/buildcompiler/api/compiler.py @@ -2,8 +2,8 @@ from __future__ import annotations -from dataclasses import dataclass, field from collections.abc import Sequence +from dataclasses import dataclass, field from typing import Any import sbol2 @@ -43,7 +43,7 @@ def from_synbiohub( auth_token: str | None = None, sbol_doc: sbol2.Document | None = None, options: BuildOptions | None = None, - ) -> "BuildCompiler": + ) -> BuildCompiler: """Create a compiler from token-authenticated SynBioHub collections. The token is assigned only to a transient ``sbol2.PartShop`` while the @@ -320,8 +320,10 @@ def _resolve_component( component = ( value if isinstance(value, sbol2.ComponentDefinition) else document.find(value) ) - if not isinstance(component, sbol2.ComponentDefinition): + if component is None: raise ValueError(f"ComponentDefinition not found: {value}") + if not isinstance(component, sbol2.ComponentDefinition): + raise TypeError(f"Expected ComponentDefinition: {value}") return component @@ -331,6 +333,8 @@ def _resolve_module( module = ( value if isinstance(value, sbol2.ModuleDefinition) else document.find(value) ) - if not isinstance(module, sbol2.ModuleDefinition): + if module is None: raise ValueError(f"ModuleDefinition not found: {value}") + if not isinstance(module, sbol2.ModuleDefinition): + raise TypeError(f"Expected ModuleDefinition: {value}") return module diff --git a/src/buildcompiler/execution/executor.py b/src/buildcompiler/execution/executor.py index d1f4d74..8164cda 100644 --- a/src/buildcompiler/execution/executor.py +++ b/src/buildcompiler/execution/executor.py @@ -70,7 +70,7 @@ def from_dependencies( logger: Any = None, resolver: SbolResolver | None = None, **stage_overrides: Any, - ) -> "FullBuildExecutor": + ) -> FullBuildExecutor: active_resolver = resolver or SbolResolver(sbol_document) return cls( context=BuildContext( @@ -203,7 +203,7 @@ def execute( stage_results=stage_results, options=self.context.options.protocol, ) - except Exception as exc: + except Exception as exc: # noqa: BLE001 - Preserve compiler results when any output adapter fails. stage_results.append( StageResult( id="protocol:bundle", diff --git a/src/buildcompiler/inventory/__init__.py b/src/buildcompiler/inventory/__init__.py index cdf558e..bc4aa3a 100644 --- a/src/buildcompiler/inventory/__init__.py +++ b/src/buildcompiler/inventory/__init__.py @@ -1,16 +1,16 @@ """Inventory package exports for deterministic lookup/indexing contracts.""" from .compatibility import Lvl1Route, Lvl2Route, RouteScore, RouteSelection -from .inventory import Inventory from .indexing import index_collections +from .inventory import Inventory from .selector import CompatibilitySelector __all__ = [ "CompatibilitySelector", "Inventory", - "index_collections", "Lvl1Route", "Lvl2Route", "RouteScore", "RouteSelection", + "index_collections", ] diff --git a/src/buildcompiler/inventory/indexing.py b/src/buildcompiler/inventory/indexing.py index 7202498..6351b58 100644 --- a/src/buildcompiler/inventory/indexing.py +++ b/src/buildcompiler/inventory/indexing.py @@ -273,7 +273,7 @@ def _insert_identities( identities = { child.identity for child in _children(component, document) - if insert_roles & set(str(role) for role in child.roles) + if insert_roles & {str(role) for role in child.roles} } return sorted(identities) diff --git a/src/buildcompiler/inventory/inventory.py b/src/buildcompiler/inventory/inventory.py index 7de5922..06c0bdb 100644 --- a/src/buildcompiler/inventory/inventory.py +++ b/src/buildcompiler/inventory/inventory.py @@ -12,7 +12,6 @@ MaterialState, ) - _MATERIAL_ORDER = { MaterialState.PLANNED: 0, MaterialState.GENERATED: 1, diff --git a/src/buildcompiler/inventory/selector.py b/src/buildcompiler/inventory/selector.py index 4eaf93f..f0fa4f8 100644 --- a/src/buildcompiler/inventory/selector.py +++ b/src/buildcompiler/inventory/selector.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Mapping, Sequence -from itertools import permutations, product +from itertools import pairwise, permutations, product from typing import Any from buildcompiler.api.options import BuildOptions @@ -16,7 +16,6 @@ ) from buildcompiler.inventory.inventory import Inventory - _STATE_RANK = { MaterialState.PLANNED: 0, MaterialState.GENERATED: 1, @@ -73,7 +72,7 @@ def _key(p: Any) -> tuple[int, int, str]: state_penalty = -_STATE_RANK[p.state] if prefer_state else 0 return (generated_penalty, state_penalty, p.identity) - return sorted(filtered, key=_key)[0] + return min(filtered, key=_key) def _candidate_key(self, candidate: Any) -> tuple[int, int, str]: prefer_existing = self.options.selection.prefer_existing_collection_material @@ -93,7 +92,7 @@ def _chain_is_compatible(self, chain: Sequence[Any]) -> bool: return True if any(len(sites) != 2 for sites in annotated): return False - return all(left[1] == right[0] for left, right in zip(annotated, annotated[1:])) + return all(left[1] == right[0] for left, right in pairwise(annotated)) def _select_compatible_chain( self, candidate_groups: Sequence[list[Any]] diff --git a/src/buildcompiler/planning/__init__.py b/src/buildcompiler/planning/__init__.py index 61c009f..27152f2 100644 --- a/src/buildcompiler/planning/__init__.py +++ b/src/buildcompiler/planning/__init__.py @@ -11,10 +11,10 @@ __all__ = [ "BuildPlan", - "UnsupportedPlanningRecord", - "FullBuildPlanner", "DomesticationPlan", "DomesticationPlanner", + "FullBuildPlanner", "SequenceEditProposal", + "UnsupportedPlanningRecord", "select_deterministic_flanking_sequence", ] diff --git a/src/buildcompiler/planning/combinatorial.py b/src/buildcompiler/planning/combinatorial.py index 101ec02..f8592e8 100644 --- a/src/buildcompiler/planning/combinatorial.py +++ b/src/buildcompiler/planning/combinatorial.py @@ -1,6 +1,9 @@ from __future__ import annotations + import itertools + import sbol2 + from buildcompiler.api.options import BuildOptions from buildcompiler.domain import BuildRequest, BuildStage, BuildWarning, DesignKind from buildcompiler.planning.classifier import request_id_for @@ -13,7 +16,7 @@ def _collect_variant_sets(derivation): variables.sort( key=lambda variable: (str(getattr(variable, "variable", "")), variable.identity) ) - return variables, [sorted(list(vc.variants), key=str) for vc in variables] + return variables, [sorted(vc.variants, key=str) for vc in variables] def expand_combinatorial_derivation( @@ -22,7 +25,7 @@ def expand_combinatorial_derivation( warnings = [] unsupported = [] requests = [] - variables, variant_sets = _collect_variant_sets(derivation) + _variables, variant_sets = _collect_variant_sets(derivation) if not variant_sets or any(len(v) == 0 for v in variant_sets): unsupported.append( UnsupportedPlanningRecord( diff --git a/src/buildcompiler/planning/validation.py b/src/buildcompiler/planning/validation.py index b00c70e..ffc2e52 100644 --- a/src/buildcompiler/planning/validation.py +++ b/src/buildcompiler/planning/validation.py @@ -97,7 +97,7 @@ def ordered_lvl1_parts( try: ordered_components = list(component_definition.getInSequentialOrder()) - except Exception: + except Exception: # noqa: BLE001 - Unusable SDK ordering falls back to role order below. ordered_components = [] if len(ordered_components) == 4: diff --git a/src/buildcompiler/protocols/__init__.py b/src/buildcompiler/protocols/__init__.py index 7321a57..3898150 100644 --- a/src/buildcompiler/protocols/__init__.py +++ b/src/buildcompiler/protocols/__init__.py @@ -35,19 +35,19 @@ "CompiledProtocol", "MaterialRef", "OpentronsAssemblyProfile", - "ProtocolCompiler", + "OpentronsPlatingProfile", + "OpentronsTransformationProfile", + "OutputManifest", + "PlatingConfig", + "PlatingRequest", "ProtocolBackend", + "ProtocolCompiler", "ProtocolPlan", - "assembly_request_from_json", "TransformationConfig", "TransformationReaction", "TransformationRequest", - "OpentronsTransformationProfile", - "transformation_request_from_json", - "plasmid_manifest_from_json", - "PlatingRequest", - "PlatingConfig", - "OpentronsPlatingProfile", + "assembly_request_from_json", "bacterium_manifest_from_json", - "OutputManifest", + "plasmid_manifest_from_json", + "transformation_request_from_json", ] diff --git a/src/buildcompiler/sbol/__init__.py b/src/buildcompiler/sbol/__init__.py index 0fe6149..98cf1f3 100644 --- a/src/buildcompiler/sbol/__init__.py +++ b/src/buildcompiler/sbol/__init__.py @@ -23,8 +23,8 @@ "DomesticationService", "PullPolicy", "SbolResolver", - "load_synbiohub_collections", "TransformationJob", "TransformationSbolResult", "TransformationService", + "load_synbiohub_collections", ] diff --git a/src/buildcompiler/sbol/assembly.py b/src/buildcompiler/sbol/assembly.py index 3fbb854..698545c 100644 --- a/src/buildcompiler/sbol/assembly.py +++ b/src/buildcompiler/sbol/assembly.py @@ -130,7 +130,7 @@ def _component_from_record( f"Missing SBOL ComponentDefinition for {field_name} record {record.identity}" ) if not isinstance(component, sbol2.ComponentDefinition): - raise ValueError( + raise TypeError( f"{field_name} record {record.identity} must resolve to sbol2.ComponentDefinition" ) return component @@ -165,11 +165,15 @@ def _implementation_from_record( record.metadata.get("implementation_identity") or record.identity ) implementation = source_document.find(impl_identity) - if not isinstance(implementation, sbol2.Implementation): + if implementation is None: raise ValueError( "Missing SBOL Implementation for reagent " f"{record.identity}; expected metadata['implementation_identity'] or identity to resolve" ) + if not isinstance(implementation, sbol2.Implementation): + raise TypeError( + f"Reagent {record.identity} must resolve to sbol2.Implementation" + ) return implementation def _indexed_product_from_legacy_product( diff --git a/src/buildcompiler/sbol/domestication.py b/src/buildcompiler/sbol/domestication.py index 14bdf06..70124d4 100644 --- a/src/buildcompiler/sbol/domestication.py +++ b/src/buildcompiler/sbol/domestication.py @@ -2,8 +2,8 @@ from __future__ import annotations -from dataclasses import dataclass, field import hashlib +from dataclasses import dataclass, field from typing import Any import sbol2 @@ -16,7 +16,6 @@ MaterialState, ) - ROLE_TO_FUSION_SITE_SEQUENCES = { "promoter": ("GGAG", "TACT"), "rbs": ("TACT", "AATG"), @@ -163,7 +162,7 @@ def run(self, job: DomesticationJob) -> DomesticationSbolResult: def _ensure_component(self, component: Any) -> sbol2.ComponentDefinition: if not isinstance(component, sbol2.ComponentDefinition): - raise ValueError( + raise TypeError( "DomesticationJob.part_component must be an sbol2.ComponentDefinition" ) return component @@ -185,9 +184,9 @@ def _apply_sequence_edit_proposals( key=lambda item: int(getattr(item, "position", 0)), reverse=True, ): - position = int(getattr(proposal, "position")) - original = str(getattr(proposal, "original_sequence")).upper() - proposed = str(getattr(proposal, "proposed_sequence")).upper() + position = int(proposal.position) + original = str(proposal.original_sequence).upper() + proposed = str(proposal.proposed_sequence).upper() if sequence[position : position + len(original)] != original: raise ValueError( "Sequence edit proposal does not match source sequence at " diff --git a/src/buildcompiler/sbol/resolver.py b/src/buildcompiler/sbol/resolver.py index 3618983..f171929 100644 --- a/src/buildcompiler/sbol/resolver.py +++ b/src/buildcompiler/sbol/resolver.py @@ -2,8 +2,9 @@ from __future__ import annotations +from collections.abc import Callable from enum import Enum -from typing import Any, Callable +from typing import Any import sbol2 diff --git a/src/buildcompiler/sbol/synbiohub.py b/src/buildcompiler/sbol/synbiohub.py index b11f4e6..a1ecf37 100644 --- a/src/buildcompiler/sbol/synbiohub.py +++ b/src/buildcompiler/sbol/synbiohub.py @@ -29,7 +29,7 @@ def load_synbiohub_collections( try: shop = sbol2.PartShop(sbh_registry) shop.key = auth_token - except Exception as exc: + except Exception as exc: # noqa: BLE001 - Sanitize all upstream errors before exposing them. raise _normalized_error( exc, operation="client initialization", @@ -93,7 +93,7 @@ def _pull( ) -> None: try: shop.pull(identity, document) - except Exception as exc: + except Exception as exc: # noqa: BLE001 - Sanitize all upstream errors before exposing them. raise _normalized_error( exc, operation=operation, @@ -141,7 +141,7 @@ def _sbol_error_code(exc: Exception) -> Any: return None try: return exc.error_code() - except Exception: + except Exception: # noqa: BLE001 - Malformed SDK errors must still be normalized. return None diff --git a/src/buildcompiler/stages/assembly_lvl2.py b/src/buildcompiler/stages/assembly_lvl2.py index ed4549c..e87f7b0 100644 --- a/src/buildcompiler/stages/assembly_lvl2.py +++ b/src/buildcompiler/stages/assembly_lvl2.py @@ -85,8 +85,10 @@ def run( request_ids=[request.id], protocol_artifacts=artifacts, logs=[ - "No lvl2 route selected by CompatibilitySelector. Provide explicit region_order " - "or enable large-order search for large designs.", + ( + "No lvl2 route selected by CompatibilitySelector. Provide explicit region_order " + "or enable large-order search for large designs." + ), ], ) diff --git a/tests/automation/test_json_backend.py b/tests/automation/test_json_backend.py index b08c26d..fd24620 100644 --- a/tests/automation/test_json_backend.py +++ b/tests/automation/test_json_backend.py @@ -4,6 +4,7 @@ import json import pytest +from conftest import engine_actions from buildcompiler.adapters.opentrons import OpentronsSimulationAdapter from buildcompiler.api import ProtocolOptions @@ -15,8 +16,6 @@ ) from buildcompiler.protocols.backends.simulation import SimulationError, analyze_source -from conftest import engine_actions - pytestmark = pytest.mark.automation diff --git a/tests/conftest.py b/tests/conftest.py index 18e616f..1be37d7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ from __future__ import annotations -import sbol2 import pytest +import sbol2 from buildcompiler.api import BuildOptions from buildcompiler.domain import BuildRequest, BuildStage, DesignKind diff --git a/tests/integration/test_full_build_happy_path.py b/tests/integration/test_full_build_happy_path.py index bc31584..e75d249 100644 --- a/tests/integration/test_full_build_happy_path.py +++ b/tests/integration/test_full_build_happy_path.py @@ -13,8 +13,8 @@ StageStatus, ) from buildcompiler.execution import BuildContext, FullBuildExecutor -from buildcompiler.planning import BuildPlan from buildcompiler.inventory import Inventory +from buildcompiler.planning import BuildPlan from buildcompiler.sbol import SbolResolver diff --git a/tests/integration/test_legacy_offline_fixtures.py b/tests/integration/test_legacy_offline_fixtures.py index 1022a4e..7badd6f 100644 --- a/tests/integration/test_legacy_offline_fixtures.py +++ b/tests/integration/test_legacy_offline_fixtures.py @@ -7,7 +7,6 @@ from buildcompiler.domain import IndexedBackbone, IndexedReagent, StageStatus from buildcompiler.inventory import Inventory - COLLECTION_PATHS = [ "tests/test_files/CIDARMoCloParts_collection.xml", "tests/test_files/CIDARMoCloPlasmidsKit_collection.xml", diff --git a/tests/integration/test_missing_lvl1_then_domestication.py b/tests/integration/test_missing_lvl1_then_domestication.py index a0a91a6..036c96d 100644 --- a/tests/integration/test_missing_lvl1_then_domestication.py +++ b/tests/integration/test_missing_lvl1_then_domestication.py @@ -12,8 +12,8 @@ StageStatus, ) from buildcompiler.execution import BuildContext, FullBuildExecutor -from buildcompiler.planning import BuildPlan from buildcompiler.inventory import Inventory +from buildcompiler.planning import BuildPlan from buildcompiler.sbol import SbolResolver diff --git a/tests/integration/test_offline_quickstart_notebook.py b/tests/integration/test_offline_quickstart_notebook.py index d6e93a4..7ba342b 100644 --- a/tests/integration/test_offline_quickstart_notebook.py +++ b/tests/integration/test_offline_quickstart_notebook.py @@ -2,7 +2,6 @@ import pytest - nbformat = pytest.importorskip("nbformat") nbclient = pytest.importorskip("nbclient") diff --git a/tests/unit/adapters/opentrons/test_simulation_boundary.py b/tests/unit/adapters/opentrons/test_simulation_boundary.py index 5dbbb01..7d67670 100644 --- a/tests/unit/adapters/opentrons/test_simulation_boundary.py +++ b/tests/unit/adapters/opentrons/test_simulation_boundary.py @@ -8,9 +8,9 @@ OpentronsSimulationAdapter, OptionalAutomationDependencyError, ProtocolSimulationError, + simulation, ) from buildcompiler.api import ProtocolOptions -from buildcompiler.adapters.opentrons import simulation def test_opentrons_import_is_lazy(): diff --git a/tests/unit/adapters/test_protocol_boundaries.py b/tests/unit/adapters/test_protocol_boundaries.py index be4a638..ca95766 100644 --- a/tests/unit/adapters/test_protocol_boundaries.py +++ b/tests/unit/adapters/test_protocol_boundaries.py @@ -1,6 +1,6 @@ -import sys import hashlib import json +import sys import pytest diff --git a/tests/unit/api/test_synbiohub_factory.py b/tests/unit/api/test_synbiohub_factory.py index a1d9e1b..0ab646b 100644 --- a/tests/unit/api/test_synbiohub_factory.py +++ b/tests/unit/api/test_synbiohub_factory.py @@ -1,5 +1,6 @@ import subprocess import sys +from typing import ClassVar import pytest import sbol2 @@ -16,9 +17,9 @@ class FakePartShop: - resources = {} - calls = [] - instances = [] + resources: ClassVar[dict] = {} + calls: ClassVar[list] = [] + instances: ClassVar[list] = [] failure = None def __init__(self, registry): diff --git a/tests/unit/planning/test_combinatorial.py b/tests/unit/planning/test_combinatorial.py index ea54215..fbacbb6 100644 --- a/tests/unit/planning/test_combinatorial.py +++ b/tests/unit/planning/test_combinatorial.py @@ -1,4 +1,5 @@ import sbol2 + from buildcompiler.api import BuildOptions from buildcompiler.planning.combinatorial import expand_combinatorial_derivation @@ -29,7 +30,7 @@ def _build_comb(valid=True): def test_expansion_and_blocking_behaviors(): comb = _build_comb(valid=True) - reqs, unsupported, warnings = expand_combinatorial_derivation( + reqs, unsupported, _warnings = expand_combinatorial_derivation( comb, options=BuildOptions() ) assert len(reqs) == 1 and unsupported == [] and reqs[0].variant_index == 0 @@ -77,7 +78,7 @@ def test_part_order_follows_template_sequence_not_variable_ids(): doc.add(part) expected_order.append(part.identity) vc.variants = [part.identity] - reqs, unsupported, warnings = expand_combinatorial_derivation( + reqs, unsupported, _warnings = expand_combinatorial_derivation( comb, options=BuildOptions() ) assert len(reqs) == 1 and unsupported == [] diff --git a/tests/unit/planning/test_domestication_planner.py b/tests/unit/planning/test_domestication_planner.py index 2767b0c..dfd9e03 100644 --- a/tests/unit/planning/test_domestication_planner.py +++ b/tests/unit/planning/test_domestication_planner.py @@ -1,5 +1,5 @@ -import sbol2 import pytest +import sbol2 from buildcompiler.constants import PART_ROLES from buildcompiler.planning import ( @@ -26,7 +26,7 @@ def _part( def test_supported_role_produces_plan() -> None: planner = DomesticationPlanner() - part = _part("https://example.org/p", sorted(PART_ROLES)[0], "ATGCGT") + part = _part("https://example.org/p", min(PART_ROLES), "ATGCGT") plan = planner.plan(part) assert plan.part_identity == part.identity assert plan.part_role in {"promoter", "rbs", "cds", "terminator"} @@ -41,7 +41,7 @@ def test_unsupported_role_fails_structurally() -> None: def test_missing_sequence_fails() -> None: planner = DomesticationPlanner() - part = _part("https://example.org/p2", sorted(PART_ROLES)[0]) + part = _part("https://example.org/p2", min(PART_ROLES)) with pytest.raises(ValueError, match="missing a usable DNA sequence"): planner.plan(part) @@ -49,7 +49,7 @@ def test_missing_sequence_fails() -> None: def test_bsai_sites_create_edit_proposals_without_mutating_sequence() -> None: planner = DomesticationPlanner() original = "AAAGGTCTCTTT" - part = _part("https://example.org/p3", sorted(PART_ROLES)[0], original) + part = _part("https://example.org/p3", min(PART_ROLES), original) plan = planner.plan(part) assert len(plan.sequence_edit_proposals) == 1 assert plan.sequence_edit_proposals[0].site_sequence == "GGTCTC" diff --git a/tests/unit/planning/test_validation.py b/tests/unit/planning/test_validation.py index 32b53ef..b8f5cbe 100644 --- a/tests/unit/planning/test_validation.py +++ b/tests/unit/planning/test_validation.py @@ -1,4 +1,5 @@ import sbol2 + from buildcompiler.planning.validation import ( ordered_lvl1_parts, validate_lvl1_cardinality, @@ -35,7 +36,7 @@ def test_validate_cardinality_and_order_fallback_and_warning(): ok, warnings = validate_lvl1_cardinality(lvl1) assert ok is True assert warnings == [] - ordered, ow = ordered_lvl1_parts(lvl1) + ordered, _ow = ordered_lvl1_parts(lvl1) assert len(ordered) == 4 diff --git a/tests/unit/protocols/test_opentrons_json.py b/tests/unit/protocols/test_opentrons_json.py index 47d0dc8..46c82e1 100644 --- a/tests/unit/protocols/test_opentrons_json.py +++ b/tests/unit/protocols/test_opentrons_json.py @@ -193,6 +193,6 @@ def find_spec(self, fullname, path, target=None): assert 'opentrons' not in sys.modules """ process = subprocess.run( - [sys.executable, "-c", code], capture_output=True, text=True + [sys.executable, "-c", code], capture_output=True, text=True, check=False ) assert process.returncode == 0, process.stderr diff --git a/tests/unit/sbol/test_assembly_service.py b/tests/unit/sbol/test_assembly_service.py index 4a977a4..70afc7d 100644 --- a/tests/unit/sbol/test_assembly_service.py +++ b/tests/unit/sbol/test_assembly_service.py @@ -1,5 +1,5 @@ -import sbol2 import pytest +import sbol2 from buildcompiler.domain import ( BuildStage, diff --git a/tests/unit/sbol/test_resolver.py b/tests/unit/sbol/test_resolver.py index 8db078b..dfc8787 100644 --- a/tests/unit/sbol/test_resolver.py +++ b/tests/unit/sbol/test_resolver.py @@ -1,5 +1,5 @@ -import sbol2 import pytest +import sbol2 from buildcompiler.sbol import PullPolicy, SbolResolver diff --git a/tests/unit/stages/test_assembly_lvl2.py b/tests/unit/stages/test_assembly_lvl2.py index 26c3aa4..df9d2da 100644 --- a/tests/unit/stages/test_assembly_lvl2.py +++ b/tests/unit/stages/test_assembly_lvl2.py @@ -221,8 +221,8 @@ def test_assembly_lvl2_incomplete_region_order_blocks_as_hard_constraint(): def test_buildcompiler_assembly_lvl2_respects_supplied_backbone(monkeypatch): from types import SimpleNamespace - from buildcompiler.buildcompiler import BuildCompiler import buildcompiler.buildcompiler as buildcompiler_module + from buildcompiler.buildcompiler import BuildCompiler compiler = BuildCompiler.from_local_documents([]) compiler.BbsI_impl = SimpleNamespace(built="https://example.org/reagents/BbsI") diff --git a/tests/unit/stages/test_transformation.py b/tests/unit/stages/test_transformation.py index 311ede8..f29de09 100644 --- a/tests/unit/stages/test_transformation.py +++ b/tests/unit/stages/test_transformation.py @@ -1,8 +1,8 @@ import sbol2 -from buildcompiler.api import BuildOptions, compile_transformation, compile_plating -from buildcompiler.protocols import TransformationConfig +from buildcompiler.api import BuildOptions, compile_plating, compile_transformation from buildcompiler.domain import IndexedPlasmid, StageStatus +from buildcompiler.protocols import TransformationConfig from buildcompiler.stages import TransformationStage diff --git a/tests/unit/test_core_imports.py b/tests/unit/test_core_imports.py index bc45619..9231d69 100644 --- a/tests/unit/test_core_imports.py +++ b/tests/unit/test_core_imports.py @@ -3,20 +3,22 @@ def test_core_imports_do_not_load_optional_automation_dependencies(): import buildcompiler - from buildcompiler.adapters.pudu import ( - assembly_route_to_pudu_json, - plating_to_pudu_json, - transformation_to_pudu_json, - ) - from buildcompiler.api import BuildCompiler, BuildOptions from buildcompiler import ( BuildCompiler as RootBuildCompiler, + ) + from buildcompiler import ( assembly_lvl1, assembly_lvl2, domestication, full_build, transformation, ) + from buildcompiler.adapters.pudu import ( + assembly_route_to_pudu_json, + plating_to_pudu_json, + transformation_to_pudu_json, + ) + from buildcompiler.api import BuildCompiler, BuildOptions from buildcompiler.execution import FullBuildExecutor from buildcompiler.reporting import BuildGraph, BuildReport, BuildSummary diff --git a/tools/update_api_reference.py b/tools/update_api_reference.py index 52a5b8d..9848a15 100644 --- a/tools/update_api_reference.py +++ b/tools/update_api_reference.py @@ -10,6 +10,7 @@ SECTION_ORDER = [ ("Public API", "buildcompiler.api"), + ("Protocols", "buildcompiler.protocols"), ("Stages", "buildcompiler.stages"), ("Planning", "buildcompiler.planning"), ("Inventory", "buildcompiler.inventory"), @@ -48,7 +49,7 @@ def discover_modules() -> list[str]: modules = [] for path in sorted(PACKAGE_ROOT.rglob("*.py")): name = module_name(path) - if name.endswith(".__init__") or name in EXCLUDED_MODULES: + if path.stem.startswith("_") or name in EXCLUDED_MODULES: continue modules.append(name) return modules diff --git a/uv.lock b/uv.lock index e9f8aa3..df21ca2 100644 --- a/uv.lock +++ b/uv.lock @@ -2542,28 +2542,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/b9/9bd84453ed6dd04688de9b3f3a4146a1698e8faae2ceeccce4e14c67ae17/ruff-0.14.0.tar.gz", hash = "sha256:62ec8969b7510f77945df916de15da55311fade8d6050995ff7f680afe582c57", size = 5452071 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/4e/79d463a5f80654e93fa653ebfb98e0becc3f0e7cf6219c9ddedf1e197072/ruff-0.14.0-py3-none-linux_armv6l.whl", hash = "sha256:58e15bffa7054299becf4bab8a1187062c6f8cafbe9f6e39e0d5aface455d6b3", size = 12494532 }, - { url = "https://files.pythonhosted.org/packages/ee/40/e2392f445ed8e02aa6105d49db4bfff01957379064c30f4811c3bf38aece/ruff-0.14.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:838d1b065f4df676b7c9957992f2304e41ead7a50a568185efd404297d5701e8", size = 13160768 }, - { url = "https://files.pythonhosted.org/packages/75/da/2a656ea7c6b9bd14c7209918268dd40e1e6cea65f4bb9880eaaa43b055cd/ruff-0.14.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:703799d059ba50f745605b04638fa7e9682cc3da084b2092feee63500ff3d9b8", size = 12363376 }, - { url = "https://files.pythonhosted.org/packages/42/e2/1ffef5a1875add82416ff388fcb7ea8b22a53be67a638487937aea81af27/ruff-0.14.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ba9a8925e90f861502f7d974cc60e18ca29c72bb0ee8bfeabb6ade35a3abde7", size = 12608055 }, - { url = "https://files.pythonhosted.org/packages/4a/32/986725199d7cee510d9f1dfdf95bf1efc5fa9dd714d0d85c1fb1f6be3bc3/ruff-0.14.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e41f785498bd200ffc276eb9e1570c019c1d907b07cfb081092c8ad51975bbe7", size = 12318544 }, - { url = "https://files.pythonhosted.org/packages/9a/ed/4969cefd53315164c94eaf4da7cfba1f267dc275b0abdd593d11c90829a3/ruff-0.14.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30a58c087aef4584c193aebf2700f0fbcfc1e77b89c7385e3139956fa90434e2", size = 14001280 }, - { url = "https://files.pythonhosted.org/packages/ab/ad/96c1fc9f8854c37681c9613d825925c7f24ca1acfc62a4eb3896b50bacd2/ruff-0.14.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f8d07350bc7af0a5ce8812b7d5c1a7293cf02476752f23fdfc500d24b79b783c", size = 15027286 }, - { url = "https://files.pythonhosted.org/packages/b3/00/1426978f97df4fe331074baf69615f579dc4e7c37bb4c6f57c2aad80c87f/ruff-0.14.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eec3bbbf3a7d5482b5c1f42d5fc972774d71d107d447919fca620b0be3e3b75e", size = 14451506 }, - { url = "https://files.pythonhosted.org/packages/58/d5/9c1cea6e493c0cf0647674cca26b579ea9d2a213b74b5c195fbeb9678e15/ruff-0.14.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16b68e183a0e28e5c176d51004aaa40559e8f90065a10a559176713fcf435206", size = 13437384 }, - { url = "https://files.pythonhosted.org/packages/29/b4/4cd6a4331e999fc05d9d77729c95503f99eae3ba1160469f2b64866964e3/ruff-0.14.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb732d17db2e945cfcbbc52af0143eda1da36ca8ae25083dd4f66f1542fdf82e", size = 13447976 }, - { url = "https://files.pythonhosted.org/packages/3b/c0/ac42f546d07e4f49f62332576cb845d45c67cf5610d1851254e341d563b6/ruff-0.14.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c958f66ab884b7873e72df38dcabee03d556a8f2ee1b8538ee1c2bbd619883dd", size = 13682850 }, - { url = "https://files.pythonhosted.org/packages/5f/c4/4b0c9bcadd45b4c29fe1af9c5d1dc0ca87b4021665dfbe1c4688d407aa20/ruff-0.14.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7eb0499a2e01f6e0c285afc5bac43ab380cbfc17cd43a2e1dd10ec97d6f2c42d", size = 12449825 }, - { url = "https://files.pythonhosted.org/packages/4b/a8/e2e76288e6c16540fa820d148d83e55f15e994d852485f221b9524514730/ruff-0.14.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c63b2d99fafa05efca0ab198fd48fa6030d57e4423df3f18e03aa62518c565f", size = 12272599 }, - { url = "https://files.pythonhosted.org/packages/18/14/e2815d8eff847391af632b22422b8207704222ff575dec8d044f9ab779b2/ruff-0.14.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:668fce701b7a222f3f5327f86909db2bbe99c30877c8001ff934c5413812ac02", size = 13193828 }, - { url = "https://files.pythonhosted.org/packages/44/c6/61ccc2987cf0aecc588ff8f3212dea64840770e60d78f5606cd7dc34de32/ruff-0.14.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a86bf575e05cb68dcb34e4c7dfe1064d44d3f0c04bbc0491949092192b515296", size = 13628617 }, - { url = "https://files.pythonhosted.org/packages/73/e6/03b882225a1b0627e75339b420883dc3c90707a8917d2284abef7a58d317/ruff-0.14.0-py3-none-win32.whl", hash = "sha256:7450a243d7125d1c032cb4b93d9625dea46c8c42b4f06c6b709baac168e10543", size = 12367872 }, - { url = "https://files.pythonhosted.org/packages/41/77/56cf9cf01ea0bfcc662de72540812e5ba8e9563f33ef3d37ab2174892c47/ruff-0.14.0-py3-none-win_amd64.whl", hash = "sha256:ea95da28cd874c4d9c922b39381cbd69cb7e7b49c21b8152b014bd4f52acddc2", size = 13464628 }, - { url = "https://files.pythonhosted.org/packages/c6/2a/65880dfd0e13f7f13a775998f34703674a4554906167dce02daf7865b954/ruff-0.14.0-py3-none-win_arm64.whl", hash = "sha256:f42c9495f5c13ff841b1da4cb3c2a42075409592825dada7c5885c2c844ac730", size = 12565142 }, +version = "0.16.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966 }, + { url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498 }, + { url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004 }, + { url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558 }, + { url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332 }, + { url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125 }, + { url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694 }, + { url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448 }, + { url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796 }, + { url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115 }, + { url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582 }, + { url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644 }, + { url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569 }, + { url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666 }, + { url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472 }, + { url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899 }, + { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316 }, ] [[package]] @@ -2711,7 +2710,7 @@ requires-dist = [ { name = "pytest", marker = "extra == 'test'", specifier = ">=7,<9" }, { name = "pytest-cov", extras = ["all"], marker = "extra == 'dev'" }, { name = "pytest-cov", extras = ["all"], marker = "extra == 'test'" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.14.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = "==0.16.8" }, { name = "sbol2" }, { name = "sbolinventory", marker = "python_full_version >= '3.10' and extra == 'automation'", git = "https://github.com/DRAGGON-Lab/SBOLInventory.git" }, { name = "twine", marker = "extra == 'dev'", specifier = ">=5.0" }, @@ -2720,7 +2719,7 @@ requires-dist = [ provides-extras = ["test", "dev", "automation", "notebook"] [package.metadata.requires-dev] -dev = [{ name = "ruff", specifier = ">=0.14.0" }] +dev = [{ name = "ruff", specifier = "==0.16.8" }] [[package]] name = "terminado"