Repository: qte77/so101-biolab-automation Files analyzed: 91 Estimated tokens: 243.6k Directory structure: └── qte77-so101-biolab-automation/ ├── README.md ├── AGENT_LEARNINGS.md ├── AGENT_REQUESTS.md ├── AGENTS.md ├── CHANGELOG.md ├── CLAUDE.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── pyproject.toml ├── uv.lock ├── .env.example ├── .gitmessage ├── .lychee.toml ├── .markdownlint.json ├── configs/ │ ├── arms.yaml │ ├── plate_layout.yaml │ └── tool_dock.yaml ├── docs/ │ ├── architecture.md │ ├── demo-scenarios.md │ ├── research.md │ ├── UserStory.md │ └── hardware/ │ └── BOM.md ├── hardware/ │ ├── parts.json │ ├── render.py │ ├── cad/ │ │ ├── fridge_hook.py │ │ ├── gripper_tips.py │ │ ├── pipette_mount.py │ │ ├── plate_holder.py │ │ ├── stl_to_svg.py │ │ ├── theme_svgs.py │ │ ├── tip_rack_holder.py │ │ ├── tool_changer.py │ │ └── tool_dock.py │ ├── scad/ │ │ ├── fridge_hook.scad │ │ ├── gripper_tips.scad │ │ ├── pipette_mount.scad │ │ ├── plate_holder.scad │ │ ├── tip_rack_holder.scad │ │ ├── tool_changer.scad │ │ └── tool_dock.scad │ ├── slicer/ │ │ ├── validate.py │ │ └── profiles/ │ │ ├── pla_plus_02mm.ini │ │ └── tpu_95a_02mm.ini │ ├── stl/ │ │ └── README.md │ └── svg/ ├── scripts/ │ ├── coordinate_cmd.py │ └── run_demo.py ├── src/ │ ├── biolab/ │ │ ├── __init__.py │ │ ├── arms.py │ │ ├── camera.py │ │ ├── pipette.py │ │ ├── plate.py │ │ ├── safety.py │ │ ├── tool_changer.py │ │ └── workflow.py │ └── dashboard/ │ ├── __init__.py │ └── server.py ├── tests/ │ ├── conftest.py │ ├── test_arms.py │ ├── test_camera.py │ ├── test_config_loading.py │ ├── test_coordinate_cmd.py │ ├── test_dashboard.py │ ├── test_pipette.py │ ├── test_plate_coords.py │ ├── test_run_demo.py │ ├── test_safety.py │ ├── test_scad_svg.py │ ├── test_slicer_validate.py │ ├── test_tool_changer.py │ └── test_workflow.py ├── .claude/ │ ├── settings.json │ ├── rules/ │ │ ├── compound-learning.md │ │ ├── context-management.md │ │ ├── core-principles.md │ │ ├── robotics-safety.md │ │ └── testing.md │ └── scripts/ │ └── statusline.sh ├── .devcontainer/ │ └── devcontainer.json └── .github/ ├── dependabot.yaml ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE/ │ ├── bug_report.md │ ├── config.yaml │ └── question.md ├── templates/ │ └── llms.txt.tpl └── workflows/ ├── codeql.yaml ├── generate-sbom.yaml ├── links-fail-fast.yaml ├── pytest.yaml ├── ruff.yaml └── write-llms-txt.yaml ================================================ FILE: README.md ================================================ # so101-biolab-automation [![License](https://img.shields.io/badge/license-Apache2.0-58f4c2.svg)](LICENSE) ![Version](https://img.shields.io/badge/version-0.1.0-58f4c2.svg) [![CodeQL](https://github.com/qte77/so101-biolab-automation/actions/workflows/codeql.yaml/badge.svg)](https://github.com/qte77/so101-biolab-automation/actions/workflows/codeql.yaml) [![CodeFactor](https://www.codefactor.io/repository/github/qte77/so101-biolab-automation/badge)](https://www.codefactor.io/repository/github/qte77/so101-biolab-automation) [![ruff](https://github.com/qte77/so101-biolab-automation/actions/workflows/ruff.yaml/badge.svg)](https://github.com/qte77/so101-biolab-automation/actions/workflows/ruff.yaml) [![pytest](https://github.com/qte77/so101-biolab-automation/actions/workflows/pytest.yaml/badge.svg)](https://github.com/qte77/so101-biolab-automation/actions/workflows/pytest.yaml) [![Link Checker](https://github.com/qte77/so101-biolab-automation/actions/workflows/links-fail-fast.yaml/badge.svg)](https://github.com/qte77/so101-biolab-automation/actions/workflows/links-fail-fast.yaml) [![Flat Repo (UitHub)](https://img.shields.io/badge/Flat_Repo-uithub-800080.svg)](https://uithub.com/qte77/so101-biolab-automation) [![Flat Repo (GitToDoc)](https://img.shields.io/badge/Flat_Repo-GitToDoc-fe4a60.svg)](https://gittodoc.com/qte77/so101-biolab-automation) [![vscode.dev](https://img.shields.io/static/v1?logo=visualstudiocode&label=&message=vscode.dev&labelColor=2c2c32&color=007acc&logoColor=007acc)](https://vscode.dev/github/qte77/so101-biolab-automation) [![Codespace Dev](https://img.shields.io/static/v1?logo=visualstudiocode&label=&message=Codespace%20Dev&labelColor=2c2c32&color=007acc&logoColor=007acc)](https://github.com/codespaces/new?repo=qte77/so101-biolab-automation) Dual SO-101 robotic arm bio-lab automation: 96-well pipetting, tool changing, remote oversight. ## What This Demonstrates - **Teacher-student learning** — Leader arm teaches follower via imitation learning (ACT policy) - **Coordinate commands** — Direct well-to-well pipetting by coordinate grid - **Remote oversight** — WebRTC camera feeds + WebSocket command injection from browser - **Tool changing** — Arms swap between pipette, gripper, and fridge hook autonomously ## Hardware Two [SO-101](https://github.com/therobotstudio/so-arm100) follower arms + one leader arm, controlled via [LeRobot](https://huggingface.co/docs/lerobot/index). ~$350–$650 depending on config. See [docs/hardware/BOM.md](docs/hardware/BOM.md) for full shopping list with links. ## Quick Start ```bash # Setup (all deps + tools) make setup_all # Generate 3D-printed parts (CadQuery preferred, OpenSCAD fallback) make render_parts # Optional: validate printability (PrusaSlicer) make setup_slicer make check_prints # Calibrate arms make calibrate_arms # Teleoperate (teacher-student) make start_teleop # Record pipetting episodes make record_episodes TASK="pipette row A" # Train policy make train_policy # Run demo make run_demo ``` ## Architecture ![Workspace Layout](hardware/svg/system_overview.svg) See [docs/architecture.md](docs/architecture.md) for full system design, module responsibilities, and data flows. ## Project Structure ```text src/biolab/ Core: arm control, pipette, plate coords, tool changer, safety, workflow src/dashboard/ FastAPI server, WebSocket commands, browser UI scripts/ CLI entry points for use cases and demo orchestration configs/ Arm ports, plate layout, tool dock positions (YAML) hardware/cad/ CadQuery scripts — primary STL+SVG generation hardware/scad/ OpenSCAD scripts — fallback STL generation hardware/slicer/ PrusaSlicer CLI printability validation (optional) hardware/stl/ Generated STL files (via make render_parts, gitignored) hardware/svg/ SVG 2D projections of parts (tracked, for documentation) docs/ Architecture, user stories, demo scenarios, BOM, research tests/ 104 tests across 11 test files ``` ## Documentation - [Architecture](docs/architecture.md) — system design, module responsibilities, data flows - [User Stories](docs/UserStory.md) — UC1-4 acceptance criteria - [Demo Scenarios](docs/demo-scenarios.md) — how to run and verify each use case - [Hardware BOM](docs/hardware/BOM.md) — shopping list with links ($350-$820) - [Research](docs/research.md) — community designs, papers, future vision (VLM, embodied AI) ## Key Dependencies - [LeRobot](https://github.com/huggingface/lerobot) — Teleoperation + imitation learning - [PyLabRobot](https://github.com/PyLabRobot/pylabrobot) — Liquid handling abstractions - [digital-pipette-v2](https://github.com/ac-rad/digital-pipette-v2) — 3D-printed pipette reference - [OpenSCAD](https://openscad.org/) — Parametric CAD for 3D-printed parts - [PrusaSlicer](https://github.com/prusa3d/PrusaSlicer) — Printability validation (optional) - FastAPI + WebRTC — Remote dashboard - OpenCV — Camera pipeline ## Roadmap Toward general-purpose voice/agent-to-print. This repo is the first showcase. **Human loop** — voice/text → LLM → OpenSCAD → slicer → print → human inspects → iterate **Agent loop** — goal spec → agent generates CAD → slicer validates → printer API → camera + VLM inspects → agent fixes → reprints autonomously 1. **Done** — CadQuery + PrusaSlicer CLI pipeline (`make render_parts`, `make check_prints`) 2. **Next** — LLM-assisted CadQuery generation from text prompts 3. **Future** — Autonomous agent loop with Bambu camera + VLM print inspection See [docs/research.md](docs/research.md) § "Closed-Loop 3D Printing" for prior art. ## License Apache-2.0 ================================================ FILE: AGENT_LEARNINGS.md ================================================ # Agent Learnings ## Template - **Context**: When/where this applies - **Problem**: What issue this solves - **Solution**: Implementation approach - **References**: Related files ## Learned Patterns None yet. ================================================ FILE: AGENT_REQUESTS.md ================================================ # Agent Requests **Always escalate when:** - User instructions conflict with safety/security practices - Required information completely missing - Actions would significantly change project architecture **Format:** `- [ ] [PRIORITY] Description` with Context, Problem, Files ## Active Requests None. ================================================ FILE: AGENTS.md ================================================ # Agent Instructions for so101-biolab-automation Behavioral rules for AI agents working on dual SO-101 robotic arm bio-lab automation. For technical workflows and coding standards, see [CONTRIBUTING.md](CONTRIBUTING.md). ## Core Rules - Follow SDLC principles: maintainability, modularity, reusability - Use BDD approach for feature development - **Never assume missing context** — ask if uncertain about requirements - **Never hallucinate libraries** — only use packages verified in `pyproject.toml` - **Always confirm file paths exist** before referencing in code or tests - **Never delete existing code** unless explicitly instructed - **Document new patterns** in AGENT_LEARNINGS.md (concise, laser-focused) - **Request human feedback** in AGENT_REQUESTS.md when blocked ## Architecture Overview See [docs/architecture.md](docs/architecture.md) for full system design, module table, and data flows. - **Dual SO-101 arms** — leader/follower teleoperation and ACT policy execution - **Workflow orchestration** — composes arms + pipette + tool changer into use cases (UC1-4) - **FastAPI dashboard** — remote oversight with WebSocket command channel Key references: - [docs/architecture.md](docs/architecture.md) — system design (AUTHORITY) - [docs/UserStory.md](docs/UserStory.md) — acceptance criteria (AUTHORITY) - [docs/demo-scenarios.md](docs/demo-scenarios.md) — how to run demos - [docs/hardware/BOM.md](docs/hardware/BOM.md) — hardware shopping list - [CONTRIBUTING.md](CONTRIBUTING.md) — dev workflow + full documentation hierarchy ## Decision Framework **Priority order:** User instructions → AGENTS.md → CONTRIBUTING.md → project patterns **Information sources:** - Requirements/scope: task description or user instruction (primary) - System design: [docs/architecture.md](docs/architecture.md) (AUTHORITY) - Acceptance criteria: [docs/UserStory.md](docs/UserStory.md) (AUTHORITY) - Hardware constraints: [docs/hardware/BOM.md](docs/hardware/BOM.md) + `configs/*.yaml` - Implementation detail: `src/` code (reference, not authority) **Anti-scope-creep:** Only implement what is explicitly requested. Hardware automation is safety-critical — never add untested behaviour speculatively. ## Quality Thresholds Before starting any task: - **Context**: 8/10 — understand requirements, codebase patterns, hardware constraints - **Clarity**: 7/10 — clear implementation path and expected outcomes - **Alignment**: 8/10 — follows project patterns and hardware safety rules - **Success**: 7/10 — confident in completing task correctly If below threshold: gather more context or escalate to AGENT_REQUESTS.md. ## Agent Quick Reference **Pre-task:** - Read AGENTS.md → CONTRIBUTING.md - Confirm quality thresholds met - Check AGENT_LEARNINGS.md for prior art **During task:** - Use `make` commands (document deviations) - Follow BDD: write tests first, implement iteratively - Tag hardware tests with `@pytest.mark.hardware` **Post-task:** - Run `make validate` — must pass all checks - Update CHANGELOG.md for non-trivial changes - Document new patterns in AGENT_LEARNINGS.md - Escalate to AGENT_REQUESTS.md if blocked ================================================ FILE: CHANGELOG.md ================================================ # Changelog Format based on [Keep a Changelog](https://keepachangelog.com/), [Semantic Versioning](https://semver.org/). ## [Unreleased] ### Added - E2E workflow orchestration (`src/biolab/workflow.py`): UC1 pipetting (single/row/col/full plate), UC2 fridge ops, UC3 tool interchange, UC4 demo mode - `PlateLayout` config loader for workspace-frame coordinates - `create_workflow_context()` factory wiring all modules from YAML - `--use-case` CLI dispatch in `run_demo.py` - Dashboard: `run_workflow` WebSocket command, full component lifespan wiring - `hardware/parts.json` manifest as single source of truth for all 10 printable parts - `hardware/render.py` unified runner — reads manifest, dispatches CadQuery or OpenSCAD - CadQuery scripts (`hardware/cad/`) as primary STL+SVG generator, OpenSCAD (`hardware/scad/`) as runtime fallback - PrusaSlicer validation script (`hardware/slicer/validate.py`) with graceful fallback when slicer unavailable - Makefile targets: `setup_cad`, `setup_scad`, `setup_slicer`, `render_parts`, `check_prints`, `render_all` - Generated STL + SVG: plate holder, tool changer cones, fridge hook - `docs/architecture.md`, `docs/UserStory.md`, `docs/demo-scenarios.md` - `docs/hardware/BOM.md` with first-party sourced links - `docs/research.md` — community designs, papers, future vision - YAML frontmatter on all docs (title, purpose, authority, dates) - Documentation hierarchy in `CONTRIBUTING.md` with authority table - `.github/` infrastructure: issue templates, PR template, dependabot, CI workflows - `.devcontainer/devcontainer.json` for Codespace dev environment - `.claude/` harness: settings.json, rules (core-principles, context-management, compound-learning, robotics-safety, testing), statusline - CC plugins: python-dev, docs-governance, commit-helper, codebase-tools - `LICENSE` (Apache-2.0) - 104 tests across 11 test files ### Changed - Dashboard wired to real `DualArmController` + `SafetyMonitor` via FastAPI lifespan - `camera.py`: cv2 import deferred to `start()` for headless environments - `arms.py`: stub-safe `get_observation`/`send_action`, added `send_to_well()` + `park_all()` - `pipette.py`: fill state tracking with over-aspiration/over-dispense guards - Makefile: `.SILENT`/`.ONESHELL`, `.DEFAULT_GOAL`, `# MARK:` grouped help, removed `render_parts`/`setup_cad` - SVGs generated as isometric wireframes (CadQuery) or 2D projections (OpenSCAD fallback) with dark mode theming - `render_scad` renamed to `render_parts` with CadQuery/OpenSCAD fallback logic - `check_prints` now delegates slicer detection to `validate.py` (no Makefile binary check) - `pyproject.toml`: dependency-groups, pytest ini_options, ruff N/UP, pyright ================================================ FILE: CLAUDE.md ================================================ # Insertions @AGENTS.md ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Technical workflows and coding standards for so101-biolab-automation. For AI agent behavioral rules, see [AGENTS.md](AGENTS.md). ## Instant Commands | Command | Purpose | |---------|---------| | `make setup_dev` | Install dev + test dependencies | | `make setup_all` | Install all dependencies + tools (CadQuery, PrusaSlicer, lychee) | | `make setup_cad` | Install CadQuery for STL+SVG generation | | `make setup_scad` | Install OpenSCAD (fallback CAD) | | `make setup_slicer` | Install PrusaSlicer for printability validation | | `make render_parts` | Generate STL + SVG (CadQuery preferred, OpenSCAD fallback) | | `make check_prints` | Run PrusaSlicer printability checks on STLs | | `make render_all` | Generate parts + validate printability | | `make validate` | Complete pre-commit validation (lint + type check + test) | | `make quick_validate` | Fast development validation (lint + type check) | | `make run_tests` | Run all non-hardware tests with pytest | | `make calibrate_arms` | Calibrate all arms | | `make start_teleop` | Start teleoperation | | `make record_episodes` | Record training episodes | | `make train_policy` | Train ACT policy | | `make serve_dashboard` | Start remote dashboard | **Emergency fallback** (if make commands fail): ```bash uv run ruff format . && uv run ruff check . --fix uv run pyright uv run pytest -m "not hardware" ``` ## Testing Strategy ### Unit Tests (always required) - Mock all hardware and external dependencies using `@patch` - Test business logic and data validation - Mirror `src/` structure in `tests/` ### Hardware Tests (opt-in) - Tag with `@pytest.mark.hardware` — excluded from `make run_tests` by default - Run explicitly: `uv run pytest -m hardware` - Require physical arms connected and calibrated ### BDD Approach - Write tests first, implement code iteratively - All tests must pass before advancing to the next step ## Code Style - **Python 3.12+** with full type hints - **Google-style docstrings** for all functions, classes, methods - **Absolute imports** only (no relative imports) - **Ruff** for formatting and linting, **pyright** for type checking - **YAML configs** for all hardware-specific values — never hardcode ports, positions, or coordinates - **LeRobot API** for all arm operations — never bypass the abstraction layer - Coordinates in mm, SBS 96-well standard (A1 origin top-left, 9mm spacing) - Add `# Reason:` comments for complex logic explaining the *why* ## Pre-commit Checklist 1. `make validate` — lint + type check + tests must all pass 2. Update `CHANGELOG.md` — add entry to `## [Unreleased]` section 3. Commit with conventional commit format (see `.gitmessage`) ## Conventional Commits Types: `feat | fix | build | chore | ci | docs | style | refactor | perf | test` Scopes: `biolab | dashboard | configs | scripts | tests | docs | hardware` ## Documentation Hierarchy Each document has a specific authority. Do not duplicate information across documents — reference the authoritative source instead. | Document | Authority | Audience | Content | |----------|-----------|----------|---------| | [README.md](README.md) | Human entry point | Humans | What, why, quick start, badges, doc links | | [AGENTS.md](AGENTS.md) | Agent entry point | AI agents | Rules, decision framework, all authority references | | [CONTRIBUTING.md](CONTRIBUTING.md) | Dev workflow | Both | Commands, testing, code style, this hierarchy | | [docs/architecture.md](docs/architecture.md) | System design | Both | Module responsibilities, data flows, design decisions | | [docs/UserStory.md](docs/UserStory.md) | Acceptance criteria | Both | User stories US-1.1–US-5.2 with testable criteria | | [docs/demo-scenarios.md](docs/demo-scenarios.md) | Operations | Both | How to run and verify each use case | | [docs/hardware/BOM.md](docs/hardware/BOM.md) | Hardware | Both | Shopping list, vendor links, cost summary | | [CHANGELOG.md](CHANGELOG.md) | Version history | Both | Keep a Changelog format | | [AGENT_LEARNINGS.md](AGENT_LEARNINGS.md) | Patterns | AI agents | Discovered patterns and solutions | | [AGENT_REQUESTS.md](AGENT_REQUESTS.md) | Escalation | AI agents | Blocked items requiring human input | | `.claude/rules/*.md` | Session rules | AI agents | Always-loaded: core-principles, context-management, compound-learning | **Anti-redundancy:** Update the authoritative document, then remove duplicates elsewhere. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to the Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by the Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding any notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS Copyright 2026 qte77 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ # Require GNU Make >= 3.82 (.ONESHELL support) ifeq ($(filter oneshell,$(.FEATURES)),) $(error GNU Make >= 3.82 required (.ONESHELL). macOS ships 3.81 — install via: brew install make, then use gmake) endif .SILENT: .ONESHELL: .PHONY: \ setup_dev setup_all setup_train setup_cad setup_scad setup_slicer setup_rtk setup_lychee \ render_parts check_prints render_all \ lint_code check_links check_types run_tests rerun_tests quick_validate validate \ calibrate_arms start_teleop record_episodes train_policy \ eval_policy serve_dashboard run_demo \ show_help .DEFAULT_GOAL := show_help # -- config -- VERBOSE ?= 0 ifeq ($(VERBOSE),0) RUFF_QUIET := --quiet PYTEST_QUIET := -q --tb=short --no-header PYRIGHT_QUIET := > /dev/null else RUFF_QUIET := PYTEST_QUIET := PYRIGHT_QUIET := endif LEADER_PORT ?= /dev/ttyACM0 FOLLOWER_A_PORT ?= /dev/ttyACM1 FOLLOWER_B_PORT ?= /dev/ttyACM2 HF_USER ?= $(shell hf auth whoami 2>/dev/null | head -n 1) DATASET ?= $(HF_USER)/so101-biolab-pipetting TASK ?= "Pick up pipette tip and aspirate from well A1" NUM_EPISODES ?= 10 POLICY ?= act # MARK: SETUP setup_dev: ## Install dev + test dependencies uv sync setup_all: setup_dev setup_cad setup_slicer setup_lychee ## Install all dependencies + tools setup_train: ## Install training dependencies (torch, wandb) uv sync --group train setup_cad: ## Install CadQuery for SVG wireframe generation (requires Python 3.12) uv sync --group cad setup_scad: ## Install OpenSCAD for parametric STL generation if command -v openscad > /dev/null 2>&1; then echo "openscad already installed: $$(openscad --version 2>&1 | head -1)" else echo "Installing OpenSCAD ..." if command -v apt-get > /dev/null 2>&1; then sudo apt-get update -qq && sudo apt-get install -y -qq openscad elif command -v brew > /dev/null 2>&1; then brew install openscad elif command -v snap > /dev/null 2>&1; then sudo snap install openscad else echo "ERROR: No supported package manager found. Install manually: https://openscad.org/downloads" exit 1 fi fi setup_slicer: ## Install PrusaSlicer for printability validation (optional) if command -v prusa-slicer > /dev/null 2>&1; then echo "PrusaSlicer already installed: $$(prusa-slicer --version 2>&1 | head -1)" else echo "Installing PrusaSlicer ..." if command -v apt-get > /dev/null 2>&1; then sudo apt-get update -qq && sudo apt-get install -y -qq prusa-slicer elif command -v brew > /dev/null 2>&1; then brew install --cask prusaslicer elif command -v flatpak > /dev/null 2>&1; then flatpak install -y flathub com.prusa3d.PrusaSlicer else echo "WARN: Install manually from https://github.com/prusa3d/PrusaSlicer/releases" fi fi setup_rtk: ## Install RTK CLI for token-optimized LLM output if command -v rtk > /dev/null 2>&1; then echo "rtk already installed: $$(rtk --version)" else curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh fi rtk init -g setup_lychee: ## Install lychee link checker if command -v lychee > /dev/null 2>&1; then echo "lychee already installed: $$(lychee --version)" else curl -sSfL https://github.com/lycheeverse/lychee/releases/latest/download/lychee-x86_64-unknown-linux-gnu.tar.gz \ | tar xz -C /usr/local/bin 2>/dev/null \ || echo "Install failed — download manually from https://github.com/lycheeverse/lychee/releases" fi # MARK: HARDWARE render_parts: ## Generate STL + SVG from hardware/parts.json (CadQuery preferred, OpenSCAD fallback) uv run --group cad python hardware/render.py || python3 hardware/render.py check_prints: ## Run PrusaSlicer printability checks on STLs (optional) python hardware/slicer/validate.py --all render_all: render_parts check_prints ## Generate parts + validate printability calibrate_arms: ## Calibrate all arms (leader + followers) lerobot-calibrate --robot.type=so101_follower --robot.port=$(FOLLOWER_A_PORT) --robot.id=arm_a lerobot-calibrate --robot.type=so101_follower --robot.port=$(FOLLOWER_B_PORT) --robot.id=arm_b lerobot-calibrate --teleop.type=so101_leader --teleop.port=$(LEADER_PORT) --teleop.id=leader start_teleop: ## Start teleoperation (leader → follower) lerobot-teleoperate \ --robot.type=so101_follower \ --robot.port=$(FOLLOWER_A_PORT) \ --robot.id=arm_a \ --robot.cameras="{ overhead: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}, wrist: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30}}" \ --teleop.type=so101_leader \ --teleop.port=$(LEADER_PORT) \ --teleop.id=leader \ --display_data=true record_episodes: ## Record teleoperation episodes lerobot-record \ --robot.type=so101_follower \ --robot.port=$(FOLLOWER_A_PORT) \ --robot.id=arm_a \ --robot.cameras="{ overhead: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}, wrist: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30}}" \ --teleop.type=so101_leader \ --teleop.port=$(LEADER_PORT) \ --teleop.id=leader \ --dataset.repo_id=$(DATASET) \ --dataset.num_episodes=$(NUM_EPISODES) \ --dataset.single_task=$(TASK) \ --dataset.streaming_encoding=true \ --display_data=true train_policy: ## Train policy on recorded data lerobot-train \ --dataset.repo_id=$(DATASET) \ --policy.type=$(POLICY) \ --output_dir=outputs/train/$(POLICY)_biolab \ --job_name=$(POLICY)_biolab \ --policy.device=cuda \ --wandb.enable=true # MARK: DEV lint_code: ## Format and lint with ruff uv run ruff format . $(RUFF_QUIET) && uv run ruff check . --fix $(RUFF_QUIET) check_links: ## Check links with lychee if command -v lychee > /dev/null 2>&1; then lychee --config .lychee.toml . else echo "lychee not installed — run: make setup_lychee" fi check_types: ## Run pyright type checking uv run pyright src $(PYRIGHT_QUIET) run_tests: ## Run all tests with pytest uv run pytest $(PYTEST_QUIET) rerun_tests: ## Rerun last failed tests only uv run pytest --lf -x quick_validate: lint_code check_types ## Fast validation (lint + type check) validate: lint_code check_types run_tests ## Full validation (lint + type check + test) # MARK: APP eval_policy: ## Evaluate trained policy python scripts/run_demo.py --mode=eval --arm-port=$(FOLLOWER_A_PORT) serve_dashboard: ## Start remote dashboard uvicorn src.dashboard.server:app --host 0.0.0.0 --port 8080 --reload run_demo: ## Run full demo pipeline python scripts/run_demo.py --mode=full # MARK: HELP show_help: ## Show available recipes grouped by section echo "Usage: make [recipe]" echo "" awk '/^# MARK:/ { \ section = substr($$0, index($$0, ":")+2); \ printf "\n\033[1m%s\033[0m\n", section \ } \ /^[a-zA-Z0-9_-]+:.*?##/ { \ helpMessage = match($$0, /## (.*)/); \ if (helpMessage) { \ recipe = $$1; \ sub(/:/, "", recipe); \ printf " \033[36m%-22s\033[0m %s\n", recipe, substr($$0, RSTART + 3, RLENGTH) \ } \ }' $(MAKEFILE_LIST) ================================================ FILE: pyproject.toml ================================================ [project] name = "so101-biolab-automation" version = "0.1.0" description = "Dual SO-101 robotic arm bio-lab automation: pipetting, tool changing, remote oversight" readme = "README.md" license = "Apache-2.0" requires-python = ">=3.12" dependencies = [ "fastapi>=0.115", "uvicorn[standard]>=0.30", "websockets>=13.0", "opencv-python>=4.10", "pyserial>=3.5", "pyyaml>=6.0", "numpy>=1.26", ] [dependency-groups] dev = [ "ruff>=0.8", "pyright>=1.1", ] test = [ "pytest>=8.0", "pytest-asyncio>=0.24", "httpx>=0.27", ] train = [ "torch>=2.4", "wandb>=0.18", ] cad = [ "cadquery>=2.4", ] [tool.hatch.build.targets.wheel] packages = ["src/biolab", "src/dashboard"] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.uv] default-groups = ["dev", "test"] exclude-newer = "2026-04-04T00:00:00Z" [tool.ruff] target-version = "py312" line-length = 100 src = ["src", "tests"] [tool.ruff.lint] select = ["E", "F", "I", "N", "W", "UP"] [tool.ruff.lint.pydocstyle] convention = "google" [tool.pyright] pythonVersion = "3.12" typeCheckingMode = "basic" include = ["src"] reportMissingImports = "warning" [tool.pytest.ini_options] pythonpath = ["src"] testpaths = ["tests"] addopts = "--strict-markers" asyncio_mode = "auto" markers = [ "integration: tests loading real config files", "network: tests requiring network access", "hardware: tests requiring physical SO-101 arms or cameras", ] ================================================ FILE: uv.lock ================================================ version = 1 revision = 3 requires-python = ">=3.12" [options] exclude-newer = "2026-04-04T00:00:00Z" [[package]] name = "annotated-doc" version = "0.0.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] [[package]] name = "anyio" version = "4.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, ] [[package]] name = "attrs" version = "26.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] [[package]] name = "autobahn" version = "20.12.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "hyperlink" }, { name = "txaio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/36/22/5a612815041387e99855fbeed2c803c3bfbb560b48726506418d5e00e6db/autobahn-20.12.3.tar.gz", hash = "sha256:410a93e0e29882c8b5d5ab05d220b07609b886ef5f23c0b8d39153254ffd6895", size = 1268873, upload-time = "2020-12-19T23:18:46.597Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/2c/cee767d20510e6ca256b4acbfdba55edb0596765d9f214ec555c1bc58acd/autobahn-20.12.3-py2.py3-none-any.whl", hash = "sha256:52ee4236ff9a1fcbbd9500439dcf3284284b37f8a6b31ecc8a36e00cf9f95049", size = 1494411, upload-time = "2020-12-19T23:18:43.556Z" }, ] [[package]] name = "automat" version = "25.4.16" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e3/0f/d40bbe294bbf004d436a8bcbcfaadca8b5140d39ad0ad3d73d1a8ba15f14/automat-25.4.16.tar.gz", hash = "sha256:0017591a5477066e90d26b0e696ddc143baafd87b588cfac8100bc6be9634de0", size = 129977, upload-time = "2025-04-16T20:12:16.002Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/02/ff/1175b0b7371e46244032d43a56862d0af455823b5280a50c63d99cc50f18/automat-25.4.16-py3-none-any.whl", hash = "sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1", size = 42842, upload-time = "2025-04-16T20:12:14.447Z" }, ] [[package]] name = "cadquery" version = "2.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cadquery-ocp" }, { name = "casadi" }, { name = "ezdxf" }, { name = "multimethod" }, { name = "nlopt" }, { name = "path" }, { name = "pyparsing" }, { name = "runtype" }, { name = "trame" }, { name = "trame-components" }, { name = "trame-vtk" }, { name = "trame-vuetify" }, ] sdist = { url = "https://files.pythonhosted.org/packages/03/c7/23e200821c307b7765661f19494edea93aa3ae9d28fee0db72ab20448855/cadquery-2.7.0.tar.gz", hash = "sha256:224e7d861c018bedd9e77b9727bc2a86b086f630596124d13ccc8a92b2151ee6", size = 270261, upload-time = "2026-02-13T16:50:47.701Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/85/46/2544f1d16a912770ecfed25602a273ab0ad4648838decf974781a8f83b78/cadquery-2.7.0-py3-none-any.whl", hash = "sha256:13bdffe3a1c9a7b29d86d43313021173b1620f45223929d2631cf8d4c5dbe2c0", size = 182553, upload-time = "2026-02-13T16:50:46.085Z" }, ] [[package]] name = "cadquery-ocp" version = "7.8.1.1.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "vtk" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/3a/98/7b81196dd990bfbbdeff7858db7d319dede6fef2fb6c153ede9eb409a9e9/cadquery_ocp-7.8.1.1.post1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0022e854a3840efd5c7fc14fe933772613794777d5eb056a4754d44a86baf02a", size = 68590290, upload-time = "2025-01-29T14:34:16.804Z" }, { url = "https://files.pythonhosted.org/packages/b4/b3/aea4e4d84916b6a26bc3635a0aeaa3737b24671ac90c117e5779554eebbb/cadquery_ocp-7.8.1.1.post1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:53dc24aed402b2ae52634a29b3b17e9c01e857b8ac34bb101d4e8fa76d3cd7f7", size = 69523485, upload-time = "2025-01-29T14:34:27.338Z" }, { url = "https://files.pythonhosted.org/packages/fa/3f/4b28aedbbb7c6cd5f1aa4e1d6e9a0f88d138941096a3d70f1878a406075f/cadquery_ocp-7.8.1.1.post1-cp312-cp312-manylinux_2_31_x86_64.whl", hash = "sha256:4882074e86722208153579baaee246be4fb10bda22dc20d101c4151f364207b9", size = 70313551, upload-time = "2025-01-29T14:34:36.484Z" }, { url = "https://files.pythonhosted.org/packages/b9/2f/d8473c8db5f0820449819bf5ce606292ead9e2072712cbdcc5657995f6cb/cadquery_ocp-7.8.1.1.post1-cp312-cp312-win_amd64.whl", hash = "sha256:06982855db94fa0056b922276f0ca94154e5d1eb16f6cba854d704885844924a", size = 51755169, upload-time = "2025-01-29T14:34:45.096Z" }, { url = "https://files.pythonhosted.org/packages/23/1d/f2ef5da38774f3f1d2a55f01567e81190b15f765bbfc8e97d3bfbeff3fd9/cadquery_ocp-7.8.1.1.post1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:08fdc32b79e6f974cf692584be865985161e4fd8cbf854ed64c8c1530458fce3", size = 68589505, upload-time = "2025-01-29T14:34:53.501Z" }, { url = "https://files.pythonhosted.org/packages/54/e0/d2b4a5499af452400a49c85d98e83789acdb2b64826a95b634e9069feff6/cadquery_ocp-7.8.1.1.post1-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:fd5ffec5e27846fe4796db9cdc0748324e4d7c59da7c6c7d86a6eb38e823b3f5", size = 69525070, upload-time = "2025-01-29T14:35:02.585Z" }, { url = "https://files.pythonhosted.org/packages/65/7d/873c560967fc79e4c7c850bdca6418801610acd7f7041a40b71812827588/cadquery_ocp-7.8.1.1.post1-cp313-cp313-manylinux_2_31_x86_64.whl", hash = "sha256:081017e5387debe4bf31a9dc222c2513e26d1860ca990119bfe90a6970a77104", size = 70305329, upload-time = "2025-01-29T14:35:11.799Z" }, { url = "https://files.pythonhosted.org/packages/fe/08/edb59c820f339f7fb35b20a4580839ed91488bffcd3c7ba341f8b971d91c/cadquery_ocp-7.8.1.1.post1-cp313-cp313-win_amd64.whl", hash = "sha256:22877143d06cb52bd7d48a591510f8e72c2fc5768bafebb99e5cf077798ee939", size = 51752390, upload-time = "2025-01-29T14:35:42.303Z" }, ] [[package]] name = "casadi" version = "3.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/92/62/1e98662024915ecb09c6894c26a3f497f4afa66570af3f53db4651fc45f1/casadi-3.7.2.tar.gz", hash = "sha256:b4d7bd8acdc4180306903ae1c9eddaf41be2a3ae2fa7154c57174ae64acdc60d", size = 6053600, upload-time = "2025-09-10T10:05:49.521Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/65/c8/689d085447b1966f42bdb8aa4fbebef49a09697dbee32ab02a865c17ac1b/casadi-3.7.2-cp312-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:309ea41a69c9230390d349b0dd899c6a19504d1904c0756bef463e47fb5c8f9a", size = 47116756, upload-time = "2025-09-10T07:53:00.931Z" }, { url = "https://files.pythonhosted.org/packages/1e/c0/3c4704394a6fd4dfb2123a4fd71ba64a001f340670a3eba45be7a19ac736/casadi-3.7.2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6033381234db810b2247d16c6352e679a009ec4365d04008fc768866e011ed58", size = 42293718, upload-time = "2025-09-10T08:07:16.415Z" }, { url = "https://files.pythonhosted.org/packages/f3/24/4cf05469ddf8544da5e92f359f96d716a97e7482999f085a632bc4ef344a/casadi-3.7.2-cp312-none-manylinux2014_aarch64.whl", hash = "sha256:732f2804d0766454bb75596339e4f2da6662ffb669621da0f630ed4af9e83d6a", size = 47276175, upload-time = "2025-09-10T08:08:09.29Z" }, { url = "https://files.pythonhosted.org/packages/82/08/b5f57fea03128efd5c860673b6ac44776352e6c1af862b8177f4c503fffe/casadi-3.7.2-cp312-none-manylinux2014_i686.whl", hash = "sha256:cf17298ff0c162735bdf9bf72b765c636ae732130604017a3b52e26e35402857", size = 72430454, upload-time = "2025-09-10T08:09:10.781Z" }, { url = "https://files.pythonhosted.org/packages/24/ab/d7233c915b12c005655437c6c4cf0ae46cbbb2b20d743cb5e4881ad3104a/casadi-3.7.2-cp312-none-manylinux2014_x86_64.whl", hash = "sha256:cde616930fa1440ad66f1850670399423edd37354eed9b12e74b3817b98d1187", size = 75568903, upload-time = "2025-09-10T08:10:07.108Z" }, { url = "https://files.pythonhosted.org/packages/3e/b9/5b984124f539656efdf079f3d8f09d73667808ec8d0546e6bce6dc60ade6/casadi-3.7.2-cp312-none-win_amd64.whl", hash = "sha256:81d677d2b020c1307c1eb25eae15686e5de199bb066828c3eaabdfaaaf457ffd", size = 50991347, upload-time = "2025-09-10T08:10:46.629Z" }, { url = "https://files.pythonhosted.org/packages/85/23/f13181cd2ba693aeabdb23e6025b2bbae856a23b2a75c57d0bf94bfb6642/casadi-3.7.2-cp313-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:b53e9cc44e9d45fd0276322e85c721977ab32fefe5147069cf57f23352253479", size = 47112576, upload-time = "2025-09-10T07:53:51.287Z" }, { url = "https://files.pythonhosted.org/packages/09/b7/087fcbfe2a0a0b44e236c9853d7fa7c539db6b8c60ab5702fffd73be5a7c/casadi-3.7.2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:2e37806a2d6a57320da79e200398239ae432a34569afbb0268598dd3381dafb9", size = 42293771, upload-time = "2025-09-10T08:11:30.329Z" }, { url = "https://files.pythonhosted.org/packages/69/85/0512e695a9194795ed126825a2d7781bf1f82a116aaeae9c7525bde7c1d9/casadi-3.7.2-cp313-none-manylinux2014_aarch64.whl", hash = "sha256:8c22837ff751ab22ea3333198427e0dd2aa1f3974a10867de897fe26bcb57438", size = 47276199, upload-time = "2025-09-10T08:12:12.487Z" }, { url = "https://files.pythonhosted.org/packages/50/30/9d130b6956fb2bc9d6d154b12b6f420b1338ce0bc8d99041465ded3df1eb/casadi-3.7.2-cp313-none-manylinux2014_i686.whl", hash = "sha256:4a92b5c28abb8d00ae24ce243fed36df36c53a511449aedcdf4db54f78efaf64", size = 72430461, upload-time = "2025-09-10T08:13:06.594Z" }, { url = "https://files.pythonhosted.org/packages/3f/5b/7120e22f6e22ca77283f4a086ab2e59d107f00bfc952116db41a015385fe/casadi-3.7.2-cp313-none-manylinux2014_x86_64.whl", hash = "sha256:63a406ead6582ddc730ea9bfcb100fc299d0793f2e6b177a967a1495846f9a72", size = 75568903, upload-time = "2025-09-10T08:14:04.404Z" }, { url = "https://files.pythonhosted.org/packages/ba/b7/2c80912fc6655deb6a78fa2ae9aa9a4a3c59ac5daa83f2dd549547441a08/casadi-3.7.2-cp313-none-win_amd64.whl", hash = "sha256:d1a996d5904ba74ee2c0bb9991344c9b0963adc08864ddce908fe92cfdf36bf0", size = 50991336, upload-time = "2025-09-10T08:14:43.907Z" }, { url = "https://files.pythonhosted.org/packages/19/60/76074fcbd1cc246dd542a91ca53ed638133c3fb52ebf8400ea8edffd7a98/casadi-3.7.2-cp314-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:f8faf88720477f63b48e96f443ba557931ddd3f5d7d08fc905148893b5c25917", size = 47116064, upload-time = "2025-09-11T08:15:13.381Z" }, { url = "https://files.pythonhosted.org/packages/66/79/b1eda4d4eeefa51f3b75e51f332e3837b86d063b9b889fdbcb92081f6831/casadi-3.7.2-cp314-none-macosx_11_0_arm64.whl", hash = "sha256:08762169bd464d5a00a15bf28d2ff7deac41d24a1da842a13153a833cd247b61", size = 42295048, upload-time = "2025-09-11T08:15:20.517Z" }, { url = "https://files.pythonhosted.org/packages/f2/06/e52fdaee135ebb5d0a004827848890d66e9e05b2148b4beb2a0150e7418d/casadi-3.7.2-cp314-none-manylinux2014_i686.whl", hash = "sha256:9b5683d06f7c5c2bc044585f2d3591d81ec0ad81de84db29765a8bca8247f9d7", size = 72430409, upload-time = "2025-09-10T14:09:48.176Z" }, { url = "https://files.pythonhosted.org/packages/92/50/8834ae629e425802b66505c9861061439b4510d1aca1c94ea067b129e3b5/casadi-3.7.2-cp314-none-manylinux2014_x86_64.whl", hash = "sha256:8861213b7fc605a67cf9b46b29c05337b70c773c0b9615939a7c8a3361cabed1", size = 75571314, upload-time = "2025-09-10T14:10:45.54Z" }, { url = "https://files.pythonhosted.org/packages/59/3a/aaf951d7921a7b657a3402e1e628ccd2c9dfdc6d29bf5aed209dca93073b/casadi-3.7.2-cp314-none-win_amd64.whl", hash = "sha256:62b7b1f943456447205673865e130ec9d97a6f931239968a46d9a7b40ea8c4c3", size = 50991682, upload-time = "2025-09-10T14:11:24.747Z" }, ] [[package]] name = "certifi" version = "2026.2.25" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] [[package]] name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, ] [[package]] name = "click" version = "8.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, ] [[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" } 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" }, ] [[package]] name = "constantly" version = "23.10.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4d/6f/cb2a94494ff74aa9528a36c5b1422756330a75a8367bf20bd63171fc324d/constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd", size = 13300, upload-time = "2023-10-28T23:18:24.316Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b8/40/c199d095151addf69efdb4b9ca3a4f20f70e20508d6222bffb9b76f58573/constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9", size = 13547, upload-time = "2023-10-28T23:18:23.038Z" }, ] [[package]] name = "contourpy" version = "1.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, ] [[package]] name = "cryptography" version = "46.0.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" }, { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" }, { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" }, { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" }, { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" }, { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" }, { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" }, { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" }, { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" }, { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" }, { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" }, { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" }, { url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" }, { url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" }, { url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147, upload-time = "2026-03-25T23:33:48.249Z" }, { url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" }, { url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" }, { url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" }, { url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" }, { url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" }, { url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" }, { url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" }, { url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" }, { url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" }, { url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" }, { url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" }, { url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511, upload-time = "2026-03-25T23:34:09.892Z" }, { url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692, upload-time = "2026-03-25T23:34:11.613Z" }, { url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" }, { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" }, { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" }, { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" }, { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" }, { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" }, { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" }, { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" }, { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" }, { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" }, { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" }, { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" }, { url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" }, { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" }, ] [[package]] name = "cuda-bindings" version = "13.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cuda-pathfinder" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" }, { url = "https://files.pythonhosted.org/packages/1f/92/f899f7bbb5617bb65ec52a6eac1e9a1447a86b916c4194f8a5001b8cde0c/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955", size = 6320619, upload-time = "2026-03-11T00:12:45.939Z" }, { url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" }, { url = "https://files.pythonhosted.org/packages/18/23/6db3aba46864aee357ab2415135b3fe3da7e9f1fa0221fa2a86a5968099c/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d", size = 6149914, upload-time = "2026-03-11T00:12:52.374Z" }, { url = "https://files.pythonhosted.org/packages/c0/87/87a014f045b77c6de5c8527b0757fe644417b184e5367db977236a141602/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6464b30f46692d6c7f65d4a0e0450d81dd29de3afc1bb515653973d01c2cd6e", size = 5685673, upload-time = "2026-03-11T00:12:56.371Z" }, { url = "https://files.pythonhosted.org/packages/ee/5e/c0fe77a73aaefd3fff25ffaccaac69c5a63eafdf8b9a4c476626ef0ac703/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4af9f3e1be603fa12d5ad6cfca7844c9d230befa9792b5abdf7dd79979c3626", size = 6191386, upload-time = "2026-03-11T00:12:58.965Z" }, { url = "https://files.pythonhosted.org/packages/5f/58/ed2c3b39c8dd5f96aa7a4abef0d47a73932c7a988e30f5fa428f00ed0da1/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df850a1ff8ce1b3385257b08e47b70e959932f5f432d0a4e46a355962b4e4771", size = 5507469, upload-time = "2026-03-11T00:13:04.063Z" }, { url = "https://files.pythonhosted.org/packages/1f/01/0c941b112ceeb21439b05895eace78ca1aa2eaaf695c8521a068fd9b4c00/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8a16384c6494e5485f39314b0b4afb04bee48d49edb16d5d8593fd35bbd231b", size = 6059693, upload-time = "2026-03-11T00:13:06.003Z" }, ] [[package]] name = "cuda-pathfinder" version = "1.5.0" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/93/66/0c02bd330e7d976f83fa68583d6198d76f23581bcbb5c0e98a6148f326e5/cuda_pathfinder-1.5.0-py3-none-any.whl", hash = "sha256:498f90a9e9de36044a7924742aecce11c50c49f735f1bc53e05aa46de9ea4110", size = 49739, upload-time = "2026-03-24T21:14:30.869Z" }, ] [[package]] name = "cuda-toolkit" version = "13.0.2" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, ] [package.optional-dependencies] cublas = [ { name = "nvidia-cublas", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] cudart = [ { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] cufft = [ { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] cufile = [ { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, ] cupti = [ { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] curand = [ { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] cusolver = [ { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] cusparse = [ { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] nvjitlink = [ { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] nvrtc = [ { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] nvtx = [ { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] [[package]] name = "cycler" version = "0.12.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] [[package]] name = "ezdxf" version = "1.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fonttools" }, { name = "numpy" }, { name = "pyparsing" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6d/ff/e2fea17633a4c04abdf260d53e0d67463b01e11d957b8faaf3b195666e10/ezdxf-1.4.3.tar.gz", hash = "sha256:403adf7ce305877f6c9f3c007fe2e5c5df504dfb797032122abedd7170176764", size = 1816226, upload-time = "2025-10-19T03:48:12.137Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d4/24/7c0f057662411c3fa55075108f5135b43f236f262273a505758f045fc125/ezdxf-1.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a30f4062c2b6143581a62e0494cf6203f192795a06a21d7cd794cedc84003012", size = 3550835, upload-time = "2025-10-19T03:51:21.196Z" }, { url = "https://files.pythonhosted.org/packages/45/65/cd21b022f1db333b369447e0d24c94ec48143b2cc8a574dc3addf6efdaf5/ezdxf-1.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f50c39894c517f69604ea64e2f7d311bb47e75d22d9eefff5a7709c300d1294d", size = 2986230, upload-time = "2025-10-19T03:51:22.292Z" }, { url = "https://files.pythonhosted.org/packages/7b/27/ca6d20fc5815548b120b9d64f6648e3625fd5f8e8c0d577455c70bb0173c/ezdxf-1.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7ff3d6befadc6758ad9c16974e8242d7bf887ec4132dd34bd8b87662c960d132", size = 2972595, upload-time = "2025-10-19T03:51:23.693Z" }, { url = "https://files.pythonhosted.org/packages/a8/c0/ad888db5e753e8baffddb39a0a14163a7e6f590662acf819a2da6440d348/ezdxf-1.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aea2c146828de59eef2ef4a2e4788aa8d0440cd82eb3dbcbb8f17284e31b9d14", size = 5810205, upload-time = "2025-10-19T03:55:24.452Z" }, { url = "https://files.pythonhosted.org/packages/da/96/416f3e3c6bf4df7da94aee4566bc6abb2c317f74d9bf8fd348ebc8284d45/ezdxf-1.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cef58f6d2d65b5aa523272cf07f65408af00661b445e15ed23891516c18d9f8b", size = 5808336, upload-time = "2025-10-19T03:55:05.018Z" }, { url = "https://files.pythonhosted.org/packages/c6/cb/3fa51f0774f64d40814bddaf54e7a60649b4b7ac9aaa755e10077377c416/ezdxf-1.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4827eaf03f5f456cf638d310f03eeb95b71f7f4468638d10ec467b359115486c", size = 5777759, upload-time = "2025-10-19T03:55:26.027Z" }, { url = "https://files.pythonhosted.org/packages/fb/4f/dc81f4597f2f530a60d092ac560dcd5ec9314c7d128c7e5b03027c68c18b/ezdxf-1.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:230f5283f3380454e0b066bd9b91de2bbda56c50490fd23767a1e4e6728284f9", size = 5870643, upload-time = "2025-10-19T03:55:06.63Z" }, { url = "https://files.pythonhosted.org/packages/73/0e/cac3816a2f00ca604d699408167de8460bb15f808dc162995ea81d39c3c5/ezdxf-1.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:7a2ec122035e07ff7e68d01cafaf74b916e8c16c0e29aed1dfdc390e00b30467", size = 2920462, upload-time = "2025-10-19T03:52:31.19Z" }, { url = "https://files.pythonhosted.org/packages/3f/1d/349f14b5b0282cabaea15be4f6094deca89fc59c35a29fae4bdb1b5e4643/ezdxf-1.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5d24bf4db3ad5bc10046a10f0ecf3de619e96cf3ed9afe42c44f00ec6661409c", size = 3539526, upload-time = "2025-10-19T03:51:25.153Z" }, { url = "https://files.pythonhosted.org/packages/7e/be/401c736172ec5ef09be23f8ed7a850ff115f64220b4719c041b36fed4f8b/ezdxf-1.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:096b71bc9e1a9cd425648feaf4b9f560e3256a871f5bdb1be1f064820906c21a", size = 2979824, upload-time = "2025-10-19T03:51:26.272Z" }, { url = "https://files.pythonhosted.org/packages/9b/79/fb74c285ddf9fc2c599aa6763fa2d172518cff6a4a172e3f6ee541802ac0/ezdxf-1.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6c0afc6aa7938f959dd6985f42f2617eed77fa35da4cceaf07516fd9591cdccf", size = 2967065, upload-time = "2025-10-19T03:51:28.196Z" }, { url = "https://files.pythonhosted.org/packages/72/6b/cbf69705793a4b3376cf312b1c8bb84ad6a90ec57332c55692f3b6b95fe0/ezdxf-1.4.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7cff30c9987eb250bc4d35e494606affb618727c3b660accd68b74709629a41", size = 5771939, upload-time = "2025-10-19T03:55:27.778Z" }, { url = "https://files.pythonhosted.org/packages/f5/db/2f69f471744cb827dc8b989ec3eb5253b708bb58c9f6d9464d354f743909/ezdxf-1.4.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ccc79f0c375090ac406e3ae0e469451d1f636d1682c8b9a8bb305d92847a33", size = 5769539, upload-time = "2025-10-19T03:55:08.241Z" }, { url = "https://files.pythonhosted.org/packages/28/3a/14d6f7cb33c213b03a8ed48e5ee60bf55aff51992a8951f29c97d68d2f0b/ezdxf-1.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5949b8c7914feca043409f51209ad7711aa7f0a6254ea36bdf28478829b2fa7e", size = 5737837, upload-time = "2025-10-19T03:55:29.5Z" }, { url = "https://files.pythonhosted.org/packages/96/44/de74a6ed02a0ecc9d3fe0c0ba6c302d48ae868b821837a3719f97395c50c/ezdxf-1.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:48c3737b786c81b49fbdc82cda75ea38cb133142d214616e190aac6cc3eb2649", size = 5835253, upload-time = "2025-10-19T03:55:09.822Z" }, { url = "https://files.pythonhosted.org/packages/1f/09/896ab8e30095007a99682b41d4367503e512178331539827c495844a84e0/ezdxf-1.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:d95cd8dcc6e6051ff9adf6eaed0d055ac464abea23c2a71aaba701dcfe7f49de", size = 2917537, upload-time = "2025-10-19T03:52:32.268Z" }, { url = "https://files.pythonhosted.org/packages/65/c9/a3a21eb7fc2c515a73d023052f67ab44c3ba580dfc08a6825fc15cf00cac/ezdxf-1.4.3-py3-none-any.whl", hash = "sha256:19e464aa4525dca3f1dabce165308de7ac262f1122b3c3986320cbec9e8ca6be", size = 1330027, upload-time = "2025-10-19T03:47:33.617Z" }, ] [[package]] name = "fastapi" version = "0.135.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c4/73/5903c4b13beae98618d64eb9870c3fac4f605523dd0312ca5c80dadbd5b9/fastapi-0.135.2.tar.gz", hash = "sha256:88a832095359755527b7f63bb4c6bc9edb8329a026189eed83d6c1afcf419d56", size = 395833, upload-time = "2026-03-23T14:12:41.697Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8f/ea/18f6d0457f9efb2fc6fa594857f92810cadb03024975726db6546b3d6fcf/fastapi-0.135.2-py3-none-any.whl", hash = "sha256:0af0447d541867e8db2a6a25c23a8c4bd80e2394ac5529bd87501bbb9e240ca5", size = 117407, upload-time = "2026-03-23T14:12:43.284Z" }, ] [[package]] name = "filelock" version = "3.25.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, ] [[package]] name = "fonttools" version = "4.62.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" }, { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" }, { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" }, { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" }, { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" }, { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" }, { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" }, { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" }, { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, ] [[package]] name = "fsspec" version = "2026.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, ] [[package]] name = "gitdb" version = "4.0.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "smmap" }, ] sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, ] [[package]] name = "gitpython" version = "3.1.46" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, ] [[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" } 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" }, ] [[package]] name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } 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" } 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" }, ] [[package]] name = "httptools" version = "0.7.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, ] [[package]] name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { 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" } 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" }, ] [[package]] name = "hyperlink" version = "21.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743, upload-time = "2021-01-08T05:51:20.972Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638, upload-time = "2021-01-08T05:51:22.906Z" }, ] [[package]] name = "idna" version = "3.11" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] name = "incremental" version = "24.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/3c/82e84109e02c492f382c711c58a3dd91badda6d746def81a1465f74dc9f5/incremental-24.11.0.tar.gz", hash = "sha256:87d3480dbb083c1d736222511a8cf380012a8176c2456d01ef483242abbbcf8c", size = 24000, upload-time = "2025-11-28T02:30:17.861Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1d/55/0f4df2a44053867ea9cbea73fc588b03c55605cd695cee0a3d86f0029cb2/incremental-24.11.0-py3-none-any.whl", hash = "sha256:a34450716b1c4341fe6676a0598e88a39e04189f4dce5dc96f656e040baa10b3", size = 21109, upload-time = "2025-11-28T02:30:16.442Z" }, ] [[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" } 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" }, ] [[package]] name = "jinja2" version = "3.1.6" 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" } 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" }, ] [[package]] name = "kiwisolver" version = "1.5.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, ] [[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/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" }, ] [[package]] name = "matplotlib" version = "3.10.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "contourpy" }, { name = "cycler" }, { name = "fonttools" }, { name = "kiwisolver" }, { name = "numpy" }, { name = "packaging" }, { name = "pillow" }, { name = "pyparsing" }, { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, ] [[package]] name = "mpmath" version = "1.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] [[package]] name = "multimethod" version = "1.12" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ed/f3/930a6dc1d35b2ab65faffa2a75bbcc67f12d8227857188273783df4e5134/multimethod-1.12.tar.gz", hash = "sha256:8db8ef2a8d2a247e3570cc23317680892fdf903d84c8c1053667c8e8f7671a67", size = 17423, upload-time = "2024-07-04T16:10:08.179Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/af/98/cff14d53a2f2f67d7fe8a4e235a383ee71aba6a1da12aeea24b325d0c72a/multimethod-1.12-py3-none-any.whl", hash = "sha256:fd0c473c43558908d97cc06e4d68e8f69202f167db46f7b4e4058893e7dbdf60", size = 10646, upload-time = "2024-07-04T16:10:06.482Z" }, ] [[package]] name = "networkx" version = "3.6.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] [[package]] name = "nlopt" version = "2.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a4/39/76558756c758962fcf2c6f8450384e43a8e65cb8dfbb8a93d40014b09b3a/nlopt-2.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:19e7a5dd823eab1d167a4fb2f3da13978b997029c9b5e6164d33c747fc7ec542", size = 637168, upload-time = "2025-12-23T15:23:59.667Z" }, { url = "https://files.pythonhosted.org/packages/2e/57/87a00a49664ae90f312cf9fd12262a3803d4f81709e01653bc2be6299b63/nlopt-2.10.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:939a0f3ed1710a6b9493a029744e07e8703f56aea4cfd3010d8619c4fba0df8e", size = 440214, upload-time = "2025-12-23T15:24:00.774Z" }, { url = "https://files.pythonhosted.org/packages/a5/03/3140b6417a4cb113cd0f5d53b27ada263f81f158355ad991aaeee770e10e/nlopt-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:8364bdd98c8265eb87779155f4ab144dd67c7990620244b629f2ebc024d727d1", size = 641684, upload-time = "2025-12-23T15:24:02.094Z" }, { url = "https://files.pythonhosted.org/packages/99/85/9f4d06c156d6007da8594f04343c360978595b0de6c1fa41c2fa1295cb11/nlopt-2.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4ff0577b4e866f5696f44d546368f5ee752a249ac73f9c45d8a29513c5f2430f", size = 636965, upload-time = "2025-12-23T15:24:03.226Z" }, { url = "https://files.pythonhosted.org/packages/c0/eb/1dbdb4fa2ac8550870eef7f74dcd5c35f4c4df58d223e068daeac20f7c98/nlopt-2.10.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c3a9697d24f3cb92b53c37cad7dc8900c7cc125dbe95da73bc92a4fed133eaf0", size = 439869, upload-time = "2025-12-23T15:24:04.598Z" }, { url = "https://files.pythonhosted.org/packages/b6/44/20f39446c3edb9bd80e37fa0f996118170f8509eea0e118595a6c5aa3b18/nlopt-2.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:98cbe9012ae641970366c9935ffcbb9cdcd6deef8521881a3e2ad7d35ed33506", size = 641822, upload-time = "2025-12-23T15:24:05.708Z" }, { url = "https://files.pythonhosted.org/packages/60/87/6e2b468190f8a4467efb9165c94e3ab6c13e03a579fe821fae10479a5003/nlopt-2.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:404ddd3f77958d54edf8b1dbed187730fe2ab13cd6e6fb2e7b80cfac2958460f", size = 636962, upload-time = "2025-12-23T15:24:06.823Z" }, { url = "https://files.pythonhosted.org/packages/10/92/87b81b0d149ef4439c1edd475ac62127904e63efe7aacc89f6279aba957c/nlopt-2.10.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6a49ba09083bdb10c2fde31e2190a16e0e57050a2e98e37ece9b606ad2cb2a31", size = 439883, upload-time = "2025-12-23T15:24:08.233Z" }, { url = "https://files.pythonhosted.org/packages/e8/1d/be16a2bd80f28f7cc838448950c1468ab6fced9939806b6a88396cc4028c/nlopt-2.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:0b0cb9de4270b7ed2d9a299ba379ae6b24d5095b0365a2af9702ff0ccdff5235", size = 660414, upload-time = "2025-12-23T15:24:09.296Z" }, ] [[package]] name = "nodeenv" version = "1.10.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] [[package]] name = "numpy" version = "2.4.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" }, { url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" }, { url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" }, { url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" }, { url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" }, { url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" }, { url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" }, { url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" }, { url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" }, { url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" }, { url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" }, { url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" }, { url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" }, { url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" }, { url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" }, { url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" }, { url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" }, { url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" }, { url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" }, { url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" }, { url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" }, { url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" }, { url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" }, { url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" }, { url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" }, { url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" }, { url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" }, { url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" }, { url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" }, { url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" }, { url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" }, { url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" }, { url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" }, { url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" }, { url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" }, { url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" }, { url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" }, { url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" }, { url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" }, { url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" }, { url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" }, { url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" }, { url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" }, { url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" }, { url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" }, { url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" }, { url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" }, { url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" }, { url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" }, { url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" }, { url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" }, { url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" }, { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" }, ] [[package]] name = "nvidia-cublas" version = "13.1.0.3" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/e1/a5/fce49e2ae977e0ccc084e5adafceb4f0ac0c8333cb6863501618a7277f67/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c86fc7f7ae36d7528288c5d88098edcb7b02c633d262e7ddbb86b0ad91be5df2", size = 542851226, upload-time = "2025-10-09T08:59:04.818Z" }, { url = "https://files.pythonhosted.org/packages/e7/44/423ac00af4dd95a5aeb27207e2c0d9b7118702149bf4704c3ddb55bb7429/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ee8722c1f0145ab246bccb9e452153b5e0515fd094c3678df50b2a0888b8b171", size = 423133236, upload-time = "2025-10-09T08:59:32.536Z" }, ] [[package]] name = "nvidia-cuda-cupti" version = "13.0.85" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, ] [[package]] name = "nvidia-cuda-nvrtc" version = "13.0.88" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, ] [[package]] name = "nvidia-cuda-runtime" version = "13.0.96" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, ] [[package]] name = "nvidia-cudnn-cu13" version = "9.19.0.56" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nvidia-cublas" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" }, { url = "https://files.pythonhosted.org/packages/a3/22/0b4b932655d17a6da1b92fa92ab12844b053bb2ac2475e179ba6f043da1e/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:d20e1734305e9d68889a96e3f35094d733ff1f83932ebe462753973e53a572bf", size = 366066321, upload-time = "2026-02-03T20:44:52.837Z" }, ] [[package]] name = "nvidia-cufft" version = "12.0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, ] [[package]] name = "nvidia-cufile" version = "1.15.1.6" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, ] [[package]] name = "nvidia-curand" version = "10.4.0.35" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, ] [[package]] name = "nvidia-cusolver" version = "12.0.4.66" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nvidia-cublas" }, { name = "nvidia-cusparse" }, { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, ] [[package]] name = "nvidia-cusparse" version = "12.6.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, ] [[package]] name = "nvidia-cusparselt-cu13" version = "0.8.0" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/46/10/8dcd1175260706a2fc92a16a52e306b71d4c1ea0b0cc4a9484183399818a/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:400c6ed1cf6780fc6efedd64ec9f1345871767e6a1a0a552a1ea0578117ea77c", size = 220791277, upload-time = "2025-08-13T19:22:40.982Z" }, { url = "https://files.pythonhosted.org/packages/fd/53/43b0d71f4e702fa9733f8b4571fdca50a8813f1e450b656c239beff12315/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25e30a8a7323935d4ad0340b95a0b69926eee755767e8e0b1cf8dd85b197d3fd", size = 169884119, upload-time = "2025-08-13T19:23:41.967Z" }, ] [[package]] name = "nvidia-nccl-cu13" version = "2.28.9" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/39/55/1920646a2e43ffd4fc958536b276197ed740e9e0c54105b4bb3521591fc7/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:01c873ba1626b54caa12272ed228dc5b2781545e0ae8ba3f432a8ef1c6d78643", size = 196561677, upload-time = "2025-11-18T05:49:03.45Z" }, { url = "https://files.pythonhosted.org/packages/b0/b4/878fefaad5b2bcc6fcf8d474a25e3e3774bc5133e4b58adff4d0bca238bc/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:e4553a30f34195f3fa1da02a6da3d6337d28f2003943aa0a3d247bbc25fefc42", size = 196493177, upload-time = "2025-11-18T05:49:17.677Z" }, ] [[package]] name = "nvidia-nvjitlink" version = "13.0.88" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, ] [[package]] name = "nvidia-nvshmem-cu13" version = "3.4.5" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, ] [[package]] name = "nvidia-nvtx" version = "13.0.85" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, ] [[package]] name = "opencv-python" version = "4.13.0.92" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/fc/6f/5a28fef4c4a382be06afe3938c64cc168223016fa520c5abaf37e8862aa5/opencv_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:caf60c071ec391ba51ed00a4a920f996d0b64e3e46068aac1f646b5de0326a19", size = 46247052, upload-time = "2026-02-05T07:01:25.046Z" }, { url = "https://files.pythonhosted.org/packages/08/ac/6c98c44c650b8114a0fb901691351cfb3956d502e8e9b5cd27f4ee7fbf2f/opencv_python-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:5868a8c028a0b37561579bfb8ac1875babdc69546d236249fff296a8c010ccf9", size = 32568781, upload-time = "2026-02-05T07:01:41.379Z" }, { url = "https://files.pythonhosted.org/packages/3e/51/82fed528b45173bf629fa44effb76dff8bc9f4eeaee759038362dfa60237/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bc2596e68f972ca452d80f444bc404e08807d021fbba40df26b61b18e01838a", size = 47685527, upload-time = "2026-02-05T06:59:11.24Z" }, { url = "https://files.pythonhosted.org/packages/db/07/90b34a8e2cf9c50fe8ed25cac9011cde0676b4d9d9c973751ac7616223a2/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:402033cddf9d294693094de5ef532339f14ce821da3ad7df7c9f6e8316da32cf", size = 70460872, upload-time = "2026-02-05T06:59:19.162Z" }, { url = "https://files.pythonhosted.org/packages/02/6d/7a9cc719b3eaf4377b9c2e3edeb7ed3a81de41f96421510c0a169ca3cfd4/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bccaabf9eb7f897ca61880ce2869dcd9b25b72129c28478e7f2a5e8dee945616", size = 46708208, upload-time = "2026-02-05T06:59:15.419Z" }, { url = "https://files.pythonhosted.org/packages/fd/55/b3b49a1b97aabcfbbd6c7326df9cb0b6fa0c0aefa8e89d500939e04aa229/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:620d602b8f7d8b8dab5f4b99c6eb353e78d3fb8b0f53db1bd258bb1aa001c1d5", size = 72927042, upload-time = "2026-02-05T06:59:23.389Z" }, { url = "https://files.pythonhosted.org/packages/fb/17/de5458312bcb07ddf434d7bfcb24bb52c59635ad58c6e7c751b48949b009/opencv_python-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:372fe164a3148ac1ca51e5f3ad0541a4a276452273f503441d718fab9c5e5f59", size = 30932638, upload-time = "2026-02-05T07:02:14.98Z" }, { url = "https://files.pythonhosted.org/packages/e9/a5/1be1516390333ff9be3a9cb648c9f33df79d5096e5884b5df71a588af463/opencv_python-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:423d934c9fafb91aad38edf26efb46da91ffbc05f3f59c4b0c72e699720706f5", size = 40212062, upload-time = "2026-02-05T07:02:12.724Z" }, ] [[package]] name = "packaging" version = "26.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] name = "path" version = "17.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/dd/52/a7bdd5ef8488977d354b7915d1e75009bebbd04f73eff14e52372d5e9435/path-17.1.1.tar.gz", hash = "sha256:2dfcbfec8b4d960f3469c52acf133113c2a8bf12ac7b98d629fa91af87248d42", size = 50528, upload-time = "2025-07-27T20:40:23.79Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7c/50/11c9ee1ede64b45d687fd36eb8768dafc57afc78b4d83396920cfd69ed30/path-17.1.1-py3-none-any.whl", hash = "sha256:ec7e136df29172e5030dd07e037d55f676bdb29d15bfa09b80da29d07d3b9303", size = 23936, upload-time = "2025-07-27T20:40:22.453Z" }, ] [[package]] name = "pillow" version = "12.1.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, ] [[package]] name = "platformdirs" version = "4.9.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, ] [[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" } 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" }, ] [[package]] name = "protobuf" version = "6.33.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] [[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" } 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" }, ] [[package]] name = "pydantic" version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [[package]] name = "pydantic-core" version = "2.41.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, ] [[package]] name = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pyparsing" version = "3.3.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] name = "pyright" version = "1.1.408" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, ] [[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" } 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" }, ] [[package]] name = "pytest" version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] name = "pytest-asyncio" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] [[package]] name = "python-dateutil" version = "2.9.0.post0" 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" } 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" }, ] [[package]] name = "python-dotenv" version = "1.2.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] name = "pywebvue" version = "1.2.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wslink" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4d/a6/843be8d871bc48599a3cb7e91cd29d81f6654473369bcb898b5d739ce79d/pywebvue-1.2.6.tar.gz", hash = "sha256:889327cf26f205024d88de6f88158d78dc2462ee9fde9ab82e28ca7255a26c5a", size = 4954036, upload-time = "2021-07-27T20:47:38.747Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f8/63/1fdd9edc06399559bcb7b0b7c2a8adc9eddf215ef6faf744567072b2acff/pywebvue-1.2.6-py3-none-any.whl", hash = "sha256:7f9459711c23414d6c9984ab4a12a4f951043697029fbc91c316b47ebc77248d", size = 5002821, upload-time = "2021-07-27T20:47:36.518Z" }, ] [[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/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" }, ] [[package]] name = "requests" version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { 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" } 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" }, ] [[package]] name = "ruff" version = "0.15.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size = 4601277, upload-time = "2026-03-19T16:26:22.605Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size = 10489037, upload-time = "2026-03-19T16:26:32.47Z" }, { url = "https://files.pythonhosted.org/packages/91/4a/82e0fa632e5c8b1eba5ee86ecd929e8ff327bbdbfb3c6ac5d81631bef605/ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477", size = 10955433, upload-time = "2026-03-19T16:27:00.205Z" }, { url = "https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size = 10269302, upload-time = "2026-03-19T16:26:26.183Z" }, { url = "https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size = 10607625, upload-time = "2026-03-19T16:27:03.263Z" }, { url = "https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size = 10324743, upload-time = "2026-03-19T16:27:09.791Z" }, { url = "https://files.pythonhosted.org/packages/7a/87/b8a8f3d56b8d848008559e7c9d8bf367934d5367f6d932ba779456e2f73b/ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0", size = 11138536, upload-time = "2026-03-19T16:27:06.101Z" }, { url = "https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size = 11994292, upload-time = "2026-03-19T16:26:48.718Z" }, { url = "https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size = 11398981, upload-time = "2026-03-19T16:26:54.513Z" }, { url = "https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size = 11242422, upload-time = "2026-03-19T16:26:29.277Z" }, { url = "https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size = 11232158, upload-time = "2026-03-19T16:26:42.321Z" }, { url = "https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size = 10577861, upload-time = "2026-03-19T16:26:57.459Z" }, { url = "https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size = 10327310, upload-time = "2026-03-19T16:26:35.909Z" }, { url = "https://files.pythonhosted.org/packages/99/9e/271afdffb81fe7bfc8c43ba079e9d96238f674380099457a74ccb3863857/ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d", size = 10840752, upload-time = "2026-03-19T16:26:45.723Z" }, { url = "https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size = 11336961, upload-time = "2026-03-19T16:26:39.076Z" }, { url = "https://files.pythonhosted.org/packages/26/6b/8786ba5736562220d588a2f6653e6c17e90c59ced34a2d7b512ef8956103/ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de", size = 10582538, upload-time = "2026-03-19T16:26:15.992Z" }, { url = "https://files.pythonhosted.org/packages/2b/e9/346d4d3fffc6871125e877dae8d9a1966b254fbd92a50f8561078b88b099/ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1", size = 11755839, upload-time = "2026-03-19T16:26:19.897Z" }, { url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" }, ] [[package]] name = "runtype" version = "0.5.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/60/76/d8a0f754c834c3d71a0896b1d40a1938244aab4bb5b4bab0b21d21525694/runtype-0.5.3.tar.gz", hash = "sha256:ccaec05c74f8d213342b9fc25e304560d114bc4d72ec117639cd1e7af9c5db1f", size = 30223, upload-time = "2025-03-03T08:39:00.081Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/80/db/879902580d720925092c86eaab2ceee7493e2fadbb5b718f54289b04d277/runtype-0.5.3-py3-none-any.whl", hash = "sha256:ea8cc6828217ebfda5c159dce969e832efd865a09d6ad1fc993f5bf5e1a627ee", size = 31944, upload-time = "2025-03-03T08:38:58.329Z" }, ] [[package]] name = "sentry-sdk" version = "2.56.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/df/5008954f5466085966468612a7d1638487596ee6d2fd7fb51783a85351bf/sentry_sdk-2.56.0.tar.gz", hash = "sha256:fdab72030b69625665b2eeb9738bdde748ad254e8073085a0ce95382678e8168", size = 426820, upload-time = "2026-03-24T09:56:36.575Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cd/1a/b3a3e9f6520493fed7997af4d2de7965d71549c62f994a8fd15f2ecd519e/sentry_sdk-2.56.0-py2.py3-none-any.whl", hash = "sha256:5afafb744ceb91d22f4cc650c6bd048ac6af5f7412dcc6c59305a2e36f4dbc02", size = 451568, upload-time = "2026-03-24T09:56:34.807Z" }, ] [[package]] name = "setuptools" version = "81.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, ] [[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" } 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" }, ] [[package]] name = "smmap" version = "5.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, ] [[package]] name = "so101-biolab-automation" version = "0.1.0" source = { editable = "." } dependencies = [ { name = "fastapi" }, { name = "numpy" }, { name = "opencv-python" }, { name = "pyserial" }, { name = "pyyaml" }, { name = "uvicorn", extra = ["standard"] }, { name = "websockets" }, ] [package.dev-dependencies] cad = [ { name = "cadquery" }, ] dev = [ { name = "pyright" }, { name = "ruff" }, ] test = [ { name = "httpx" }, { name = "pytest" }, { name = "pytest-asyncio" }, ] train = [ { name = "torch" }, { name = "wandb" }, ] [package.metadata] requires-dist = [ { name = "fastapi", specifier = ">=0.115" }, { name = "numpy", specifier = ">=1.26" }, { name = "opencv-python", specifier = ">=4.10" }, { name = "pyserial", specifier = ">=3.5" }, { name = "pyyaml", specifier = ">=6.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.30" }, { name = "websockets", specifier = ">=13.0" }, ] [package.metadata.requires-dev] cad = [{ name = "cadquery", specifier = ">=2.4" }] dev = [ { name = "pyright", specifier = ">=1.1" }, { name = "ruff", specifier = ">=0.8" }, ] test = [ { name = "httpx", specifier = ">=0.27" }, { name = "pytest", specifier = ">=8.0" }, { name = "pytest-asyncio", specifier = ">=0.24" }, ] train = [ { name = "torch", specifier = ">=2.4" }, { name = "wandb", specifier = ">=0.18" }, ] [[package]] name = "starlette" version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, ] [[package]] name = "sympy" version = "1.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mpmath" }, ] sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] [[package]] name = "torch" version = "2.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, { name = "filelock" }, { name = "fsspec" }, { name = "jinja2" }, { name = "networkx" }, { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, { name = "setuptools" }, { name = "sympy" }, { name = "triton", marker = "sys_platform == 'linux'" }, { name = "typing-extensions" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/6f/8b/69e3008d78e5cee2b30183340cc425081b78afc5eff3d080daab0adda9aa/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b5866312ee6e52ea625cd211dcb97d6a2cdc1131a5f15cc0d87eec948f6dd34", size = 80606338, upload-time = "2026-03-23T18:11:34.781Z" }, { url = "https://files.pythonhosted.org/packages/13/16/42e5915ebe4868caa6bac83a8ed59db57f12e9a61b7d749d584776ed53d5/torch-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f99924682ef0aa6a4ab3b1b76f40dc6e273fca09f367d15a524266db100a723f", size = 419731115, upload-time = "2026-03-23T18:11:06.944Z" }, { url = "https://files.pythonhosted.org/packages/1a/c9/82638ef24d7877510f83baf821f5619a61b45568ce21c0a87a91576510aa/torch-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0f68f4ac6d95d12e896c3b7a912b5871619542ec54d3649cf48cc1edd4dd2756", size = 530712279, upload-time = "2026-03-23T18:10:31.481Z" }, { url = "https://files.pythonhosted.org/packages/1c/ff/6756f1c7ee302f6d202120e0f4f05b432b839908f9071157302cedfc5232/torch-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:fbf39280699d1b869f55eac536deceaa1b60bd6788ba74f399cc67e60a5fab10", size = 114556047, upload-time = "2026-03-23T18:10:55.931Z" }, { url = "https://files.pythonhosted.org/packages/87/89/5ea6722763acee56b045435fb84258db7375c48165ec8be7880ab2b281c5/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6debd97ccd3205bbb37eb806a9d8219e1139d15419982c09e23ef7d4369d18", size = 80606801, upload-time = "2026-03-23T18:10:18.649Z" }, { url = "https://files.pythonhosted.org/packages/32/d1/8ed2173589cbfe744ed54e5a73efc107c0085ba5777ee93a5f4c1ab90553/torch-2.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:63a68fa59de8f87acc7e85a5478bb2dddbb3392b7593ec3e78827c793c4b73fd", size = 419732382, upload-time = "2026-03-23T18:08:30.835Z" }, { url = "https://files.pythonhosted.org/packages/3d/e1/b73f7c575a4b8f87a5928f50a1e35416b5e27295d8be9397d5293e7e8d4c/torch-2.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:cc89b9b173d9adfab59fd227f0ab5e5516d9a52b658ae41d64e59d2e55a418db", size = 530711509, upload-time = "2026-03-23T18:08:47.213Z" }, { url = "https://files.pythonhosted.org/packages/66/82/3e3fcdd388fbe54e29fd3f991f36846ff4ac90b0d0181e9c8f7236565f82/torch-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:4dda3b3f52d121063a731ddb835f010dc137b920d7fec2778e52f60d8e4bf0cd", size = 114555842, upload-time = "2026-03-23T18:09:52.111Z" }, { url = "https://files.pythonhosted.org/packages/db/38/8ac78069621b8c2b4979c2f96dc8409ef5e9c4189f6aac629189a78677ca/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8b394322f49af4362d4f80e424bcaca7efcd049619af03a4cf4501520bdf0fb4", size = 80959574, upload-time = "2026-03-23T18:10:14.214Z" }, { url = "https://files.pythonhosted.org/packages/6d/6c/56bfb37073e7136e6dd86bfc6af7339946dd684e0ecf2155ac0eee687ae1/torch-2.11.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:2658f34ce7e2dabf4ec73b45e2ca68aedad7a5be87ea756ad656eaf32bf1e1ea", size = 419732324, upload-time = "2026-03-23T18:09:36.604Z" }, { url = "https://files.pythonhosted.org/packages/07/f4/1b666b6d61d3394cca306ea543ed03a64aad0a201b6cd159f1d41010aeb1/torch-2.11.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:98bb213c3084cfe176302949bdc360074b18a9da7ab59ef2edc9d9f742504778", size = 530596026, upload-time = "2026-03-23T18:09:20.842Z" }, { url = "https://files.pythonhosted.org/packages/48/6b/30d1459fa7e4b67e9e3fe1685ca1d8bb4ce7c62ef436c3a615963c6c866c/torch-2.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a97b94bbf62992949b4730c6cd2cc9aee7b335921ee8dc207d930f2ed09ae2db", size = 114793702, upload-time = "2026-03-23T18:09:47.304Z" }, { url = "https://files.pythonhosted.org/packages/26/0d/8603382f61abd0db35841148ddc1ffd607bf3100b11c6e1dab6d2fc44e72/torch-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01018087326984a33b64e04c8cb5c2795f9120e0d775ada1f6638840227b04d7", size = 80573442, upload-time = "2026-03-23T18:09:10.117Z" }, { url = "https://files.pythonhosted.org/packages/c7/86/7cd7c66cb9cec6be330fff36db5bd0eef386d80c031b581ec81be1d4b26c/torch-2.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:2bb3cc54bd0dea126b0060bb1ec9de0f9c7f7342d93d436646516b0330cd5be7", size = 419749385, upload-time = "2026-03-23T18:07:33.77Z" }, { url = "https://files.pythonhosted.org/packages/47/e8/b98ca2d39b2e0e4730c0ee52537e488e7008025bc77ca89552ff91021f7c/torch-2.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4dc8b3809469b6c30b411bb8c4cad3828efd26236153d9beb6a3ec500f211a60", size = 530716756, upload-time = "2026-03-23T18:07:50.02Z" }, { url = "https://files.pythonhosted.org/packages/78/88/d4a4cda8362f8a30d1ed428564878c3cafb0d87971fbd3947d4c84552095/torch-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:2b4e811728bd0cc58fb2b0948fe939a1ee2bf1422f6025be2fca4c7bd9d79718", size = 114552300, upload-time = "2026-03-23T18:09:05.617Z" }, { url = "https://files.pythonhosted.org/packages/bf/46/4419098ed6d801750f26567b478fc185c3432e11e2cad712bc6b4c2ab0d0/torch-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8245477871c3700d4370352ffec94b103cfcb737229445cf9946cddb7b2ca7cd", size = 80959460, upload-time = "2026-03-23T18:09:00.818Z" }, { url = "https://files.pythonhosted.org/packages/fd/66/54a56a4a6ceaffb567231994a9745821d3af922a854ed33b0b3a278e0a99/torch-2.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:ab9a8482f475f9ba20e12db84b0e55e2f58784bdca43a854a6ccd3fd4b9f75e6", size = 419735835, upload-time = "2026-03-23T18:07:18.974Z" }, { url = "https://files.pythonhosted.org/packages/b1/e7/0b6665f533aa9e337662dc190425abc0af1fe3234088f4454c52393ded61/torch-2.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:563ed3d25542d7e7bbc5b235ccfacfeb97fb470c7fee257eae599adb8005c8a2", size = 530613405, upload-time = "2026-03-23T18:08:07.014Z" }, { url = "https://files.pythonhosted.org/packages/cf/bf/c8d12a2c86dbfd7f40fb2f56fbf5a505ccf2d9ce131eb559dfc7c51e1a04/torch-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b2a43985ff5ef6ddd923bbcf99943e5f58059805787c5c9a2622bf05ca2965b0", size = 114792991, upload-time = "2026-03-23T18:08:19.216Z" }, ] [[package]] name = "trame" version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pywebvue" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ad/56/aa3668f6482da6ce887dff1242a9c6b96a616bde263001be9d1bc2540300/trame-1.19.1.tar.gz", hash = "sha256:6542180c7e93a79d43e2c6b89bfb4a29db2098626af9f8d2140def50b07987d4", size = 94754, upload-time = "2022-04-19T00:36:47.401Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8e/61/5eb47deff8d7060c86902e697890e4aee3237e26042441d129776c1d4b81/trame-1.19.1-py3-none-any.whl", hash = "sha256:ba88ade1913a9b24746688e2385fbae0327561219b3a6cf9afcced931270ca94", size = 111716, upload-time = "2022-04-19T00:36:45.772Z" }, ] [[package]] name = "trame-client" version = "3.11.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "trame-common" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/a7/9dfddbbe6e4df73fe3365dd35ed35a5a96a1bef9746f8ad10e34731e45e9/trame_client-3.11.4.tar.gz", hash = "sha256:46f8d74e5cfd8dcf240a0357a77ba8047ad7e8fa2bece5ca63eb7a68d245f7e4", size = 241284, upload-time = "2026-03-24T00:20:37.078Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/64/ba/fe836f9ea0407ab85ae39c2ed322b5eb6841a975975cc1be0bc9b760925f/trame_client-3.11.4-py3-none-any.whl", hash = "sha256:adc41582374a5177bbc1688782b7d7b0e161c03cc103c8faf614d532014b0d2c", size = 245353, upload-time = "2026-03-24T00:20:35.433Z" }, ] [[package]] name = "trame-common" version = "1.1.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ae/86/cbb08d6b5229783781a4a1ee882c95ab7c905d163f610b841335e6ddd759/trame_common-1.1.3.tar.gz", hash = "sha256:25a3894823bebf509d3bad2b0c545fbeee9eed5d6320d94f781ec595c18d8068", size = 18632, upload-time = "2026-03-17T22:52:35.223Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c6/40/bf161cf981eebf94bffbe9c23f4b35bf592b44d20b47d734258a17f1729c/trame_common-1.1.3-py3-none-any.whl", hash = "sha256:8d93cda32cfea869aaabaec5d91ded369882b1e7f28c0dba2a101a7896cfa5b2", size = 21977, upload-time = "2026-03-17T22:52:34.191Z" }, ] [[package]] name = "trame-components" version = "2.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "trame-client" }, ] sdist = { url = "https://files.pythonhosted.org/packages/82/47/d773f35615c22553a7547b4336af62e412548ec3607626fa3db128f30460/trame-components-2.5.0.tar.gz", hash = "sha256:df7a1d387b98c5dd71699737804f5288957ca370eb1a60bbe40e89a1f9f62b12", size = 81972, upload-time = "2025-05-30T14:21:36.718Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/02/e9/627ebbf56e00d80300940e8bbf54c6594f98fd214e3565b3abaa5450e0d4/trame_components-2.5.0-py3-none-any.whl", hash = "sha256:897a6c0ebcc72d95a461bde28d2c2e37c4bc4922013ad07df3a65e64d4884672", size = 82345, upload-time = "2025-05-30T14:21:35.526Z" }, ] [[package]] name = "trame-vtk" version = "2.11.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "trame-client" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/7b/7d6f64010f98595df1dfd87bd50701acbff166c99bb88ed43224667b1dbe/trame_vtk-2.11.3.tar.gz", hash = "sha256:99c814edaa855d9dda90015e39e449906774f840ff190f22a46f14dd483c1a1b", size = 808026, upload-time = "2026-03-15T04:42:07.977Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0d/50/a8576c62e0a8b612513dc41e60da428fde95256e35253856b5a65e521e48/trame_vtk-2.11.3-py3-none-any.whl", hash = "sha256:583ed3d5121541d95636efcd05b0c797018c28aa7b77604e5c2bbf58f09aad09", size = 829663, upload-time = "2026-03-15T04:42:05.947Z" }, ] [[package]] name = "trame-vuetify" version = "3.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "trame-client" }, ] sdist = { url = "https://files.pythonhosted.org/packages/91/15/f45c0b63a1fb5696c531dbd43e8b698225b43c0a71939ee5165581e3f217/trame_vuetify-3.2.1.tar.gz", hash = "sha256:1578904a8fc5313ba8033076ea2d9338a050a26c68ceebb207fb6b15e18c0a45", size = 5107294, upload-time = "2026-02-02T17:07:59.366Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/73/08e61712de488fddca38bced13968f2c0fc74bc16e1034c21254246e8528/trame_vuetify-3.2.1-py3-none-any.whl", hash = "sha256:45239b9972bcfb8f121487efe7f537c0dc44174a87535bbd2131a9c86e4053bf", size = 5134606, upload-time = "2026-02-02T17:07:57.609Z" }, ] [[package]] name = "triton" version = "3.6.0" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243, upload-time = "2026-01-20T16:16:07.857Z" }, { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, { url = "https://files.pythonhosted.org/packages/3c/12/34d71b350e89a204c2c7777a9bba0dcf2f19a5bfdd70b57c4dbc5ffd7154/triton-3.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd", size = 176133521, upload-time = "2026-01-20T16:16:13.321Z" }, { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, { url = "https://files.pythonhosted.org/packages/ce/4e/41b0c8033b503fd3cfcd12392cdd256945026a91ff02452bef40ec34bee7/triton-3.6.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1722e172d34e32abc3eb7711d0025bb69d7959ebea84e3b7f7a341cd7ed694d6", size = 176276087, upload-time = "2026-01-20T16:16:18.989Z" }, { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, { url = "https://files.pythonhosted.org/packages/49/55/5ecf0dcaa0f2fbbd4420f7ef227ee3cb172e91e5fede9d0ecaddc43363b4/triton-3.6.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5523241e7d1abca00f1d240949eebdd7c673b005edbbce0aca95b8191f1d43", size = 176138577, upload-time = "2026-01-20T16:16:25.426Z" }, { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, { url = "https://files.pythonhosted.org/packages/48/db/56ee649cab5eaff4757541325aca81f52d02d4a7cd3506776cad2451e060/triton-3.6.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b3a97e8ed304dfa9bd23bb41ca04cdf6b2e617d5e782a8653d616037a5d537d", size = 176274804, upload-time = "2026-01-20T16:16:31.528Z" }, { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, ] [[package]] name = "twisted" version = "25.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "automat" }, { name = "constantly" }, { name = "hyperlink" }, { name = "incremental" }, { name = "typing-extensions" }, { name = "zope-interface" }, ] sdist = { url = "https://files.pythonhosted.org/packages/13/0f/82716ed849bf7ea4984c21385597c949944f0f9b428b5710f79d0afc084d/twisted-25.5.0.tar.gz", hash = "sha256:1deb272358cb6be1e3e8fc6f9c8b36f78eb0fa7c2233d2dbe11ec6fee04ea316", size = 3545725, upload-time = "2025-06-07T09:52:24.858Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/eb/66/ab7efd8941f0bc7b2bd555b0f0471bff77df4c88e0cc31120c82737fec77/twisted-25.5.0-py3-none-any.whl", hash = "sha256:8559f654d01a54a8c3efe66d533d43f383531ebf8d81d9f9ab4769d91ca15df7", size = 3204767, upload-time = "2025-06-07T09:52:21.428Z" }, ] [[package]] name = "txaio" version = "25.12.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7f/67/ea9c9ddbaa3e0b4d53c91f8778a33e42045be352dc7200ed2b9aaa7dc229/txaio-25.12.2.tar.gz", hash = "sha256:9f232c21e12aa1ff52690e365b5a0ecfd42cc27a6ec86e1b92ece88f763f4b78", size = 117393, upload-time = "2025-12-09T15:03:26.527Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/50/05/bdb6318120cac9bf97779674f49035e0595d894b42d4c43b60637bafdb1f/txaio-25.12.2-py3-none-any.whl", hash = "sha256:5f6cd6c6b397fc3305790d15efd46a2d5b91cdbefa96543b4f8666aeb56ba026", size = 31208, upload-time = "2025-12-09T04:30:27.811Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] name = "typing-inspection" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] name = "urllib3" version = "2.6.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] name = "uvicorn" version = "0.42.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0621e62d86c402b4a17ab2be7f7c055d9bd2f638b9e2/uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775", size = 85393, upload-time = "2026-03-16T06:19:50.077Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" }, ] [package.optional-dependencies] standard = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "httptools" }, { name = "python-dotenv" }, { name = "pyyaml" }, { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, { name = "watchfiles" }, { name = "websockets" }, ] [[package]] name = "uvloop" version = "0.22.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, ] [[package]] name = "vtk" version = "9.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "matplotlib" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/6f/ba/1571d61013f3f5778c11741d5de19db197b437d1a52215560f016662597b/vtk-9.3.1-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:05a4b6e387a906e8c8d6844441f9200116e937069fcf81f43e2600f26eb046de", size = 76832738, upload-time = "2024-06-29T03:15:26.41Z" }, { url = "https://files.pythonhosted.org/packages/8e/75/c637c620d23ccecb8ddf58fdb80af1dc56ecdd60f3e018c55e041663398b/vtk-9.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bdbefb1aef9599a0a0b8222c9582f26946732a93534e6ec37d4b8e2c524c627e", size = 70385880, upload-time = "2024-06-29T03:15:33.131Z" }, { url = "https://files.pythonhosted.org/packages/01/ee/730d57c6d7353c1afb919ceedfac387a190ccb92e611c4b14f88e6f39066/vtk-9.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f728bb61f43fce850d622ced3b3d51b3116f767685ca4e4e0076f624e2d2307d", size = 92159868, upload-time = "2024-06-29T03:15:39.072Z" }, { url = "https://files.pythonhosted.org/packages/b1/34/b9b6de4009be2fe90919c4943ae99ae3d465ada73061e928d4744683f915/vtk-9.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:685988e09070e06c8605886591698fd42d8225489509b6537a5046cd034cc93e", size = 52529544, upload-time = "2024-06-29T03:15:43.967Z" }, ] [[package]] name = "wandb" version = "0.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "gitpython" }, { name = "packaging" }, { name = "platformdirs" }, { name = "protobuf" }, { name = "pydantic" }, { name = "pyyaml" }, { name = "requests" }, { name = "sentry-sdk" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/60/bb/eb579bf9abac70934a014a9d4e45346aab307994f3021d201bebe5fa25ec/wandb-0.25.1.tar.gz", hash = "sha256:b2a95cd777ecbe7499599a43158834983448a0048329bc7210ef46ca18d21994", size = 43983308, upload-time = "2026-03-10T23:51:44.227Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e7/d8/873553b6818499d1b1de314067d528b892897baf0dc81fedc0e845abc2dd/wandb-0.25.1-py3-none-macosx_12_0_arm64.whl", hash = "sha256:9bb0679a3e2dcd96db9d9b6d3e17d046241d8d122974b24facb85cc93309a8c9", size = 23615900, upload-time = "2026-03-10T23:51:06.278Z" }, { url = "https://files.pythonhosted.org/packages/71/ea/b131f319aaa5d0bf7572b6bfcff3dd89e1cf92b17eee443bbab71d12d74c/wandb-0.25.1-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:0fb13ed18914027523e7b4fc20380c520e0d10da0ee452f924a13f84509fbe12", size = 25576144, upload-time = "2026-03-10T23:51:11.527Z" }, { url = "https://files.pythonhosted.org/packages/70/5f/81508581f0bb77b0495665c1c78e77606a48e66e855ca71ba7c8ae29efa4/wandb-0.25.1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:cc4521eb5223429ddab5e8eee9b42fdf4caabdf0bc4e0e809042720e5fbef0ed", size = 23070425, upload-time = "2026-03-10T23:51:15.71Z" }, { url = "https://files.pythonhosted.org/packages/f2/c7/445155ef010e2e35d190797d7c36ff441e062a5b566a6da4778e22233395/wandb-0.25.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:e73b4c55b947edae349232d5845204d30fac88e18eb4ad1d4b96bf7cf898405a", size = 25628142, upload-time = "2026-03-10T23:51:19.326Z" }, { url = "https://files.pythonhosted.org/packages/d5/63/f5c55ee00cf481ef1ccd3c385a0585ad52e7840d08419d4f82ddbeeea959/wandb-0.25.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:22b84065aa398e1624d2e5ad79e08bc4d2af41a6db61697b03b3aaba332977c6", size = 23123172, upload-time = "2026-03-10T23:51:23.418Z" }, { url = "https://files.pythonhosted.org/packages/3e/d9/19eb7974c0e9253bcbaee655222c0f0e1a52e63e9479ee711b4208f8ac31/wandb-0.25.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:005c4c6b5126ef8f4b4110e5372d950918b00637d6dc4b615ad17445f9739478", size = 25714479, upload-time = "2026-03-10T23:51:27.421Z" }, { url = "https://files.pythonhosted.org/packages/11/19/466c1d03323a4a0ed7d4036a59b18d6b6f67cb5032e444205927e226b18d/wandb-0.25.1-py3-none-win32.whl", hash = "sha256:8f2d04f16b88d65bfba9d79fb945f6c64e2686215469a841936e0972be8ec6a5", size = 24967338, upload-time = "2026-03-10T23:51:31.833Z" }, { url = "https://files.pythonhosted.org/packages/89/22/680d34c1587f3a979c701b66d71aa7c42b4ef2fdf0774f67034e618e834e/wandb-0.25.1-py3-none-win_amd64.whl", hash = "sha256:62db5166de14456156d7a85953a58733a631228e6d4248a753605f75f75fb845", size = 24967343, upload-time = "2026-03-10T23:51:36.026Z" }, { url = "https://files.pythonhosted.org/packages/c4/e8/76836b75d401ff5912aaf513176e64557ceaec4c4946bfd38a698ff84d48/wandb-0.25.1-py3-none-win_arm64.whl", hash = "sha256:cc7c34b70cf4b7be4d395541e82e325fd9d2be978d62c9ec01f1a7141523b6bb", size = 22080774, upload-time = "2026-03-10T23:51:40.196Z" }, ] [[package]] name = "watchfiles" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, ] [[package]] name = "websockets" version = "16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] [[package]] name = "wslink" version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "autobahn" }, { name = "twisted" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9a/4b/9e971a908e89845f12a3568e3c91b8e3c1f891488c1548c04c78752fb2c9/wslink-0.2.0.tar.gz", hash = "sha256:322d32b15e17c9361bcefa16ac29fbbddfba87769698a786200d9fd2a9637c97", size = 18436, upload-time = "2020-03-18T09:43:37.806Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/11/bf/f7214dfd45ca8051dce823243c437f3a8e3f14b863c90e2613e02b964311/wslink-0.2.0-py2.py3-none-any.whl", hash = "sha256:fdcaf61d31afb84f5a16164be4993592609dcba6d67b66fc9c37be8135150a98", size = 19540, upload-time = "2020-03-18T09:43:36.124Z" }, ] [[package]] name = "zope-interface" version = "8.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/86/a4/77daa5ba398996d16bb43fc721599d27d03eae68fe3c799de1963c72e228/zope_interface-8.2.tar.gz", hash = "sha256:afb20c371a601d261b4f6edb53c3c418c249db1a9717b0baafc9a9bb39ba1224", size = 254019, upload-time = "2026-01-09T07:51:07.253Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e0/a0/1e1fabbd2e9c53ef92b69df6d14f4adc94ec25583b1380336905dc37e9a0/zope_interface-8.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:624b6787fc7c3e45fa401984f6add2c736b70a7506518c3b537ffaacc4b29d4c", size = 208785, upload-time = "2026-01-09T08:05:17.348Z" }, { url = "https://files.pythonhosted.org/packages/c3/2a/88d098a06975c722a192ef1fb7d623d1b57c6a6997cf01a7aabb45ab1970/zope_interface-8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc9ded9e97a0ed17731d479596ed1071e53b18e6fdb2fc33af1e43f5fd2d3aaa", size = 208976, upload-time = "2026-01-09T08:05:18.792Z" }, { url = "https://files.pythonhosted.org/packages/e9/e8/757398549fdfd2f8c89f32c82ae4d2f0537ae2a5d2f21f4a2f711f5a059f/zope_interface-8.2-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:532367553e4420c80c0fc0cabcc2c74080d495573706f66723edee6eae53361d", size = 259411, upload-time = "2026-01-09T08:05:20.567Z" }, { url = "https://files.pythonhosted.org/packages/91/af/502601f0395ce84dff622f63cab47488657a04d0065547df42bee3a680ff/zope_interface-8.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2bf9cf275468bafa3c72688aad8cfcbe3d28ee792baf0b228a1b2d93bd1d541a", size = 264859, upload-time = "2026-01-09T08:05:22.234Z" }, { url = "https://files.pythonhosted.org/packages/89/0c/d2f765b9b4814a368a7c1b0ac23b68823c6789a732112668072fe596945d/zope_interface-8.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0009d2d3c02ea783045d7804da4fd016245e5c5de31a86cebba66dd6914d59a2", size = 264398, upload-time = "2026-01-09T08:05:23.853Z" }, { url = "https://files.pythonhosted.org/packages/4a/81/2f171fbc4222066957e6b9220c4fb9146792540102c37e6d94e5d14aad97/zope_interface-8.2-cp312-cp312-win_amd64.whl", hash = "sha256:845d14e580220ae4544bd4d7eb800f0b6034fe5585fc2536806e0a26c2ee6640", size = 212444, upload-time = "2026-01-09T08:05:25.148Z" }, { url = "https://files.pythonhosted.org/packages/66/47/45188fb101fa060b20e6090e500682398ab415e516a0c228fbb22bc7def2/zope_interface-8.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:6068322004a0158c80dfd4708dfb103a899635408c67c3b10e9acec4dbacefec", size = 209170, upload-time = "2026-01-09T08:05:26.616Z" }, { url = "https://files.pythonhosted.org/packages/09/03/f6b9336c03c2b48403c4eb73a1ec961d94dc2fb5354c583dfb5fa05fd41f/zope_interface-8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2499de92e8275d0dd68f84425b3e19e9268cd1fa8507997900fa4175f157733c", size = 209229, upload-time = "2026-01-09T08:05:28.521Z" }, { url = "https://files.pythonhosted.org/packages/07/b1/65fe1dca708569f302ade02e6cdca309eab6752bc9f80105514f5b708651/zope_interface-8.2-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f777e68c76208503609c83ca021a6864902b646530a1a39abb9ed310d1100664", size = 259393, upload-time = "2026-01-09T08:05:29.897Z" }, { url = "https://files.pythonhosted.org/packages/eb/a5/97b49cfceb6ed53d3dcfb3f3ebf24d83b5553194f0337fbbb3a9fec6cf78/zope_interface-8.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b05a919fdb0ed6ea942e5a7800e09a8b6cdae6f98fee1bef1c9d1a3fc43aaa0", size = 264863, upload-time = "2026-01-09T08:05:31.501Z" }, { url = "https://files.pythonhosted.org/packages/cb/02/0b7a77292810efe3a0586a505b077ebafd5114e10c6e6e659f0c8e387e1f/zope_interface-8.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ccc62b5712dd7bd64cfba3ee63089fb11e840f5914b990033beeae3b2180b6cb", size = 264369, upload-time = "2026-01-09T08:05:32.941Z" }, { url = "https://files.pythonhosted.org/packages/fb/1d/0d1ff3846302ed1b5bbf659316d8084b30106770a5f346b7ff4e9f540f80/zope_interface-8.2-cp313-cp313-win_amd64.whl", hash = "sha256:34f877d1d3bb7565c494ed93828fa6417641ca26faf6e8f044e0d0d500807028", size = 212447, upload-time = "2026-01-09T08:05:35.064Z" }, { url = "https://files.pythonhosted.org/packages/1a/da/3c89de3917751446728b8898b4d53318bc2f8f6bf8196e150a063c59905e/zope_interface-8.2-cp314-cp314-macosx_10_9_x86_64.whl", hash = "sha256:46c7e4e8cbc698398a67e56ca985d19cb92365b4aafbeb6a712e8c101090f4cb", size = 209223, upload-time = "2026-01-09T08:05:36.449Z" }, { url = "https://files.pythonhosted.org/packages/00/7f/62d00ec53f0a6e5df0c984781e6f3999ed265129c4c3413df8128d1e0207/zope_interface-8.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a87fc7517f825a97ff4a4ca4c8a950593c59e0f8e7bfe1b6f898a38d5ba9f9cf", size = 209366, upload-time = "2026-01-09T08:05:38.197Z" }, { url = "https://files.pythonhosted.org/packages/ef/a2/f241986315174be8e00aabecfc2153cf8029c1327cab8ed53a9d979d7e08/zope_interface-8.2-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:ccf52f7d44d669203c2096c1a0c2c15d52e36b2e7a9413df50f48392c7d4d080", size = 261037, upload-time = "2026-01-09T08:05:39.568Z" }, { url = "https://files.pythonhosted.org/packages/02/cc/b321c51d6936ede296a1b8860cf173bee2928357fe1fff7f97234899173f/zope_interface-8.2-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aae807efc7bd26302eb2fea05cd6de7d59269ed6ae23a6de1ee47add6de99b8c", size = 264219, upload-time = "2026-01-09T08:05:41.624Z" }, { url = "https://files.pythonhosted.org/packages/ab/fb/5f5e7b40a2f4efd873fe173624795ca47eaa22e29051270c981361b45209/zope_interface-8.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05a0e42d6d830f547e114de2e7cd15750dc6c0c78f8138e6c5035e51ddfff37c", size = 264390, upload-time = "2026-01-09T08:05:42.936Z" }, { url = "https://files.pythonhosted.org/packages/f9/82/3f2bc594370bc3abd58e5f9085d263bf682a222f059ed46275cde0570810/zope_interface-8.2-cp314-cp314-win_amd64.whl", hash = "sha256:561ce42390bee90bae51cf1c012902a8033b2aaefbd0deed81e877562a116d48", size = 212585, upload-time = "2026-01-09T08:05:44.419Z" }, ] ================================================ FILE: .env.example ================================================ # RTK telemetry opt-out (https://github.com/rtk-ai/rtk) RTK_TELEMETRY_DISABLED=1 ================================================ FILE: .gitmessage ================================================ # [()][!]: # # Types: feat|fix|build|chore|ci|docs|style|refactor|perf|test # Scope: biolab|dashboard|configs|scripts|tests|docs # # Body: Explain *why*, not *what* # # Footer trailers: # BREAKING CHANGE: # Refs: # # Co-Authored-By: Claude ================================================ FILE: .lychee.toml ================================================ # Lychee link checker config # https://lychee.cli.rs/usage/config/ accept = [200, 201, 204, 429] timeout = 30 max_retries = 2 # Exclude URLs that return 401/403/302 to bots but work in browsers. # Grouped by reason. All verified manually on 2026-03-27. exclude = [ # Shields.io badges — rate-limited, transient 429/503 "^https://img\\.shields\\.io", # GitHub special pages — redirect to login "^https://github\\.com/codespaces/new", "^https://vscode\\.dev", # Code quality services — require auth "^https://www\\.codefactor\\.io", # Flat repo viewers — 401 to bots "^https://uithub\\.com", "^https://gittodoc\\.com", # Vendor/commerce sites — 403 bot protection "^https://www\\.alibaba\\.com", "^https://www\\.aliexpress\\.com", "^https://www\\.seeedstudio\\.com", "^https://wiki\\.seeedstudio\\.com", "^https://www\\.waveshare\\.com", "^https://www\\.actuonix\\.com", "^https://www\\.partabot\\.com", "^https://www\\.wowrobo\\.com", "^https://store\\.arduino\\.cc", "^https://www\\.nvidia\\.com", "^https://www\\.raspberrypi\\.com", # Maker community sites — 403 bot protection "^https://www\\.thingiverse\\.com", "^https://makerworld\\.com", "^https://www\\.printables\\.com", "^https://grabcad\\.com", # Academic publishers — 403/timeout to bots "^https://doi\\.org", "^https://www\\.biorxiv\\.org", "^https://huggingface\\.co", "^https://goldberg\\.berkeley\\.edu", "^https://pubs\\.rsc\\.org", "^https://en\\.wikipedia\\.org", # Reference sites — timeout in CI "^https://keepachangelog\\.com", "^https://semver\\.org", ] ================================================ FILE: .markdownlint.json ================================================ { "default": true, "MD003": { "style": "atx" }, "MD010": false, "MD013": false, "MD024": { "siblings_only": true }, "MD025": false, "MD041": { "front_matter_title": "^\\s*title\\s*[:=]" }, "MD060": false } ================================================ FILE: configs/arms.yaml ================================================ # Dual arm configuration # Update ports after running: lerobot-find-port arm_a: arm_id: arm_a port: /dev/ttyACM1 role: follower cameras: wrist: type: opencv index_or_path: 2 width: 640 height: 480 fps: 30 arm_b: arm_id: arm_b port: /dev/ttyACM2 role: follower cameras: wrist: type: opencv index_or_path: 4 width: 640 height: 480 fps: 30 leader: arm_id: leader port: /dev/ttyACM0 role: leader cameras: {} ================================================ FILE: configs/plate_layout.yaml ================================================ # 96-well plate layout configuration # All coordinates in mm, relative to arm base frame plate: # Plate origin (A1 corner) in arm workspace coordinates origin_x_mm: 150.0 origin_y_mm: -50.0 origin_z_mm: 20.0 # Top of plate wells # SBS standard (auto-calculated, override if needed) well_spacing_mm: 9.0 a1_offset_x_mm: 14.38 a1_offset_y_mm: 11.24 tip_rack: # Pipette tip rack position origin_x_mm: 80.0 origin_y_mm: -50.0 origin_z_mm: 45.0 reagent_trough: # Reagent trough position origin_x_mm: 200.0 origin_y_mm: 0.0 origin_z_mm: 25.0 # Heights for approach and insertion heights: safe_z_mm: 80.0 # Safe travel height above deck approach_z_mm: 40.0 # Approach height above wells aspirate_z_mm: 15.0 # Tip depth during aspiration dispense_z_mm: 25.0 # Tip height during dispense ================================================ FILE: configs/tool_dock.yaml ================================================ # Tool dock configuration # Joint positions in degrees [shoulder_pan, shoulder_lift, elbow_flex, wrist_flex, wrist_roll, gripper] stations: pipette: tool: pipette approach_joints: [-90.0, -30.0, -60.0, 0.0, 0.0, 30.0] engage_joints: [-90.0, -15.0, -45.0, 0.0, 0.0, 30.0] dock_joints: [-90.0, -15.0, -45.0, 0.0, 0.0, 0.0] gripper: tool: gripper approach_joints: [-90.0, -30.0, -60.0, 0.0, 90.0, 30.0] engage_joints: [-90.0, -15.0, -45.0, 0.0, 90.0, 30.0] dock_joints: [-90.0, -15.0, -45.0, 0.0, 90.0, 0.0] fridge_hook: tool: fridge_hook approach_joints: [-90.0, -30.0, -60.0, 0.0, 180.0, 30.0] engage_joints: [-90.0, -15.0, -45.0, 0.0, 180.0, 30.0] dock_joints: [-90.0, -15.0, -45.0, 0.0, 180.0, 0.0] ================================================ FILE: docs/architecture.md ================================================ --- title: Architecture purpose: System design, module responsibilities, data flows, stub mode, design decisions authority: System design (AUTHORITY) created: 2026-03-27 updated: 2026-03-27 --- # Architecture ## System Overview ```text ┌──────────────────────────────────────────────────────┐ │ Remote Dashboard │ │ FastAPI + WebSocket commands + REST /api/status │ │ src/dashboard/server.py │ └──────────────────┬───────────────────────────────────┘ │ WebSocket: e_stop, heartbeat, │ target_well, run_workflow ┌──────────────────▼───────────────────────────────────┐ │ Workflow Orchestration │ │ src/biolab/workflow.py │ │ Composes modules into use cases (UC1-4) │ │ Loads PlateLayout from configs/plate_layout.yaml │ ├──────────┬──────────┬──────────┬─────────────────────┤ │ Arms │ Pipette │ Tool │ Safety │ │ Control │ Control │ Changer │ Monitor │ │ arms.py │pipette.py│tool_ch.. │ safety.py │ └────┬─────┴────┬─────┴────┬─────┴─────────────────────┘ │ │ │ │ ┌────▼────┐ │ │ │ Plate │ │ │ │ Geometry │ │ │ │plate.py │ │ │ └─────────┘ │ │ │ ┌────▼─────────────────────▼───┐ │ LeRobot / Serial / Hardware │ │ (stub mode when unavailable)│ └──────────────────────────────┘ ``` ## Module Responsibilities | Module | File | Responsibility | Dependencies | |--------|------|----------------|--------------| | **Workflow** | `src/biolab/workflow.py` | Orchestrates UC1-4 by composing other modules | arms, pipette, plate, tool_changer | | **Arms** | `src/biolab/arms.py` | Dual SO-101 arm control via LeRobot. Stub mode when lerobot absent. | plate (for send_to_well), safety (for PARK_POSITION) | | **Pipette** | `src/biolab/pipette.py` | Digital pipette serial control. Tracks fill state. Stub mode when pyserial absent. | None | | **Plate** | `src/biolab/plate.py` | Pure SBS 96-well coordinate math. No hardware deps. | None | | **Tool Changer** | `src/biolab/tool_changer.py` | 3-tool magnetic dock: pipette, gripper, fridge hook. | arms (for send_action) | | **Camera** | `src/biolab/camera.py` | Multi-camera capture via OpenCV. Stub mode when cv2 absent. | None | | **Safety** | `src/biolab/safety.py` | Watchdog, e-stop, joint limits. Pure Python. | None | | **Dashboard** | `src/dashboard/server.py` | FastAPI server with WebSocket commands and REST status. | All biolab modules | ## Data Flow: Pipetting a Well ```text User/Dashboard → workflow.pipette_well() 1. arm.send_to_well("arm_a", "TROUGH") → arms.py → LeRobot (stub: log only) 2. pipette.aspirate(50.0) → pipette.py → Arduino serial (stub: track fill) 3. arm.send_to_well("arm_a", "A1") → arms.py → plate.py (SBS coords) → LeRobot 4. pipette.dispense(50.0) → pipette.py → Arduino serial (stub: track fill) ``` ## Data Flow: Tool Change ```text workflow.uc2_fridge_open_grab_move() 1. changer.change_tool(FRIDGE_HOOK) → tool_changer.py → arms.send_action (dock sequence) 2. arm.send_action(FRIDGE_APPROACH) → arms.py → LeRobot 3. arm.send_action(FRIDGE_HOOK_ENGAGED) → arms.py → LeRobot 4. changer.change_tool(GRIPPER) → tool_changer.py → arms.send_action (dock sequence) 5. arm.send_action(FRIDGE_GRAB) → arms.py → LeRobot ``` ## Configuration Files | File | Loaded By | Content | |------|-----------|---------| | `configs/arms.yaml` | `DualArmConfig.from_yaml()` | Arm ports, motor IDs, camera devices | | `configs/plate_layout.yaml` | `PlateLayout.from_yaml()` | Workspace origin, Z heights, trough position | | `configs/tool_dock.yaml` | `ToolDockConfig.from_yaml()` | Joint arrays for 3 dock stations | ## Stub Mode Every hardware-dependent module gracefully degrades when its dependency is unavailable: | Module | Trigger | Behavior | |--------|---------|----------| | arms.py | `ImportError` on `lerobot` | `send_action` logs and returns; `get_observation` returns `{"stub": True}` | | pipette.py | `ImportError` on `serial` | Fill state tracked in memory; `_move_to` is a no-op | | camera.py | `ImportError` on `cv2` | `start()` returns; `get_frames()` returns `{}` | This allows the full workflow to run end-to-end without any hardware attached. Run `make run_tests` to verify. ## Key Design Decisions - **No IK**: `send_to_well()` computes correct (x, y) mm coordinates but sends `[0.0]*6` stub joints. Real inverse kinematics deferred to MVP (requires calibrated hardware). - **PlateLayout in workflow.py**: Separates pure SBS math (plate.py) from hardware workspace config (workflow.py). Single responsibility. - **No fridge module**: Fridge joint arrays are constants in workflow.py. YAGNI until hardware exists. - **Dashboard runs workflow in thread**: `run_workflow` WebSocket command spawns `uc4_demo_all` in a daemon thread to avoid blocking the event loop. ================================================ FILE: docs/demo-scenarios.md ================================================ --- title: Demo Scenarios purpose: Runnable commands and expected output for every use case (UC1-4) authority: Operations (AUTHORITY) created: 2026-03-27 updated: 2026-03-27 --- # Demo Scenarios How to run and verify each use case. All commands work in stub mode (no hardware). ## Prerequisites ```bash make setup_all # install all dependencies + tools make serve # (optional) start dashboard on :8080 ``` ## UC1: Pipette a 96-Well Plate ### UC1.1: Single Well Aspirate 50 µL from trough, dispense to well A1. ```bash python scripts/run_demo.py --use-case uc1_single --well A1 --volume 50 ``` Expected output: ```text [UC1] Move arm_a → TROUGH (200.0, 0.0 mm) [UC1] Aspirate 50.0 µL Moving arm_a to well A1 (14.38, 11.24 mm) [UC1] Dispense 50.0 µL → A1 Demo complete. ``` ### UC1.2: Row Pipette all 12 wells in row A at 25 µL each. ```bash python scripts/run_demo.py --use-case uc1_row --row A --volume 25 ``` Expected: 12 aspirate/dispense cycles (A1 through A12). ### UC1.3: Column Pipette all 8 wells in column 1 at 20 µL each. ```bash python scripts/run_demo.py --use-case uc1_col --col 1 --volume 20 ``` Expected: 8 aspirate/dispense cycles (A1 through H1). ### UC1.4: Full Plate Pipette all 96 wells at 20 µL each. ```bash python scripts/run_demo.py --use-case uc1_full --volume 20 ``` Expected: 96 aspirate/dispense cycles. Each well gets an independent aspirate→dispense so the pipette never overflows its 200 µL capacity. ## UC2: Fridge Operations Open fridge with hook tool, swap to gripper, grab item, move to park. ```bash python scripts/run_demo.py --use-case uc2 ``` Expected output: ```text [UC2] Starting fridge sequence Tool changed to fridge_hook [UC2] Approach fridge [UC2] Engage hook — pull door [UC2] Release hook — door open Tool changed to gripper [UC2] Grab item from fridge Parking arm arm_a Parking arm arm_b [UC2] Fridge sequence complete — item at park position ``` ## UC3: Tool Interchange Cycle through all tools: pipette → gripper → fridge hook → gripper. ```bash python scripts/run_demo.py --use-case uc3 ``` Expected output: ```text [UC3] Starting tool cycle: ['pipette', 'gripper', 'fridge_hook', 'gripper'] [UC3] Equipped pipette [UC3] Equipped gripper [UC3] Equipped fridge_hook [UC3] Equipped gripper ``` ## UC4: Full Demo (All Use Cases) Runs UC1.1 + UC1.2 (row A) + UC1.2 (column 1) + UC2 + UC3 in sequence. ```bash python scripts/run_demo.py ``` Or equivalently: ```bash python scripts/run_demo.py --use-case all ``` ## Dashboard Demo Start the dashboard and trigger the workflow via WebSocket: ```bash make serve_dashboard ``` Open `http://localhost:8080` in a browser. Click "Run Demo" button. The full UC4 sequence runs in the background. Check `GET /api/status` for current state. Or via curl: ```bash # Check status curl http://localhost:8080/api/status # Trigger e-stop via WebSocket (using websocat) echo '{"command": "e_stop"}' | websocat ws://localhost:8080/ws ``` ## Verification All scenarios are covered by automated tests: ```bash uv run pytest tests/test_workflow.py -v # workflow use case tests uv run pytest tests/test_dashboard.py -v # dashboard integration tests uv run pytest -q # full suite ``` ================================================ FILE: docs/research.md ================================================ --- title: Research Findings purpose: Community designs, academic papers, future vision (VLM, embodied AI, PCR use case) authority: Research (INFORMATIONAL — not requirements) sources: MakerWorld, Thingiverse, Printables, GitHub, arXiv, bioRxiv, Reddit, MakerForge, UZH, EPFL created: 2026-03-27 updated: 2026-03-27 validated_links: 2026-03-27 --- # Research Findings: Enhancements & Good-to-Knows for so101-biolab-automation ## Key Finding: No SO-101 Pipette End Effector Exists No community design for a pipette attachment specifically for SO-101. This is an original contribution opportunity. ## Hardware Enhancements (3D-Printed) | Design | Source | Date | Relevance | |--------|--------|------|-----------| | [Parallel Gripper for SO-101](https://makerworld.com/en/models/1549112) | MakerWorld (energiazero.org) | 2025-06 | Direct SO-101 wrist replacement. Adapt jaws for pipette holding. CAD: [GrabCAD](https://grabcad.com/library/standard-open-so-101-arms-with-parallel-gripper-1) | | [TPU Grip for SO-100/101](https://www.thingiverse.com/thing:7153144) | Thingiverse (NekoMaker) | 2025-09 | Compliant fingertips for smooth objects (pipette barrels, plates). | | [Silicone Gripper Tip Mold](https://www.thingiverse.com/thing:7152864) | Thingiverse (NekoMaker) | 2025-09 | Cast silicone tips for delicate labware handling. | | [Reinforced Trigger](https://www.printables.com/model/1323562) | Printables (Chitoku) | 2025-06 | Fixes leader arm trigger breakage. **Apply Loctite 242 to motor 5 screw.** | | [Bambu-Optimized SO-101](https://makerworld.com/en/models/1399268) | MakerWorld (manfromwest) | 2025-05 | Best SO-101 print files for Bambu. Camera mount provisions. | | [AB-SO-BOT](https://github.com/Mr-C4T/AB-SO-BOT) | GitHub (54 stars) | 2025 | Aluminium body upgrade with RealSense mounts. | | [WH148 Leader Arm](https://www.thingiverse.com/thing:7041540) | Thingiverse (xuyuan) | 2025-05 | Cheaper potentiometer-based leader arm. | | [SO-101 Fin-Ray Gripper](https://makerworld.com/en/models/2075813) | MakerWorld (GauravMM) | 2025-12 | Compliant gripper for delicate/oddly-shaped objects. TPU 95A. | | [SO-101 Parallel Gripper](https://github.com/roboninecom/SO-ARM100-101-Parallel-Gripper) | GitHub (Robonine) | 2026-03 | 1.5kg payload, 100.5mm stroke, $76. Open-source STL + code. | | [XLeRobot](https://github.com/Vector-Wangel/XLeRobot) | GitHub ([docs](https://xlerobot.readthedocs.io)) | 2025 | Dual SO-101 on mobile base (LeKiwi). $660. URDFs + ManiSkill sim. 600-1000g payload/arm. | ## Tool Changer Design **Primary source: [Berkeley Passive Tool Changer](https://goldberg.berkeley.edu/pubs/CASE2018-ron-tool-changer-submitted.pdf)** **Repo: [BerkeleyAutomation/RobotToolChanger](https://github.com/BerkeleyAutomation/RobotToolChanger)** (branch: `tool-changer`) **What we reuse directly:** - Design geometry: 10° truncated cone angle, 5N magnets, spring-loaded dowel pins - 3-part architecture: robot-side adapter, tool-side base, parking housing - STL reference: 3 dirs (robot component, tool component, tool housing) — adapt dimensions from ABB YuMi flange to SO-101 wrist flange - Passive locking principle: no power, no data through the tool interface - ArUco marker guidance idea from paper's failure analysis (positioning errors, not mechanism) **What we adapt:** - Flange interface: redesign robot-side adapter for SO-101 motor 5 horn mount (M3 screw pattern, ~25mm diameter) - Tool parking rack: design for 3 tools side-by-side at fixed workspace position - Cable routing: add flat flex cable channel for pipette power (not in Berkeley design) **License note:** Berkeley repo has no stated license. Use paper's design principles and our own STL geometry. ## Open-Source Liquid Handling Projects | Project | URL | License | Date | What We Reuse | |---------|-----|---------|------|---------------| | [digital-pipette-v2](https://github.com/ac-rad/digital-pipette-v2) | GitHub | CC BY 4.0 + MIT | v2.0.0, 2025-11 | **Option A (DIY).** 5 STL parts, Arduino firmware, Python controller, Actuonix L16 actuator, full BOM. | | [FINDUS](https://github.com/FBarthels/FINDUS) | GitHub | GPL-3.0 | 2020 ([paper](https://doi.org/10.1177/2472630319877374)) | Calibration procedure (<0.3% error). Tip-fit validation method. | | [OTTO](https://github.com/DrD-Flo/OTTO) | GitHub | MIT | 2023 | Tip pickup geometry (insertion angle, force). Ejection mechanism. | | [Sidekick](https://github.com/rodolfokeesey/Liquid-Handler) | GitHub | CERN-OHL-S v2 | 2022 ([paper](https://doi.org/10.1016/j.ohx.2022.e00319)) | SBS plate addressing geometry. 10µL solenoid pump alternative. | | [GormleyLab](https://github.com/GormleyLab/Pipette-Liquid-Handler) | GitHub | MIT | 2025 | Bluetooth Integra pipette Python interface (`liquidhandler.py`). | | [MULA](https://www.printables.com/model/1019242) | Printables | open | 2024 ([paper](https://doi.org/10.1016/j.ohx.2024.e00075)) | Gastight syringe mechanism. CAD on [Mendeley](https://data.mendeley.com/datasets/3m3t4f9ft3). | | [OLA](https://docs.openlabautomata.xyz/) | Website | open | active | Community hub. Protocol sharing. | | [PHIL](https://github.com/CSDGroup/PHIL) | GitHub | MIT | 2022 ([Nature Comms](https://www.nature.com/articles/s41467-022-30643-7)) | ETH Zurich. Personal pipetting robot. Tip management, media exchange, live-cell compatible. | | [PyLabRobot](https://github.com/PyLabRobot/pylabrobot) | GitHub | Apache-2.0 | 2023 ([bioRxiv](https://www.biorxiv.org/content/10.1101/2023.07.10.547733)) | MIT Media Lab. Hardware-agnostic liquid handling. Already in our stack. Write SO-101 backend driver. | | [OptoBot](https://github.com/nicolaegues/OptoBot) | GitHub | — | 2024 | OT-2 + Python for automated experimental optimization loops. Closed-loop observe→decide→pipette pattern. | | [OT-2 Plate Handler](https://doi.org/10.26434/chemrxiv-2025-n95kk) | ChemRxiv | — | 2025 (Bolt et al.) | 3D-printed robotic claw for Opentrons OT-2. Plate gripping geometry + positioning tolerances reusable. | ## Bimanual SO-101 Support **[AIDASLab/lerobot-so101-bimanual](https://github.com/AIDASLab/lerobot-so101-bimanual)** (2026-01, 5 stars) — Drop-in LeRobot fork with `bi_so101_follower`/`bi_so101_leader` types. Calibration, teleoperation, recording all work dual-arm. **Use this instead of patching upstream.** ## Commercial References ### Andrew+ (Waters/Andrew Alliance) — [UZH page](https://www.zmb.uzh.ch/en/Available-Systems/SamplePreparationInstruments/Andrew-%2C-The-Pipetting-Robot.html) - Cartesian gantry + interchangeable electronic pipettes (single/8-channel) - Modular "Domino" snap-in accessories (heater/shaker, cooled racks, tip racks) - OneLab cloud protocol software (drag-and-drop) - Lessons: modular workspace with fixed slot positions; separate pipettes by volume range; 8-channel head for 96-well efficiency ### Sartorius Electronic Pipettes — [product page](https://www.sartorius.com/en/products/pipetting/electronic-pipettes) - Picus 2: single/multi-channel, Bluetooth, programmable protocols - Future integration target for SO-101 (see Pipette Strategy section) ## Community Resources - [MakerForge SO-101 guide](https://www.makerforge.tech/posts/seeed-so101/) — servo calibration, port detection, teleoperation setup - [Seeed Studio SO-101 wiki](https://wiki.seeedstudio.com/lerobot_so100m/) — reComputer Jetson + SO-101 kits - [PyLabRobot 2025 roadmap](http://discuss.pylabrobot.org/t/plr-dev-roadmap-2025-q1-q2/143) — extending to robotic arms (directly relevant) ## Known Hardware Issues - **Gripper screw loosening** (motor 5) — apply Loctite 242/243 before long runs - **USB disconnect during teleoperation** ([lerobot #3131](https://github.com/huggingface/lerobot/issues/3131)) — implement reconnect watchdog - **Pipetting motion** should be decomposed into discrete phases for training (approach → insert tip → aspirate → move → dispense → eject) - **50+ episodes minimum** per task, deliberate slow motions, 80k+ training steps ## Academic Papers (Most Actionable) **What we reuse from papers:** - **AutoBio**: open-source eval harness at `autobio-bench/AutoBio` — validate our policies against standard bio-lab tasks - **DexMimicGen**: NVIDIA data amplification — 20 real demos → 2000 synthetic for ACT training - **ArticuBot**: pre-trained fridge/door policy — zero-shot transfer for UC2, skip real fridge demos - **InterACT/MoE-ACT**: replace vanilla ACT with inter-arm attention for bimanual pipetting - **Tool-as-Interface**: collect pipetting demos from human video instead of robot teleoperation - **Echo**: open-source force-feedback teleoperation — improve demo quality for tip press-fit - **RoboCopilot**: human-in-the-loop correction loop — lab tech intervenes, corrections become training data - **AIDASLab bimanual fork**: drop-in dual SO-101 support — `bi_so101_follower`/`bi_so101_leader` types ### ETH / EPFL Lab Automation (Swiss) | Paper | Year | Institution | Key Finding | |-------|------|-------------|-------------| | [PHIL — Pipetting Helper Imaging Lid](https://www.nature.com/articles/s41467-022-30643-7) | 2022 | ETH Zurich (Dettinger et al., CSD Group) | Open-source personal pipetting robot. MIT license. 3D-printed + commercial parts. Live-cell incubation compatible. **Repo: [CSDGroup/PHIL](https://github.com/CSDGroup/PHIL)**. Reuse: tip management, media exchange protocols, compact form factor. | | [SIMO — Microplate Handling via Vision + Touch](https://doi.org/10.3389/frobt.2024.1462717) | 2025 | EPFL (Scamarcio, Tan, Hughes, Stellacci) | Bi-manual mobile robot (ABB GoFa) with vision + tactile feedback for microplate handling. **1.2mm accuracy, ≥95% success rate.** Reuse: 3-stage localization (SLAM → fiducial → tactile), impedance control for plate grasping. Sets accuracy benchmark for our SO-101. | ### General Lab Automation | Paper | Year | Key Finding | |-------|------|-------------| | [RoboCulture](https://arxiv.org/abs/2505.14941) | 2025 | Complete robotics platform for bio experiments (pipetting, plate handling). Aspuru-Guzik group (Toronto). | | [AutoBio](https://arxiv.org/abs/2505.14030) | 2025 | Simulation benchmark for bio-lab tasks. Open-source eval harness at `autobio-bench/AutoBio`. | | [MatteriX](https://arxiv.org/abs/2601.13232) | 2026 | Digital twin for chemistry lab. Sim-to-real for pipetting without wasting reagents. | | [Microplate Handling Accuracy](https://www.biorxiv.org/content/10.1101/2023.12.29.573685v1) | 2024 | Quantifies positioning tolerance for 96-well plate grasping. Sets engineering targets. | | [Open Liquid Handler](https://www.biorxiv.org/content/10.64898/2026.03.02.709168v1) | 2026 | Industry-grade open-source liquid handler. PyLabRobot-compatible (same author). | | [Keyframe-Guided Rewards](https://arxiv.org/abs/2603.00719) | 2026 | RL reward shaping for long-horizon lab tasks. Aligns with ACT chunked actions. | | [BioMARS](https://arxiv.org/abs/2507.01485) | 2025 | Multi-agent robot system for autonomous bio experiments. Dual-arm coordination patterns. | | [Dual Demonstration for Chemical Automation](https://arxiv.org/abs/2506.11384) | 2025 | Teaches both end-effector AND jig operations simultaneously. Tool-change + substrate-handling. | | [Cutting the Cord](https://arxiv.org/abs/2603.09051) | 2026 | GPU-accelerated bimanual mobile manipulation on LeRobot/XLeRobot. Untethered deployment. | ### Imitation Learning (ACT & Bimanual) | Paper | Year | Key Finding | |-------|------|-------------| | [ALOHA/ACT](https://arxiv.org/abs/2304.13705) | 2023 | Our training framework. 50 demos/task, low-cost bimanual. | | [LeRobot](https://arxiv.org/abs/2602.22818) | 2026 | Our stack's authoritative reference. ICLR 2026. | | [ALOHA Unleashed](https://arxiv.org/abs/2410.13126) | 2024 | More data > bigger model. DeepMind validation of ACT ceiling. | | [InterACT](https://arxiv.org/abs/2409.07914) | 2024 | Hierarchical attention for inter-arm dependencies. Better than vanilla ACT for bimanual. | | [MoE-ACT](https://arxiv.org/abs/2603.15265) | 2026 | One multi-task model via MoE. Handles 5+ tasks without per-task training. | | [DexMimicGen](https://arxiv.org/abs/2410.24185) | 2024 | 20 demos → 2000 synthetic. NVIDIA open-source. Solves data scarcity. | | [X-IL](https://arxiv.org/abs/2502.12330) | 2025 | Systematic policy architecture comparison. Guide for ACT alternatives. | | [Bi-ACT](https://arxiv.org/abs/2401.17698) | 2024 | Bilateral force-feedback ACT. Improves demo quality for contact-rich tasks (tip press-fit). | | [FTACT](https://arxiv.org/abs/2509.23112) | 2025 | Force-torque aware ACT. F/T sensor in observation space for contact-aware policies. | | [Bimanual ACT with Inter-Arm Coordination](https://arxiv.org/abs/2503.13916) | 2025 | ACT + inter-arm coordination module. Precise bimanual synchronization (AIST Japan). | ### Tool Use & Door Opening | Paper | Year | Key Finding | |-------|------|-------------| | [Tool-as-Interface](https://arxiv.org/abs/2504.04612) | 2025 | Learn from human tool-use video, not robot demos. Reduces data cost. | | [FUNCTO](https://arxiv.org/abs/2502.11744) | 2025 | One-shot tool manipulation. Generalizes across tool variants. | | [ArticuBot](https://arxiv.org/abs/2503.03045) | 2025 | Universal door/fridge policy trained in sim. Zero-shot real transfer. | | [UniDoorManip](https://arxiv.org/abs/2403.02604) | 2024 | Universal door policy (pull, push, sliding). Covers lab equipment. | ### Data Collection | Paper | Year | Key Finding | |-------|------|-------------| | [YOTO](https://arxiv.org/abs/2501.14208) | 2025 | One-shot bimanual from video. Near-zero demo cost. | | [Echo](https://arxiv.org/abs/2504.07939) | 2025 | Force-feedback teleoperation. Better demos for press-fit tasks. Open-source. | | [RoboCopilot](https://arxiv.org/abs/2503.07771) | 2025 | Human-in-the-loop correction during execution. Interventions become training data. | | [GELLO](https://arxiv.org/abs/2309.13037) | 2023 | Low-cost replica-arm teleoperation (UC Berkeley). Alternative to leader-follower. | | [Taming VR Teleoperation](https://arxiv.org/abs/2508.14542) | 2025 | VR-based multi-task bimanual demo collection. Less operator fatigue than leader arm. | | [Opening Articulated Structures](https://arxiv.org/abs/2402.17767) | 2024 | Real-world door/fridge opening without pre-mapped kinematics (UIUC, Saurabh Gupta). | ## Cross-Cutting Insights 1. **Data > model complexity** — 50+ demos per task, amplify with DexMimicGen 2. **Force feedback is the main gap** — SO-101 has no wrist F/T sensor; biggest reliability risk for pipette tip press-fit 3. **Sim-to-real works for fridge/door** — ArticuBot demonstrates zero-shot transfer; may not need real demos for UC2 4. **Decompose pipetting into phases** — approach, insert tip, aspirate, dispense, eject. Train per-phase or use keyframe rewards. 5. **Bimanual needs inter-arm attention** — vanilla ACT struggles with coupled coordination; use InterACT or MoE-ACT 6. **PyLabRobot backend driver** — writing one for SO-101 makes all existing protocols work on our arms 7. **8-channel pipette head** — 8x efficiency for 96-well work; design as a specialized end-effector ## Slicer CLI & Parametric CAD Research **Problem:** CadQuery generates geometrically correct STLs but has no FDM awareness. Vertical empty rows, unsupported overhangs, and gravity failures are invisible until a print fails. ### OpenSCAD CLI Mature, stable parametric CAD. `.scad` -> STL/SVG/DXF/PNG via CLI. ```bash openscad -o out.stl input.scad # export STL openscad -o out.svg --projection=ortho input.scad # export SVG openscad -D 'length=100' -o out.stl input.scad # parametric override openscad -p params.json -P set_name -o out.stl input.scad # parameter file ``` - Install: `apt install openscad`, snap, flatpak, or AppImage - CSG maps 1:1 from CadQuery: `box`->`cube`, `cylinder`->`cylinder`, `cut`->`difference()`, `revolve`->`rotate_extrude()` - No printability validation (must pair with slicer) ### Slicer CLI Comparison | Tool | CLI Maturity | Headless Linux | Overhang Detection | Notes | |------|-------------|----------------|-------------------|-------| | **PrusaSlicer** | Mature | Yes | Yes | `prusa-slicer --export-gcode model.stl --load config.ini`. Most stable. [Wiki](https://github.com/prusa3d/PrusaSlicer/wiki/Command-Line-Interface) | | **Bambu Studio** | Modern | Yes (Docker avail) | Yes | `--slice`, `--orient`, `--arrange`, JSON config. Best error codes. [Wiki](https://github.com/bambulab/BambuStudio/wiki/Command-Line-Usage) | | **SuperSlicer** | Mature | Yes | Enhanced | PrusaSlicer fork with better overhang detection. [Docs](https://docs.superslicer.org/advanced-usage-guides/cmd-line-guide/) | | **CuraEngine** | Stable | Yes | Limited | Pure engine, no GUI deps. JSON settings. [GitHub](https://github.com/Ultimaker/CuraEngine) | | **OrcaSlicer** | **Broken** | **No** (GTK3 crashes) | Yes (when it works) | CLI shares GUI code, segfaults headless. [#2714](https://github.com/OrcaSlicer/OrcaSlicer/issues/2714), [#12277](https://github.com/OrcaSlicer/OrcaSlicer/issues/12277) | **OrcaSlicer CLI root cause:** wxWidgets 3.1.7 (GTK3 backend) initializes display context even in CLI mode. Stack: `libslic3r` (core, decoupled) → `libslic3r_gui` (wxWidgets) → single binary. Fix paths: guard GTK3 init behind `DISPLAY` check, or build headless binary from `libslic3r` only. Fork lineage: Slic3r → PrusaSlicer → Bambu Studio → OrcaSlicer (all share `libslic3r` core). **Recommendation:** PrusaSlicer CLI for validation (stable, proven). Keep slicer-agnostic so Bambu Studio or SuperSlicer can substitute. ### Closed-Loop 3D Printing #### Human Loop (voice/text → print) ```text Human (voice/text) → LLM → OpenSCAD → STL → Slicer → Print ↑ | └── human inspects, describes fix ──────────┘ ``` Human describes a part or modification in natural language (via CC `/voice` or text). LLM generates OpenSCAD, slicer validates, human inspects result and iterates. #### Agent Loop (goal → autonomous design-print-inspect) ```text Goal/Task spec ──→ Agent ──→ OpenSCAD → STL → Slicer validate ↑ | | Printer API (MQTT) | ↓ | Camera (RTSP/JPEG) | ↓ └── VLM analyzes print ←───────┘ Agent adjusts design/params ``` Agent receives a goal (e.g., "print a plate holder fitting SBS 127.76x85.48mm with 0.5mm clearance, PLA+, must pass printability check"). Autonomously: 1. Generates OpenSCAD from spec 2. Validates via slicer CLI 3. Sends GCode to printer via MQTT API 4. Monitors print via camera feed (RTSP/JPEG) 5. VLM (vision-language model) analyzes camera frames for defects 6. On failure: agent adjusts design/params, re-slices, reprints **Bambu camera + printer API (enables agent feedback):** - Built-in cameras: X1 (RTSP), A1/P1 (JPEG frames) - Local MQTT: `:8883`, topic `device//report` - Python: [`bambu-connect`](https://pypi.org/project/bambu-connect/), [`bambulabs-api`](https://pypi.org/project/bambulabs-api/) - Cloud API: [`Bambu-Lab-Cloud-API`](https://github.com/coelacant1/Bambu-Lab-Cloud-API) (unofficial) **Camera-based failure detection:** | Tool | How | Printers | |------|-----|----------| | [Obico](https://github.com/TheSpaghettiDetective/obico-server) | Vision model, 30-60s intervals, auto-pause | OctoPrint, Klipper, Bambu | | [OctoEverywhere Gadget](https://octoeverywhere.com/gadget) | Neural net trained on millions of prints | OctoPrint | | [PrintWatch](https://plugins.octoprint.org/plugins/printwatch/) | AI video feed, auto-pause | OctoPrint | **LLM/VLM agent loops for 3D printing:** | Project | Architecture | Result | |---------|-------------|--------| | [LLM-3D Print](https://arxiv.org/abs/2408.14307) | Multi-agent: supervisor + VLM image reasoning + planner + executor → camera → corrects → reprints | 5x structural integrity, expert-level error ID | | [Build Great AI](https://zenml.io/llmops-database/llm-powered-3d-model-generation-for-3d-printing) | Text → LLaMA/GPT/Claude → OpenSCAD → STL | Hours to minutes for design | | [CADialogue](https://www.sciencedirect.com/science/article/abs/pii/S0010448525001678) | Multimodal LLM: text + speech + images → parametric CAD | Conversational CAD | **Related projects (LLM + CAD/print):** | Project | What | Relation to us | |---------|------|---------------| | [ClaudeCAD](https://github.com/niklasmh/ClaudeCAD) | Claude + voice + web UI → STL download | Similar voice→CAD idea, web-only, no slicing or print control | | [claude-3d-playground](https://github.com/ivanearisty/claude-3d-playground) | Claude Code for design, validate, slice | Closest competitor; no voice, no agent loop, no camera inspect | | [openscad-agent](https://github.com/iancanderson/openscad-agent) | Claude Code agent for OpenSCAD modeling | Single-tool agent; no slicer, no print, no feedback loop | | [CQAsk](https://github.com/OpenOrion/CQAsk) | LLM + CadQuery → STL/STEP with UI | Uses CadQuery not OpenSCAD; no print pipeline | | [ScadLM](https://github.com/KrishKrosh/ScadLM) | Agentic AI + OpenSCAD generation | Early stage; no slicer validation or print control | | [Text2CAD](https://github.com/SadilKhan/Text2CAD) | NeurIPS 2024: text → parametric CAD operations | Academic; 170K models dataset; no print integration | | [text-2-cad](https://github.com/roberto-ceraolo/text-2-cad) | OpenAI + RAG over OpenSCAD docs → .scad | Early; similar LLM+OpenSCAD approach, minimal | | [Speech-to-Reality](https://arxiv.org/html/2409.18390) | Voice → 3D generative AI → robotic assembly | Research; broader scope includes assembly, not printing | **MCP servers (integration layer):** | Server | What | Use for us | |--------|------|-----------| | [OctoEverywhere MCP](https://github.com/OctoEverywhere/mcp) | Printer control, camera, AI failure detection (OctoPrint/Klipper/Bambu) | Agent loop: send jobs, read camera, pause on failure | | [OpenSCAD MCP](https://www.pulsemcp.com/servers/jhacksman-openscad) | Generate 3D models from text/images via OpenSCAD | Alternative to direct CLI; Claude Desktop integration | | [CADQuery MCP](https://github.com/rishigundakaram/cadquery-mcp-server) | CAD generation + STL/STEP export + SVG feedback | CadQuery path if needed | **Key insight:** No project integrates all layers (voice → design → validate → print → camera+VLM inspect → agent fix). The MCP ecosystem provides the building blocks. ### SO-101 Simulation & Digital Twin **Goal:** Catch hardware problems in software. Shift-left testing for robotics. **Official SO-101 sim models:** URDF + MJCF at [TheRobotStudio/SO-ARM100/Simulation/SO101](https://github.com/TheRobotStudio/SO-ARM100/tree/main/Simulation/SO101). Includes `so101_new_calib.urdf`, `so101_new_calib.xml` (MuJoCo), `scene.xml`, joint properties, mesh assets. Generated via onshape-to-robot. **Simulation stacks:** | Stack | Physics | GPU | SO-101 Tasks | Headless CI | Use for | |-------|---------|-----|-------------|------------|---------| | [MuJoCo](https://mujoco.readthedocs.io/) direct | MuJoCo 3.0+ | No | Via URDF/MJCF | Yes (`MUJOCO_GL=osmesa`) | CI tests, lightweight sim, control dev | | [LeIsaac](https://github.com/LightwheelAI/leisaac) | Isaac Lab | Yes | `PickOrange`, `LiftCube`, `CleanToyTable`, `FoldCloth-BiArm` | Cloud (NVIDIA Brev) | Teleoperation, data collection, policy training | | [ManiSkill](https://github.com/haosulab/ManiSkill) | SAPIEN | Yes | Via custom robot loading | Limited | RL training, GPU-parallelized (200k+ FPS) | | [gym_hil](https://github.com/huggingface/gym-hil) | MuJoCo | No | Panda only (extensible) | Yes | Human-in-the-loop RL | **LeIsaac details:** - Official LeRobot EnvHub integration by [LightwheelAI](https://lightwheelai.github.io/leisaac/) - SO101 follower + leader teleoperation in sim - HDF5 → LeRobot dataset conversion built-in - Cloud sim: no local GPU required via NVIDIA Brev - Usage: `make_env("LightwheelAI/leisaac_env:envs/so101_pick_orange.py", n_envs=1, trust_remote_code=True)` **MuJoCo in CI (headless GitHub Actions):** - Set `MUJOCO_GL=osmesa` for software rendering (no GPU, no X11) - Install: `sudo apt-get install libosmesa6-dev` - Proven pattern: MuJoCo official CI, openai/mujoco-py, community SO-100 sim **OpenSCAD STL → MuJoCo collision mesh:** - Export binary STL from OpenSCAD → reference in MJCF `` tag - MuJoCo auto-converts to convex hull for collision (sufficient for simple parts) - Works for plate holders, tool changer cones, dock — may need simplification for complex geometry **Sim-to-real for SO-101:** - [lerobot-sim2real](https://github.com/StoneT2000/lerobot-sim2real): train in ManiSkill, deploy zero-shot to real SO-101 - GR00T-N1.5 policy fine-tuning on SO-101 via LeIsaac sim data - Domain randomization (colors, textures, dynamics) for robust transfer - Community SO-100 MuJoCo: [lachlanhurst/so100-mujoco-sim](https://github.com/lachlanhurst/so100-mujoco-sim) with Qt GUI + LeRobot sync **Digital twin for 3D print inspection:** - Real-time layer-by-layer CNN inspection for over/under-extrusion ([MDPI 2025](https://www.mdpi.com/2075-1702/13/6/448)) - Zero-shot multi-criteria inspection via digital twin ([arXiv 2511.23214](https://arxiv.org/abs/2511.23214)) - 3D Gaussian Splatting for photorealistic rendering of printed parts - [Systematic review: Digital Twins in 3D Printing](https://arxiv.org/html/2409.00877v1) **Recommendation:** 1. **CI testing now:** MuJoCo + OSMesa (no GPU, headless, free) 2. **Policy training later:** LeIsaac when GPU available (or NVIDIA Brev cloud) 3. **Print inspection future:** Prototype with real camera + simulated geometry comparison ### XLeRobot Reference [XLeRobot](https://github.com/Vector-Wangel/XLeRobot) — dual SO-101 mobile platform ($660, <4h assembly). - Uses STEP + 3MF workflow (no code-generated CAD) - Distributes STEP files for modification, STL/3MF for printing - Addresses gravity/support via documentation + manual slicer orientation - Z-axis scaling in slicer for fit adjustments (not in CAD) - No automated printability checking - Proves slicer-based workflow is viable for SO-101 ecosystem ### Validation Pipeline (planned) ```text OpenSCAD (.scad) ──→ STL ──→ PrusaSlicer CLI ──→ printability report ``` - OpenSCAD: parametric generator (reliable CLI, SVG via projection) - PrusaSlicer: printability validator (optional, graceful fallback if unavailable) - CadQuery: legacy reference in `hardware/cad/` (no longer used for generation) ## STL Files Plan Add draft STL files to `hardware/stl/` for custom parts. Mark as experimental — these are starting points for iteration once hardware arrives. | File | Status | Source/Approach | |------|--------|----------------| | `hardware/stl/README.md` | NEW | Index of all STL files with status (experimental/validated), print settings, notes | | `hardware/stl/pipette_mount_so101.stl` | EXPERIMENTAL | Adapt SO-101 wrist flange → digital-pipette-v2 body clamp. Reference: [digital-pipette-v2 STL](https://github.com/ac-rad/digital-pipette-v2/tree/main/stl) for pipette body dimensions. | | `hardware/stl/tool_dock_3station.stl` | EXPERIMENTAL | 3 parking slots side-by-side. Berkeley cone geometry (10° angle). Magnets for retention. Reference: [BerkeleyAutomation/RobotToolChanger](https://github.com/BerkeleyAutomation/RobotToolChanger/tree/tool-changer). | | `hardware/stl/tool_cone_robot.stl` | EXPERIMENTAL | Female cone adapter for SO-101 wrist (motor 5 horn mount, M3 pattern). Berkeley design. | | `hardware/stl/tool_cone_pipette.stl` | EXPERIMENTAL | Male cone base for pipette tool. Mates with robot-side cone. | | `hardware/stl/tool_cone_gripper.stl` | EXPERIMENTAL | Male cone base for stock gripper. | | `hardware/stl/tool_cone_hook.stl` | EXPERIMENTAL | Male cone base for fridge hook. | | `hardware/stl/fridge_hook_tool.stl` | EXPERIMENTAL | Hook end-effector for fridge door handle. | | `hardware/stl/96well_plate_holder.stl` | EXPERIMENTAL | SBS footprint (127.76 x 85.48 mm) with alignment pins. Reference: [Microplate Handling Accuracy paper](https://www.biorxiv.org/content/10.1101/2023.12.29.573685v1) for tolerance targets. | | `hardware/stl/tip_rack_holder.stl` | EXPERIMENTAL | Holds standard pipette tip rack at fixed workspace position. | | `hardware/stl/gripper_tips_tpu.stl` | EXPERIMENTAL | Compliant fingertips. Reference: [NekoMaker TPU grip](https://www.thingiverse.com/thing:7153144). Print in TPU 95A. | **Print settings:** PLA+ default, 0.4mm nozzle, 0.2mm layer, 15% infill. TPU 95A for gripper tips only. **Note:** STL files generated programmatically (OpenSCAD or CadQuery) are preferred over manual CAD — reproducible, parametric, version-controlled. If using FreeCAD/Fusion360, commit the source file alongside the STL. ## Target Application: PCR Master Mix Preparation **First real use case:** Automated PCR plate setup. - **Accuracy target:** ±1-3 µL per well - **Master mix:** 1.1x overage (e.g., for 96 reactions: prepare 106 reactions worth) - **Typical volumes:** 20-25 µL total per well (15-20 µL master mix + 2-5 µL template) - **Workflow:** Prepare 1.1x master mix in trough → distribute to 96-well PCR plate → add template per well → seal plate **PCR plate setup sequence (UC1 extension):** 1. Arm A equips pipette, aspirates master mix from trough 2. Dispense 20 µL per well across plate (uc1_full_plate or uc1_row) 3. Arm A or B adds template DNA per well (different source per well or strip tube) 4. Arm swaps to gripper, moves plate to thermocycler (fridge-like door operation) **Why ±1-3 µL is achievable:** - [FINDUS](https://github.com/FBarthels/FINDUS) achieves <0.3% error with 3D-printed mechanics - Digital-pipette-v2 uses Actuonix L16 linear actuator (0.05mm resolution over 50mm stroke) - At 20 µL target: ±3 µL = ±15% tolerance — well within DIY capability - At 200 µL master mix aliquots: ±3 µL = ±1.5% — easily achievable ## Pipette Strategy: DIY → Electronic → Autonomous **Not locked to one pipette type.** The architecture supports multiple pipette backends: | Option | Type | Control | Cost | Status | |--------|------|---------|------|--------| | [digital-pipette-v2](https://github.com/ac-rad/digital-pipette-v2) | DIY syringe | Arduino serial | ~$95 | Available now (v2.0.0, 2025-11) | | [Sartorius Picus 2](https://www.sartorius.com/en/products/pipetting/electronic-pipettes) | Commercial electronic | Bluetooth/serial | ~$800-2000 | Future — see [GormleyLab Python interface](https://github.com/GormleyLab/Pipette-Liquid-Handler) | | [Integra VIAFLO](https://www.integra-biosciences.com/global/en/electronic-pipettes) | Commercial electronic | Bluetooth | ~$1000-3000 | Future — 8/12-channel for 96-well efficiency | | Custom 8-channel head | DIY multi-channel | Arduino/stepper | ~$200 | Future — 8x efficiency for 96-well | **`src/biolab/pipette.py` is already abstracted** — `aspirate(volume)`, `dispense(volume)`, `eject_tip()`. Adding a Sartorius/Integra backend is a new class implementing the same interface. ## Future Vision: VLM + Embodied AI for Hands-Off Autonomous Operation ### Operation Modes (Progressive) ```text Phase 1 (current) → Stub mode, coordinate commands, remote dashboard Phase 2 (hardware) → Teleoperation + ACT policy training, in-situ operator Phase 3 (remote) → Remote-controlled via dashboard, human oversight Phase 4 (vision) → Wrist camera visual servoing, closed-loop correction Phase 5 (autonomous) → VLM task planning + embodied execution, long-running unattended ``` ### Why VLM + Embodied AI Is Necessary for Phase 4-5 - **Sub-mm alignment**: SO-101 open-loop repeatability (~1-2mm) insufficient for tip press-fit. Visual servoing closes the gap. - **Meniscus detection**: Liquid level tracking during aspiration requires real-time vision. - **Drift compensation**: Plate/rack positions shift during long runs. Can't rely on fixed coordinates. - **Tool verification**: Confirm tip attached, tool engaged, plate seated — visual confirmation. - **Task planning**: "Pipette row A with 25µL from reagent in fridge" → decompose into sub-tasks autonomously. - **Error recovery**: Detect dropped tip, missed well, spill — re-plan and retry without human. ### Key Technologies | Technology | Purpose | Reference | |------------|---------|-----------| | Real-time VLM | Task planning from natural language + visual scene understanding | [OpenVLA](https://arxiv.org/abs/2406.09246) (2024), [RT-2](https://arxiv.org/abs/2307.15818) (2023) | | Embodied AI policy | End-to-end visuomotor control (camera → joint actions) | ACT/LeRobot (in stack), [π0](https://arxiv.org/abs/2410.24164) (Physical Intelligence, 2024) | | Visual servoing | Sub-mm tip/well alignment from wrist camera | OpenCV + ArUco, or learned visual features | | Force estimation | Tip press-fit without F/T sensor | [Bi-ACT](https://arxiv.org/abs/2401.17698), [FTACT](https://arxiv.org/abs/2509.23112) | | Human-in-the-loop | Remote operator corrects policy; corrections become training data | [RoboCopilot](https://arxiv.org/abs/2503.07771) (2025) | | Sim-to-real | Train in simulation, deploy zero-shot | [AutoBio](https://arxiv.org/abs/2505.14030), [MatteriX](https://arxiv.org/abs/2601.13232), [LeIsaac](https://huggingface.co/docs/lerobot/envhub_leisaac) | | Data amplification | 20 real demos → 2000 synthetic | [DexMimicGen](https://arxiv.org/abs/2410.24185) (NVIDIA, open-source) | ### Long-Running Autonomous Execution For unattended multi-hour runs (e.g., 96-well plate with replicates, overnight incubation cycles): - **Watchdog**: SafetyMonitor already parks arms on heartbeat timeout (5s) - **Remote dashboard**: WebSocket commands for pause/resume/e-stop from anywhere - **State persistence**: Log every aspirate/dispense to a run journal (well, volume, timestamp) - **Error budget**: Define acceptable failure rate (e.g., ≤2 missed wells per plate) before auto-abort - **Checkpoint/resume**: If interrupted, resume from last successful well (journal-based) ### Hardware Additions for Phase 4-5 - High-res wrist cameras (in BOM: 32x32mm UVC modules) - Overhead camera with ArUco workspace calibration - Optional: depth camera ([RealSense D405 mount](https://github.com/TheRobotStudio/SO-ARM100#5-wristmount-cameras)) for 3D tip tracking - Optional: wrist F/T sensor for contact-aware manipulation ## Recommended Next Actions (When Hardware Arrives) 1. Use [AIDASLab bimanual fork](https://github.com/AIDASLab/lerobot-so101-bimanual) for dual-arm setup 2. Apply Loctite 242 to motor 5 screws on all arms 3. Print [reinforced trigger](https://www.printables.com/model/1323562) for leader arm 4. Print [parallel gripper](https://makerworld.com/en/models/1549112) as pipette holder base 5. Port Berkeley tool changer cone to SO-101 wrist dimensions 6. Record 50 episodes per task using decomposed motion phases 7. Train ACT via LeRobot; evaluate on AutoBio benchmark 8. Implement USB watchdog for long-run stability ================================================ FILE: docs/UserStory.md ================================================ --- title: User Stories purpose: Acceptance criteria for UC1-4 (pipetting, fridge, tool change, remote oversight) + developer stories authority: Acceptance criteria (AUTHORITY) created: 2026-03-27 updated: 2026-03-27 --- # User Stories ## Roles - **Lab researcher** — operates the system to automate pipetting and sample handling - **Remote operator** — monitors and controls the system via web dashboard - **Developer** — extends the system with new workflows and hardware support ## Pipetting (UC1) ### US-1.1: Single Well Pipetting > As a **lab researcher**, I want to pipette a specific volume from the reagent trough to a single well, so that I can prepare individual samples. **Acceptance criteria:** - [ ] Specify target well by name (e.g., A1, H12) and volume in µL - [ ] Arm moves to trough, aspirates, moves to well, dispenses - [ ] Pipette fill returns to 0 after dispense - [ ] Invalid well name raises clear error **Command:** `python scripts/run_demo.py --use-case uc1_single --well A1 --volume 50` ### US-1.2: Row/Column Pipetting > As a **lab researcher**, I want to pipette an entire row or column in one command, so that I can prepare a set of samples without repeating commands. **Acceptance criteria:** - [ ] Specify row (A-H) or column (1-12) and volume per well - [ ] All wells in the row/column receive the specified volume - [ ] Each well gets an independent aspirate→dispense cycle - [ ] Invalid row/column raises clear error **Commands:** - `python scripts/run_demo.py --use-case uc1_row --row A --volume 25` - `python scripts/run_demo.py --use-case uc1_col --col 1 --volume 20` ### US-1.3: Full Plate Pipetting > As a **lab researcher**, I want to pipette all 96 wells of a plate in one command, so that I can prepare a full experiment plate hands-free. **Acceptance criteria:** - [ ] All 96 wells (A1 through H12) receive the specified volume - [ ] Pipette never overflows (each well is an independent aspirate→dispense) - [ ] Sequence follows row-major order (A1, A2, ..., H12) **Command:** `python scripts/run_demo.py --use-case uc1_full --volume 20` ## Fridge Operations (UC2) ### US-2.1: Retrieve Item from Fridge > As a **lab researcher**, I want the arm to open a fridge, grab a reagent plate, and move it to the workspace, so that I can access cold-stored samples without manual intervention. **Acceptance criteria:** - [ ] Arm equips fridge hook tool - [ ] Hook engages fridge door handle, pulls open - [ ] Arm swaps to gripper tool - [ ] Gripper grabs item from fridge shelf - [ ] Arm moves item to park (safe workspace) position **Command:** `python scripts/run_demo.py --use-case uc2` ## Tool Interchange (UC3) ### US-3.1: Swap Tools Autonomously > As a **lab researcher**, I want the arm to change its end-effector without manual intervention, so that it can switch between pipetting and sample transport tasks. **Acceptance criteria:** - [ ] Arm returns current tool to magnetic dock station - [ ] Arm picks up target tool from dock - [ ] Supports 3 tools: pipette, gripper, fridge hook - [ ] Changing to the already-equipped tool is a no-op **Command:** `python scripts/run_demo.py --use-case uc3` ## Remote Oversight (UC4) ### US-4.1: Monitor and Control via Dashboard > As a **remote operator**, I want to view system status and send commands from a web browser, so that I can oversee and intervene in lab operations from anywhere. **Acceptance criteria:** - [ ] Dashboard accessible at `http://localhost:8080` - [ ] Live status: mode, e-stop state, arm connection, arm IDs - [ ] Commands via WebSocket: e-stop, heartbeat, pause, resume, target well, run workflow - [ ] E-stop triggers SafetyMonitor and parks all arms - [ ] Heartbeat resets watchdog timer (5s timeout → auto-park) **Command:** `make serve_dashboard` ### US-4.2: Run Full Demo Remotely > As a **remote operator**, I want to trigger the full demo sequence from the dashboard, so that I can demonstrate all capabilities without CLI access. **Acceptance criteria:** - [ ] "Run Demo" button in dashboard triggers UC4 (all use cases) - [ ] Workflow runs in background (dashboard stays responsive) - [ ] Status endpoint reflects "running" mode during execution ## Developer Stories ### US-5.1: Extend with New Workflow > As a **developer**, I want to add a new use case by composing existing modules, so that I can automate new lab tasks without modifying core modules. **Acceptance criteria:** - [ ] New workflow composes existing modules via `create_workflow_context()` - [ ] No modifications to core modules (`arms.py`, `pipette.py`, `plate.py`, etc.) - [ ] New workflow has corresponding tests in `tests/` See [docs/architecture.md](architecture.md) for module API and composition patterns. ### US-5.2: Run All Tests Without Hardware > As a **developer**, I want all tests to pass without physical hardware attached, so that I can develop and CI/CD in any environment. **Acceptance criteria:** - [ ] `uv run pytest` passes on any machine with Python 3.10+ - [ ] No `@pytest.mark.hardware` tests run by default - [ ] Stub mode automatically activates when lerobot/pyserial/cv2 unavailable **Command:** `make validate` ================================================ FILE: docs/hardware/BOM.md ================================================ --- title: Bill of Materials purpose: Hardware shopping list with first-party vendor links and cost estimates authority: Hardware (AUTHORITY) created: 2026-03-27 updated: 2026-03-27 validated_links: 2026-03-27 --- # Bill of Materials Hardware shopping list for the so101-biolab-automation prototype. Prices are estimates as of March 2026. Servo specs and gear ratios per [SO-ARM100 README](https://github.com/TheRobotStudio/SO-ARM100#parts-for-two-arms-follower-and-leader-setup). Leader arm motor mapping per [LeRobot SO-101 docs](https://huggingface.co/docs/lerobot/so101#step-by-step-assembly-instructions). ## Arms (2x follower + 1x leader) | Part | Qty | ~Cost | Source | |------|-----|-------|--------| | STS3215 Servo 7.4V, 1/345 gear (C001) | 13 | $14 ea | [Feetech STS3215 (Alibaba)](https://www.alibaba.com/product-detail/Feetech-STS3215-30KG-Serial-Bus-Servo_1601097543776.html), [Seeed Studio](https://www.seeedstudio.com/STS3215-30KG-Serial-Servo-p-6340.html) | | STS3215 Servo 7.4V, 1/191 gear (C044) | 2 | $14 ea | Same — leader joints 1, 3 ([gear ratio table](https://huggingface.co/docs/lerobot/so101#step-by-step-assembly-instructions)) | | STS3215 Servo 7.4V, 1/147 gear (C046) | 3 | $14 ea | Same — leader joints 4, 5, 6 | | Waveshare Serial Bus Servo Driver Board | 3 | $11 ea | [Waveshare wiki](https://www.waveshare.com/wiki/Bus_Servo_Adapter) | | USB-C Cable | 3 | $4 ea | Generic | | 5V 4A DC Power Supply (5.5x2.1mm) | 3 | $10 ea | Generic | | Table Clamp | 6 | $3 ea | Generic | | Screwdriver Set (Phillips #0, #1) | 1 | $6 | Generic | **Or buy assembled kits** (per [SO-ARM100 vendor list](https://github.com/TheRobotStudio/SO-ARM100#kits)): - [Seeed Studio](https://www.seeedstudio.com/so-arm100-Standard-Kit-p-6329.html) — international, 3D printed kits - [PartaBot](https://www.partabot.com/) — US, assembled versions - [WowRobo](https://www.wowrobo.com/) — international, assembled versions ## 3D Printed Parts Print settings per [SO-ARM100 printing guide](https://github.com/TheRobotStudio/SO-ARM100#printing-the-parts): PLA+, 0.4mm nozzle, 0.2mm layer, 15% infill, supports >45 degrees. | Part | Source | |------|--------| | Follower arm (all parts, single plate) | [SO-ARM100 STL (follower)](https://github.com/TheRobotStudio/SO-ARM100/tree/main/STL/SO101) | | Leader arm (all parts, single plate) | [SO-ARM100 STL (leader)](https://github.com/TheRobotStudio/SO-ARM100/tree/main/STL/SO101) | **Custom parts (to be designed for this project):** | Part | Planned File | Material | Notes | |------|-------------|----------|-------| | Pipette mount adapter | *TBD* | PLA+ | Adapts gripper to hold digital-pipette-v2 | | Tool dock (3-station) | *TBD* | PLA+ | Magnetic dock, ref: [Berkeley tool changer](https://goldberg.berkeley.edu/pubs/CASE2018-ron-tool-changer-submitted.pdf) | | Fridge hook end-effector | *TBD* | PLA+ | Hook for fridge door handle | | 96-well plate holder | *TBD* | PLA+ | SBS footprint alignment pins | | Tip rack holder | *TBD* | PLA+ | Holds standard pipette tip rack | | Compliant gripper tips | *TBD* | TPU 95A | Ref: [silicone mold (Thingiverse)](https://www.thingiverse.com/thing:7152864), [fin-ray gripper (MakerWorld)](https://makerworld.com/en/models/2075813) | ## Cameras | Part | Qty | ~Cost | Source | |------|-----|-------|--------| | 32x32mm UVC camera module (wrist) | 2 | $15 ea | Per [SO-ARM100 wrist camera options](https://github.com/TheRobotStudio/SO-ARM100#5-wristmount-cameras) | | USB webcam 1080p (overhead) | 1 | $25 | Per [SO-ARM100 overhead mount](https://github.com/TheRobotStudio/SO-ARM100#2-overhead-camera-mount) | ## Digital Pipette Based on [digital-pipette-v2](https://github.com/ac-rad/digital-pipette-v2) ([paper: RSC Digital Discovery 2026](https://pubs.rsc.org/en/content/articlehtml/2026/dd/d5dd00336a)). | Part | Qty | ~Cost | Source | |------|-----|-------|--------| | 1mL syringe (Luer slip) | 5 | $5 | Lab supply | | Linear actuator L16-50-63-6-R (5cm stroke) | 1 | $70 | [Actuonix (manufacturer)](https://www.actuonix.com/l16-50-63-6-r) | | Arduino Nano | 1 | $5 | [Arduino store](https://store.arduino.cc/products/arduino-nano) | | 3D-printed pipette body | 1 | — | [STL + code](https://github.com/ac-rad/digital-pipette-v2) | | Pipette tips (1-200 uL) | 1 box | $15 | Lab supply | ## Edge Compute | Option | Cost | Source | Notes | |--------|------|--------|-------| | Raspberry Pi 5 (8GB) | $80 | [raspberrypi.com](https://www.raspberrypi.com/products/raspberry-pi-5/) | LeRobot inference, no GPU training | | Jetson Orin Nano (8GB) | $250 | [nvidia.com](https://www.nvidia.com/en-us/autonomous-machines/embedded-systems/jetson-orin/) | GPU for on-device ACT policy training | | microSD 64GB+ | $10 | Generic | | | USB hub (4+ ports) | $15 | Generic | 3 motor boards + cameras | LeRobot supports both platforms per [LeRobot docs](https://huggingface.co/docs/lerobot/index). Seeed Studio offers [reComputer Jetson + SO-101 kits](https://wiki.seeedstudio.com/lerobot_so100m/). ## Lab Consumables | Part | Qty | ~Cost | Notes | |------|-----|-------|-------| | 96-well microplate (SBS, flat bottom) | 5 | $20 | [SBS/ANSI standard](https://en.wikipedia.org/wiki/Microplate#Standards): 127.76 x 85.48 mm, 9mm spacing | | Reagent trough (25mL, SBS footprint) | 2 | $10 | Lab supply | | Colored water / food dye | — | $5 | For demo visualization | ## Cost Summary | Config | Estimated Cost | |--------|----------------| | Minimum (1 follower + 1 leader, RPi, no pipette) | ~$350 | | Full prototype (2 followers + 1 leader, RPi, pipette, cameras) | ~$650 | | With Jetson (GPU training on-device) | ~$820 | ================================================ FILE: hardware/parts.json ================================================ [ {"name": "tip_rack_holder", "stl": "tip_rack_holder.stl", "svg": "tip_rack_holder.svg", "cad": "cad/tip_rack_holder.py", "build_func": "build_tip_rack_holder", "scad": "scad/tip_rack_holder.scad", "shape": "box"}, {"name": "gripper_tips", "stl": "gripper_tips_tpu.stl", "svg": "gripper_tips_tpu.svg", "cad": "cad/gripper_tips.py", "build_func": "build_gripper_tip", "scad": "scad/gripper_tips.scad", "shape": "box"}, {"name": "plate_holder", "stl": "96well_plate_holder.stl", "svg": "96well_plate_holder.svg", "cad": "cad/plate_holder.py", "build_func": "build_plate_holder", "scad": "scad/plate_holder.scad", "shape": "box"}, {"name": "fridge_hook", "stl": "fridge_hook_tool.stl", "svg": "fridge_hook_tool.svg", "cad": "cad/fridge_hook.py", "build_func": "build_fridge_hook", "scad": "scad/fridge_hook.scad", "shape": "complex"}, {"name": "tool_dock", "stl": "tool_dock_3station.stl", "svg": "tool_dock_3station.svg", "cad": "cad/tool_dock.py", "build_func": "build_tool_dock", "scad": "scad/tool_dock.scad", "shape": "box"}, {"name": "pipette_mount", "stl": "pipette_mount_so101.stl", "svg": "pipette_mount_so101.svg", "cad": "cad/pipette_mount.py", "build_func": "build_pipette_mount", "scad": "scad/pipette_mount.scad", "shape": "complex"}, {"name": "tool_cone_robot", "stl": "tool_cone_robot.stl", "svg": "tool_cone_robot.svg", "cad": "cad/tool_changer.py", "build_func": "build_robot_cone", "scad": "scad/tool_changer.scad", "scad_args": "-D PART=\"robot\"", "shape": "complex"}, {"name": "tool_cone_pipette", "stl": "tool_cone_pipette.stl", "svg": "tool_cone_pipette.svg", "cad": "cad/tool_changer.py", "build_func": "build_male_cone", "scad": "scad/tool_changer.scad", "scad_args": "-D PART=\"male\"", "shape": "complex"}, {"name": "tool_cone_gripper", "stl": "tool_cone_gripper.stl", "svg": "tool_cone_gripper.svg", "cad": "cad/tool_changer.py", "build_func": "build_male_cone", "scad": "scad/tool_changer.scad", "scad_args": "-D PART=\"male\"", "shape": "complex"}, {"name": "tool_cone_hook", "stl": "tool_cone_hook.stl", "svg": "tool_cone_hook.svg", "cad": "cad/tool_changer.py", "build_func": "build_male_cone", "scad": "scad/tool_changer.scad", "scad_args": "-D PART=\"male\"", "shape": "complex"} ] ================================================ FILE: hardware/render.py ================================================ """Render all parts from hardware/parts.json manifest. Auto-detects backend: CadQuery (preferred) or OpenSCAD (fallback). Runs theme_svgs.py after generation. Usage: python hardware/render.py # auto-detect python hardware/render.py --backend cad # force CadQuery python hardware/render.py --backend scad # force OpenSCAD """ from __future__ import annotations import argparse import importlib.util import json import shutil import subprocess import sys from pathlib import Path HARDWARE_DIR = Path(__file__).resolve().parent MANIFEST = HARDWARE_DIR / "parts.json" STL_DIR = HARDWARE_DIR / "stl" SVG_DIR = HARDWARE_DIR / "svg" def load_manifest() -> list[dict]: return json.loads(MANIFEST.read_text()) def detect_backend() -> str: """Return 'cad' if CadQuery importable, 'scad' if openscad binary found, else error.""" try: import cadquery # noqa: F401 return "cad" except ImportError: pass if shutil.which("openscad"): return "scad" print("ERROR: Neither CadQuery nor OpenSCAD found") print(" Run: make setup_cad (preferred)") print(" Or: make setup_scad (fallback)") sys.exit(1) def _load_module(cad_path: Path): """Import a CadQuery script as a module.""" spec = importlib.util.spec_from_file_location(cad_path.stem, cad_path) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod def render_cad(parts: list[dict]) -> None: """Render all parts via CadQuery — import build functions, export using manifest filenames.""" import cadquery as cq print("--- Rendering via CadQuery") # Cache modules (tool_changer.py is used by multiple parts) _modules: dict[str, object] = {} for part in parts: cad_rel = part["cad"] if cad_rel not in _modules: _modules[cad_rel] = _load_module(HARDWARE_DIR / cad_rel) mod = _modules[cad_rel] build_fn = getattr(mod, part["build_func"]) shape = build_fn() cq.exporters.export(shape, str(STL_DIR / part["stl"])) cq.exporters.export(shape, str(SVG_DIR / part["svg"]), exportType="SVG") print(f" {part['stl']} + {part['svg']}") def render_scad(parts: list[dict]) -> None: """Render all parts via OpenSCAD CLI, then generate SVGs.""" print("--- Rendering via OpenSCAD (fallback)") for part in parts: scad_path = HARDWARE_DIR / part["scad"] stl_path = STL_DIR / part["stl"] args = part.get("scad_args", "").split() cmd = ["openscad", "-o", str(stl_path), *args, str(scad_path)] cmd = [c for c in cmd if c] subprocess.run(cmd, capture_output=True, check=True) print(f" {part['stl']}") # Generate SVGs from STLs (CadQuery's stl_to_svg.py) print("--- SVG wireframe from STLs") stl_to_svg = HARDWARE_DIR / "cad" / "stl_to_svg.py" subprocess.run([sys.executable, str(stl_to_svg), "--all"], check=True) def run_theme() -> None: """Inject dark mode CSS into all SVGs.""" theme_script = HARDWARE_DIR / "cad" / "theme_svgs.py" subprocess.run([sys.executable, str(theme_script)], check=True) def main() -> int: parser = argparse.ArgumentParser(description="Render parts from manifest") parser.add_argument( "--backend", choices=["cad", "scad"], help="Force backend (default: auto-detect)" ) args = parser.parse_args() backend = args.backend or detect_backend() parts = load_manifest() if backend == "cad": render_cad(parts) else: render_scad(parts) run_theme() print(f"=== {len(parts)} parts rendered via {backend} ===") return 0 if __name__ == "__main__": sys.exit(main()) ================================================ FILE: hardware/cad/fridge_hook.py ================================================ """Fridge hook end-effector. Attaches to the male tool cone base. Hook geometry designed for standard lab fridge door handles (~20mm diameter bar). Usage: uv run --group cad python hardware/cad/fridge_hook.py Exports: hardware/stl/fridge_hook_tool.stl hardware/svg/fridge_hook_tool.svg """ from pathlib import Path import cadquery as cq # --- Parameters (all in mm) --- # Hook geometry HOOK_OPENING = 25.0 # Must fit around door handle (~20mm bar) HOOK_DEPTH = 40.0 # How far the hook reaches HOOK_THICKNESS = 6.0 HOOK_WIDTH = 20.0 # Mounting plate (mates with male tool cone base) MOUNT_DIAMETER = 30.0 # Match tool cone top diameter MOUNT_THICKNESS = 5.0 def build_fridge_hook() -> cq.Workplane: """Build fridge hook end-effector. Returns: CadQuery workplane with hook solid. """ # Mounting plate mount = cq.Workplane("XY").cylinder(MOUNT_THICKNESS, MOUNT_DIAMETER / 2) # Hook arm (vertical part) arm = ( cq.Workplane("XY") .box(HOOK_WIDTH, HOOK_THICKNESS, HOOK_DEPTH) .translate((0, 0, MOUNT_THICKNESS / 2 + HOOK_DEPTH / 2)) ) # Hook tip (horizontal part curving inward) tip = ( cq.Workplane("XY") .box(HOOK_WIDTH, HOOK_OPENING, HOOK_THICKNESS) .translate( ( 0, -HOOK_OPENING / 2 + HOOK_THICKNESS / 2, MOUNT_THICKNESS / 2 + HOOK_DEPTH - HOOK_THICKNESS / 2, ) ) ) return mount.union(arm).union(tip) def export(hook: cq.Workplane) -> None: """Export to STL and SVG.""" stl_path = Path(__file__).parent.parent / "stl" / "fridge_hook_tool.stl" svg_path = Path(__file__).parent.parent / "svg" / "fridge_hook_tool.svg" cq.exporters.export(hook, str(stl_path)) cq.exporters.export(hook, str(svg_path), exportType="SVG") print(f"Exported: {stl_path}") print(f"Exported: {svg_path}") if __name__ == "__main__": hook = build_fridge_hook() export(hook) ================================================ FILE: hardware/cad/gripper_tips.py ================================================ """Compliant gripper fingertips — print in TPU 95A. Adds soft grip surface to SO-101 stock gripper fingers. Reference: https://www.thingiverse.com/thing:7153144 (NekoMaker TPU grip) Usage: uv run --group cad python hardware/cad/gripper_tips.py """ from pathlib import Path import cadquery as cq # --- Parameters (all in mm) --- # SO-101 gripper finger (approximate) # TODO: measure from real hardware FINGER_WIDTH = 20.0 FINGER_DEPTH = 8.0 TIP_THICKNESS = 3.0 TIP_LENGTH = 25.0 # Grip texture (ridges) RIDGE_COUNT = 5 RIDGE_DEPTH = 0.8 RIDGE_WIDTH = 1.5 def build_gripper_tip() -> cq.Workplane: """Build one compliant gripper fingertip.""" tip = cq.Workplane("XY").box(FINGER_WIDTH, TIP_THICKNESS, TIP_LENGTH) # Cut grip ridges on contact face for i in range(RIDGE_COUNT): z = -TIP_LENGTH / 2 + (i + 1) * TIP_LENGTH / (RIDGE_COUNT + 1) ridge = cq.Workplane("XY").box(FINGER_WIDTH + 1, RIDGE_DEPTH, RIDGE_WIDTH) ridge = ridge.translate((0, TIP_THICKNESS / 2 - RIDGE_DEPTH / 2, z)) tip = tip.cut(ridge) return tip def export(part: cq.Workplane) -> None: """Export to STL and SVG.""" stl = Path(__file__).parent.parent / "stl" / "gripper_tips_tpu.stl" svg = Path(__file__).parent.parent / "svg" / "gripper_tips_tpu.svg" cq.exporters.export(part, str(stl)) cq.exporters.export(part, str(svg), exportType="SVG") print(f"Exported: {stl}") print(f"Exported: {svg}") if __name__ == "__main__": export(build_gripper_tip()) ================================================ FILE: hardware/cad/pipette_mount.py ================================================ """Pipette mount adapter — SO-101 gripper to digital-pipette-v2 body. Clamps around the pipette barrel and attaches to the tool cone male base. Reference: https://github.com/ac-rad/digital-pipette-v2 (STL/f3d for body dims) Usage: uv run --group cad python hardware/cad/pipette_mount.py Exports: hardware/stl/pipette_mount_so101.stl hardware/svg/pipette_mount_so101.svg """ from pathlib import Path import cadquery as cq # --- Parameters (all in mm) --- # Pipette barrel (digital-pipette-v2 approximate) # TODO: measure from real digital-pipette-v2 STL BARREL_DIAMETER = 20.0 BARREL_CLEARANCE = 0.3 # Clamp dimensions CLAMP_LENGTH = 40.0 # Along pipette axis CLAMP_WALL = 4.0 CLAMP_OUTER = BARREL_DIAMETER + BARREL_CLEARANCE * 2 + CLAMP_WALL * 2 # Mounting plate (mates with tool cone male base) MOUNT_WIDTH = 36.0 # Match tool cone base diameter MOUNT_THICKNESS = 5.0 # Clamp split (for tightening) SPLIT_GAP = 2.0 SCREW_DIAMETER = 3.2 # M3 clearance SCREW_OFFSET = CLAMP_OUTER / 2 + 3.0 def build_pipette_mount() -> cq.Workplane: """Build pipette mount clamp with mounting plate. Returns: CadQuery workplane with mount solid. """ # Mounting plate mount = cq.Workplane("XY").box(MOUNT_WIDTH, MOUNT_WIDTH, MOUNT_THICKNESS) # Clamp body (cylinder around pipette barrel) clamp = ( cq.Workplane("XY") .cylinder(CLAMP_LENGTH, CLAMP_OUTER / 2) .translate((0, 0, MOUNT_THICKNESS / 2 + CLAMP_LENGTH / 2)) ) # Cut barrel hole barrel = ( cq.Workplane("XY") .cylinder(CLAMP_LENGTH + 1, (BARREL_DIAMETER + BARREL_CLEARANCE * 2) / 2) .translate((0, 0, MOUNT_THICKNESS / 2 + CLAMP_LENGTH / 2)) ) clamp = clamp.cut(barrel) # Cut split gap for clamping gap = ( cq.Workplane("XY") .box(SPLIT_GAP, CLAMP_OUTER + 1, CLAMP_LENGTH + 1) .translate((0, 0, MOUNT_THICKNESS / 2 + CLAMP_LENGTH / 2)) ) clamp = clamp.cut(gap) # Add screw holes for clamp tightening (2x M3) for z_off in [CLAMP_LENGTH * 0.25, CLAMP_LENGTH * 0.75]: hole = ( cq.Workplane("XY") .cylinder(CLAMP_OUTER + 10, SCREW_DIAMETER / 2) .rotateAboutCenter((1, 0, 0), 90) .translate( ( 0, 0, MOUNT_THICKNESS / 2 + z_off, ) ) ) clamp = clamp.cut(hole) return mount.union(clamp) def export(part: cq.Workplane) -> None: """Export to STL and SVG.""" stl_path = Path(__file__).parent.parent / "stl" / "pipette_mount_so101.stl" svg_path = Path(__file__).parent.parent / "svg" / "pipette_mount_so101.svg" cq.exporters.export(part, str(stl_path)) cq.exporters.export(part, str(svg_path), exportType="SVG") print(f"Exported: {stl_path}") print(f"Exported: {svg_path}") if __name__ == "__main__": part = build_pipette_mount() export(part) ================================================ FILE: hardware/cad/plate_holder.py ================================================ """96-well plate holder with alignment pins. SBS/ANSI standard footprint: 127.76 × 85.48 mm. Reference: https://en.wikipedia.org/wiki/Microplate#Standards Usage: uv run --group cad python hardware/cad/plate_holder.py Exports: hardware/stl/96well_plate_holder.stl hardware/svg/96well_plate_holder.svg """ from pathlib import Path import cadquery as cq # --- Parameters (all in mm) --- # SBS standard plate footprint PLATE_LENGTH = 127.76 PLATE_WIDTH = 85.48 PLATE_HEIGHT = 14.35 # Standard microplate height # Holder dimensions WALL_THICKNESS = 2.0 BASE_THICKNESS = 3.0 HOLDER_CLEARANCE = 0.5 # Gap between plate and holder walls PIN_DIAMETER = 3.0 PIN_HEIGHT = 5.0 PIN_INSET = 5.0 # Distance from corner to pin center # Derived INNER_LENGTH = PLATE_LENGTH + HOLDER_CLEARANCE * 2 INNER_WIDTH = PLATE_WIDTH + HOLDER_CLEARANCE * 2 OUTER_LENGTH = INNER_LENGTH + WALL_THICKNESS * 2 OUTER_WIDTH = INNER_WIDTH + WALL_THICKNESS * 2 WALL_HEIGHT = PLATE_HEIGHT * 0.6 # Walls don't need to be full height def build_plate_holder() -> cq.Workplane: """Build 96-well plate holder with alignment pins. Returns: CadQuery workplane with the plate holder solid. """ # Base plate holder = ( cq.Workplane("XY") .box(OUTER_LENGTH, OUTER_WIDTH, BASE_THICKNESS) .translate((0, 0, BASE_THICKNESS / 2)) ) # Cut inner pocket for plate holder = ( holder.faces(">Z") .workplane() .rect(INNER_LENGTH, INNER_WIDTH) .cutBlind(-BASE_THICKNESS + 1.0) # Leave 1mm floor ) # Add walls walls = ( cq.Workplane("XY") .box(OUTER_LENGTH, OUTER_WIDTH, WALL_HEIGHT) .translate((0, 0, BASE_THICKNESS + WALL_HEIGHT / 2)) ) inner_cut = ( cq.Workplane("XY") .box(INNER_LENGTH, INNER_WIDTH, WALL_HEIGHT + 1) .translate((0, 0, BASE_THICKNESS + WALL_HEIGHT / 2)) ) walls = walls.cut(inner_cut) holder = holder.union(walls) # Add alignment pins at corners pin_positions = [ (INNER_LENGTH / 2 - PIN_INSET, INNER_WIDTH / 2 - PIN_INSET), (-INNER_LENGTH / 2 + PIN_INSET, INNER_WIDTH / 2 - PIN_INSET), (INNER_LENGTH / 2 - PIN_INSET, -INNER_WIDTH / 2 + PIN_INSET), (-INNER_LENGTH / 2 + PIN_INSET, -INNER_WIDTH / 2 + PIN_INSET), ] for x, y in pin_positions: pin = ( cq.Workplane("XY") .cylinder(PIN_HEIGHT, PIN_DIAMETER / 2) .translate((x, y, BASE_THICKNESS + PIN_HEIGHT / 2)) ) holder = holder.union(pin) return holder def export(holder: cq.Workplane) -> None: """Export to STL and SVG.""" stl_path = Path(__file__).parent.parent / "stl" / "96well_plate_holder.stl" svg_path = Path(__file__).parent.parent / "svg" / "96well_plate_holder.svg" cq.exporters.export(holder, str(stl_path)) cq.exporters.export(holder, str(svg_path), exportType="SVG") print(f"Exported: {stl_path}") print(f"Exported: {svg_path}") if __name__ == "__main__": holder = build_plate_holder() export(holder) ================================================ FILE: hardware/cad/stl_to_svg.py ================================================ """Convert STL files to isometric wireframe SVGs using CadQuery. CadQuery's SVG exporter renders visible edges with hidden-line removal, producing proper 3D wireframe views — unlike OpenSCAD's projection() which only gives silhouette outlines. Usage: python hardware/cad/stl_to_svg.py hardware/stl/plate_holder.stl hardware/svg/plate_holder.svg python hardware/cad/stl_to_svg.py --all # convert all STLs in hardware/stl/ """ from __future__ import annotations import argparse import sys from pathlib import Path STL_DIR = Path(__file__).resolve().parent.parent / "stl" SVG_DIR = Path(__file__).resolve().parent.parent / "svg" def stl_to_svg(stl_path: Path, svg_path: Path) -> None: """Import STL and export as isometric wireframe SVG.""" import cadquery as cq from OCP.StlAPI import StlAPI_Reader from OCP.TopoDS import TopoDS_Shape reader = StlAPI_Reader() ocp_shape = TopoDS_Shape() reader.Read(ocp_shape, str(stl_path)) shape = cq.Workplane("XY").add(cq.Shape(ocp_shape)) cq.exporters.export(shape, str(svg_path), exportType="SVG") print(f" {svg_path.name}") def main() -> int: parser = argparse.ArgumentParser(description="STL → wireframe SVG via CadQuery") parser.add_argument("stl", nargs="?", help="Input STL file") parser.add_argument("svg", nargs="?", help="Output SVG file") parser.add_argument("--all", action="store_true", help="Convert all STLs") args = parser.parse_args() if args.all: stls = sorted(STL_DIR.glob("*.stl")) if not stls: print(f"No STL files in {STL_DIR}") return 1 for stl in stls: svg = SVG_DIR / f"{stl.stem}.svg" stl_to_svg(stl, svg) return 0 if args.stl and args.svg: stl_to_svg(Path(args.stl), Path(args.svg)) return 0 parser.print_help() return 1 if __name__ == "__main__": sys.exit(main()) ================================================ FILE: hardware/cad/theme_svgs.py ================================================ """Post-process SVGs (CadQuery or OpenSCAD) to add light/dark theme support. Injects CSS with @media (prefers-color-scheme: dark) into each SVG. Adds a themed background rect and inverts stroke colors for dark mode. Usage: python hardware/cad/theme_svgs.py """ import re from pathlib import Path THEME_STYLE = """\ """ SVG_DIR = Path(__file__).parent.parent / "svg" SKIP = {"system_overview.svg"} def theme_svg(path: Path) -> None: """Inject theme CSS into a CadQuery-generated SVG.""" content = path.read_text() if "prefers-color-scheme" in content: return # Find the end of the opening tag (not ) match = re.search(r"]*>", content) if not match: print(f"Skipped: {path.name} (no tag found)") return svg_end = match.end() # Extract width/height w_match = re.search(r'width="([^"]+)"', match.group()) h_match = re.search(r'height="([^"]+)"', match.group()) w = w_match.group(1) if w_match else "100%" h = h_match.group(1) if h_match else "100%" bg_rect = f' ' injection = f"\n{THEME_STYLE}\n{bg_rect}" themed = content[:svg_end] + injection + content[svg_end:] path.write_text(themed) print(f"Themed: {path.name}") def main() -> None: """Theme all SVGs in hardware/svg/.""" for svg in sorted(SVG_DIR.glob("*.svg")): if svg.name in SKIP: print(f"Skipped: {svg.name} (manually themed)") continue theme_svg(svg) if __name__ == "__main__": main() ================================================ FILE: hardware/cad/tip_rack_holder.py ================================================ """Pipette tip rack holder — fixed workspace position. Holds a standard 96-tip rack (SBS footprint variant). Usage: uv run --group cad python hardware/cad/tip_rack_holder.py """ from pathlib import Path import cadquery as cq # --- Parameters (all in mm) --- # Standard tip rack (approximate — varies by manufacturer) RACK_LENGTH = 122.0 RACK_WIDTH = 80.0 WALL_THICKNESS = 2.0 WALL_HEIGHT = 10.0 BASE_THICKNESS = 3.0 CLEARANCE = 0.5 INNER_L = RACK_LENGTH + CLEARANCE * 2 INNER_W = RACK_WIDTH + CLEARANCE * 2 OUTER_L = INNER_L + WALL_THICKNESS * 2 OUTER_W = INNER_W + WALL_THICKNESS * 2 def build_tip_rack_holder() -> cq.Workplane: """Build tip rack holder tray.""" base = cq.Workplane("XY").box(OUTER_L, OUTER_W, BASE_THICKNESS) walls = cq.Workplane("XY").box(OUTER_L, OUTER_W, WALL_HEIGHT) walls = walls.translate((0, 0, BASE_THICKNESS / 2 + WALL_HEIGHT / 2)) inner = cq.Workplane("XY").box(INNER_L, INNER_W, WALL_HEIGHT + 1) inner = inner.translate((0, 0, BASE_THICKNESS / 2 + WALL_HEIGHT / 2)) walls = walls.cut(inner) return base.union(walls) def export(part: cq.Workplane) -> None: """Export to STL and SVG.""" stl = Path(__file__).parent.parent / "stl" / "tip_rack_holder.stl" svg = Path(__file__).parent.parent / "svg" / "tip_rack_holder.svg" cq.exporters.export(part, str(stl)) cq.exporters.export(part, str(svg), exportType="SVG") print(f"Exported: {stl}") print(f"Exported: {svg}") if __name__ == "__main__": export(build_tip_rack_holder()) ================================================ FILE: hardware/cad/tool_changer.py ================================================ """Tool changer components — robot-side and tool-side cones. Based on Berkeley AutoLab passive modular tool changer design: - Paper: https://goldberg.berkeley.edu/pubs/CASE2018-ron-tool-changer-submitted.pdf - Repo: https://github.com/BerkeleyAutomation/RobotToolChanger Key design: truncated cone (10° angle) + dowel pin alignment. Usage: uv run --group cad python hardware/cad/tool_changer.py Exports: hardware/stl/tool_cone_robot.stl (female, mounts on SO-101 wrist) hardware/stl/tool_cone_pipette.stl (male, attaches to pipette) hardware/stl/tool_cone_gripper.stl (male, attaches to gripper) hardware/stl/tool_cone_hook.stl (male, attaches to fridge hook) hardware/svg/tool_cone_robot.svg hardware/svg/tool_cone_male.svg """ import math from pathlib import Path import cadquery as cq # --- Parameters (all in mm) --- # SO-101 wrist flange (motor 5 horn mount) # TODO: measure from real hardware or SO-ARM100 STEP files WRIST_SCREW_PATTERN_RADIUS = 9.0 WRIST_SCREW_DIAMETER = 3.2 # M3 clearance # Cone geometry (Berkeley design: 10° half-angle) CONE_ANGLE_DEG = 10.0 CONE_HEIGHT = 15.0 CONE_TOP_RADIUS = 15.0 CONE_BOTTOM_RADIUS = CONE_TOP_RADIUS - CONE_HEIGHT * math.tan(math.radians(CONE_ANGLE_DEG)) # Dowel pins for alignment DOWEL_DIAMETER = 3.0 DOWEL_HEIGHT = 8.0 DOWEL_OFFSET = 10.0 # Magnet pockets (5mm dia × 3mm deep neodymium) MAGNET_DIAMETER = 5.0 MAGNET_DEPTH = 3.0 MAGNET_OFFSET = 12.0 # Base plate BASE_THICKNESS = 5.0 BASE_RADIUS = CONE_TOP_RADIUS + 3 def _make_truncated_cone(height: float, r_bottom: float, r_top: float) -> cq.Workplane: """Create a truncated cone (frustum) via revolve. Args: height: Cone height. r_bottom: Bottom radius (smaller). r_top: Top radius (larger). Returns: CadQuery solid. """ return ( cq.Workplane("XZ") .moveTo(0, 0) .lineTo(r_bottom, 0) .lineTo(r_top, height) .lineTo(0, height) .close() .revolve(360, (0, 0, 0), (0, 1, 0)) ) def build_robot_cone() -> cq.Workplane: """Build female (robot-side) cone adapter for SO-101 wrist. Returns: CadQuery workplane with female cone solid. """ # Base cylinder base = cq.Workplane("XY").cylinder(BASE_THICKNESS, BASE_RADIUS) # Cut conical pocket (female) cone = _make_truncated_cone(CONE_HEIGHT, CONE_BOTTOM_RADIUS, CONE_TOP_RADIUS) cone = cone.translate((0, 0, -BASE_THICKNESS / 2 + 0.5)) base = base.cut(cone) # M3 mounting holes (4x at 90° intervals, offset 45°) for i in range(4): angle = math.radians(i * 90 + 45) x = WRIST_SCREW_PATTERN_RADIUS * math.cos(angle) y = WRIST_SCREW_PATTERN_RADIUS * math.sin(angle) hole = cq.Workplane("XY").cylinder(BASE_THICKNESS + 1, WRIST_SCREW_DIAMETER / 2) base = base.cut(hole.translate((x, y, 0))) # Dowel pin holes (2x, opposing) for sign in [1, -1]: hole = cq.Workplane("XY").cylinder(DOWEL_HEIGHT, DOWEL_DIAMETER / 2) z = BASE_THICKNESS / 2 - DOWEL_HEIGHT / 2 base = base.cut(hole.translate((sign * DOWEL_OFFSET, 0, z))) # Magnet pockets (2x, opposing on Y axis) for sign in [1, -1]: pocket = cq.Workplane("XY").cylinder(MAGNET_DEPTH, MAGNET_DIAMETER / 2) z = -BASE_THICKNESS / 2 + MAGNET_DEPTH / 2 base = base.cut(pocket.translate((0, sign * MAGNET_OFFSET, z))) return base def build_male_cone() -> cq.Workplane: """Build male (tool-side) cone base. Returns: CadQuery workplane with male cone solid. """ # Conical protrusion (0.3mm smaller for clearance) cone = _make_truncated_cone(CONE_HEIGHT, CONE_BOTTOM_RADIUS - 0.3, CONE_TOP_RADIUS - 0.3) # Base plate for tool attachment base = cq.Workplane("XY").cylinder(BASE_THICKNESS, BASE_RADIUS) base = base.translate((0, 0, -CONE_HEIGHT / 2 - BASE_THICKNESS / 2)) result = cone.union(base) # Dowel pin protrusions (0.1mm smaller for fit) for sign in [1, -1]: pin = cq.Workplane("XY").cylinder(DOWEL_HEIGHT, DOWEL_DIAMETER / 2 - 0.1) z = CONE_HEIGHT / 2 + DOWEL_HEIGHT / 2 result = result.union(pin.translate((sign * DOWEL_OFFSET, 0, z))) # Magnet pockets (matching robot side) for sign in [1, -1]: pocket = cq.Workplane("XY").cylinder(MAGNET_DEPTH, MAGNET_DIAMETER / 2) z = CONE_HEIGHT / 2 - MAGNET_DEPTH / 2 result = result.cut(pocket.translate((0, sign * MAGNET_OFFSET, z))) return result def export_all() -> None: """Export all tool changer components.""" stl_dir = Path(__file__).parent.parent / "stl" svg_dir = Path(__file__).parent.parent / "svg" robot_cone = build_robot_cone() cq.exporters.export(robot_cone, str(stl_dir / "tool_cone_robot.stl")) cq.exporters.export(robot_cone, str(svg_dir / "tool_cone_robot.svg"), exportType="SVG") print("Exported: tool_cone_robot.stl + .svg") male_cone = build_male_cone() for name in ["pipette", "gripper", "hook"]: cq.exporters.export(male_cone, str(stl_dir / f"tool_cone_{name}.stl")) cq.exporters.export(male_cone, str(svg_dir / f"tool_cone_{name}.svg"), exportType="SVG") print(f"Exported: tool_cone_{name}.stl + .svg") cq.exporters.export(male_cone, str(svg_dir / "tool_cone_male.svg"), exportType="SVG") print("Exported: tool_cone_male.svg") if __name__ == "__main__": export_all() ================================================ FILE: hardware/cad/tool_dock.py ================================================ """3-station tool parking dock. Holds 3 tools side-by-side with magnet retention. Reference: Berkeley passive tool changer parking housing. Usage: uv run --group cad python hardware/cad/tool_dock.py """ from pathlib import Path import cadquery as cq # --- Parameters (all in mm) --- SLOT_DIAMETER = 38.0 # Fits tool cone base (36mm + clearance) SLOT_DEPTH = 20.0 SLOT_SPACING = 50.0 # Center-to-center NUM_SLOTS = 3 BASE_THICKNESS = 5.0 MAGNET_DIAMETER = 5.0 MAGNET_DEPTH = 3.0 DOCK_LENGTH = SLOT_SPACING * (NUM_SLOTS - 1) + SLOT_DIAMETER + 10 DOCK_WIDTH = SLOT_DIAMETER + 10 def build_tool_dock() -> cq.Workplane: """Build 3-station tool parking dock.""" base = cq.Workplane("XY").box(DOCK_LENGTH, DOCK_WIDTH, BASE_THICKNESS + SLOT_DEPTH) for i in range(NUM_SLOTS): x = (i - 1) * SLOT_SPACING slot = cq.Workplane("XY").cylinder(SLOT_DEPTH, SLOT_DIAMETER / 2) slot = slot.translate((x, 0, BASE_THICKNESS / 2)) base = base.cut(slot) # Magnet pocket at bottom of each slot mag = cq.Workplane("XY").cylinder(MAGNET_DEPTH, MAGNET_DIAMETER / 2) z = -(BASE_THICKNESS + SLOT_DEPTH) / 2 + MAGNET_DEPTH / 2 base = base.cut(mag.translate((x, 0, z))) return base def export(part: cq.Workplane) -> None: """Export to STL and SVG.""" stl = Path(__file__).parent.parent / "stl" / "tool_dock_3station.stl" svg = Path(__file__).parent.parent / "svg" / "tool_dock_3station.svg" cq.exporters.export(part, str(stl)) cq.exporters.export(part, str(svg), exportType="SVG") print(f"Exported: {stl}") print(f"Exported: {svg}") if __name__ == "__main__": export(build_tool_dock()) ================================================ FILE: hardware/scad/fridge_hook.scad ================================================ // Fridge hook end-effector — fits ~20mm bar handles // Port of hardware/cad/fridge_hook.py // Regenerate: openscad -o hardware/stl/fridge_hook_tool.stl hardware/scad/fridge_hook.scad // Parameters (mm) HOOK_OPENING = 25.0; // fits ~20mm bar HOOK_DEPTH = 40.0; HOOK_THICKNESS = 6.0; HOOK_WIDTH = 20.0; MOUNT_DIAMETER = 30.0; MOUNT_THICKNESS = 5.0; union() { // Mounting plate (circular) cylinder(h = MOUNT_THICKNESS, d = MOUNT_DIAMETER, $fn = 48); // Vertical arm translate([0, 0, MOUNT_THICKNESS]) translate([-HOOK_WIDTH / 2, -HOOK_THICKNESS / 2, 0]) cube([HOOK_WIDTH, HOOK_THICKNESS, HOOK_DEPTH]); // Horizontal hook tip translate([0, HOOK_OPENING / 2 - HOOK_THICKNESS / 2, MOUNT_THICKNESS + HOOK_DEPTH - HOOK_THICKNESS]) translate([-HOOK_WIDTH / 2, 0, 0]) cube([HOOK_WIDTH, HOOK_OPENING, HOOK_THICKNESS]); } ================================================ FILE: hardware/scad/gripper_tips.scad ================================================ // Gripper fingertip attachments — TPU 95A with grip ridges // Port of hardware/cad/gripper_tips.py // Regenerate: openscad -o hardware/stl/gripper_tips_tpu.stl hardware/scad/gripper_tips.scad // Parameters (mm) FINGER_WIDTH = 20.0; TIP_THICKNESS = 3.0; TIP_LENGTH = 25.0; RIDGE_COUNT = 5; RIDGE_DEPTH = 0.8; RIDGE_WIDTH = 1.5; // Tip base with grip ridges cut out difference() { cube([FINGER_WIDTH, TIP_THICKNESS, TIP_LENGTH], center = true); // Cut ridges along the grip surface for (i = [0 : RIDGE_COUNT - 1]) { z_pos = -TIP_LENGTH / 2 + TIP_LENGTH / (RIDGE_COUNT + 1) * (i + 1); translate([0, TIP_THICKNESS / 2 - RIDGE_DEPTH / 2 + 0.5, z_pos]) cube([FINGER_WIDTH + 1, RIDGE_DEPTH, RIDGE_WIDTH], center = true); } } ================================================ FILE: hardware/scad/pipette_mount.scad ================================================ // Pipette mount — barrel clamp for digital-pipette-v2 on SO-101 wrist // Port of hardware/cad/pipette_mount.py // Regenerate: openscad -o hardware/stl/pipette_mount_so101.stl hardware/scad/pipette_mount.scad // Parameters (mm) BARREL_DIAMETER = 20.0; BARREL_CLEARANCE = 0.3; CLAMP_LENGTH = 40.0; CLAMP_WALL = 4.0; MOUNT_WIDTH = 36.0; MOUNT_THICKNESS = 5.0; SPLIT_GAP = 2.0; SCREW_DIAMETER = 3.2; // M3 clearance // Derived BORE_D = BARREL_DIAMETER + BARREL_CLEARANCE * 2; // 20.6 CLAMP_OD = BORE_D + CLAMP_WALL * 2; // 28.6 SCREW_OFFSET = CLAMP_OD / 2 + 3; // 17.3 union() { // Mounting plate translate([0, 0, MOUNT_THICKNESS / 2]) cube([MOUNT_WIDTH, MOUNT_WIDTH, MOUNT_THICKNESS], center = true); // Clamp body with barrel hole and split gap translate([0, 0, MOUNT_THICKNESS]) difference() { // Outer clamp cylinder cylinder(h = CLAMP_LENGTH, d = CLAMP_OD, $fn = 48); // Barrel bore translate([0, 0, -0.5]) cylinder(h = CLAMP_LENGTH + 1, d = BORE_D, $fn = 48); // Split gap for tightening translate([-SPLIT_GAP / 2, -CLAMP_OD / 2 - 0.5, -0.5]) cube([SPLIT_GAP, CLAMP_OD + 1, CLAMP_LENGTH + 1]); // Screw holes (2x, through the split ears) for (z_frac = [0.25, 0.75]) { translate([0, -SCREW_OFFSET - 5, CLAMP_LENGTH * z_frac]) rotate([90, 0, 90]) translate([0, 0, -MOUNT_WIDTH / 2]) cylinder(h = MOUNT_WIDTH, d = SCREW_DIAMETER, $fn = 24); } } } ================================================ FILE: hardware/scad/plate_holder.scad ================================================ // 96-well microplate holder — SBS/ANSI standard with alignment pins // Port of hardware/cad/plate_holder.py // Regenerate: openscad -o hardware/stl/96well_plate_holder.stl hardware/scad/plate_holder.scad // Parameters (mm) PLATE_LENGTH = 127.76; // SBS standard PLATE_WIDTH = 85.48; // SBS standard PLATE_HEIGHT = 14.35; WALL_THICKNESS = 2.0; BASE_THICKNESS = 3.0; HOLDER_CLEARANCE = 0.5; PIN_DIAMETER = 3.0; PIN_HEIGHT = 5.0; PIN_INSET = 5.0; // Derived INNER_L = PLATE_LENGTH + HOLDER_CLEARANCE * 2; // 128.76 INNER_W = PLATE_WIDTH + HOLDER_CLEARANCE * 2; // 86.48 OUTER_L = INNER_L + WALL_THICKNESS * 2; // 132.76 OUTER_W = INNER_W + WALL_THICKNESS * 2; // 90.48 WALL_HEIGHT = PLATE_HEIGHT * 0.6; // 8.61 FLOOR_DEPTH = 1.0; union() { // Base plate with pocket (floor_depth = 1mm) difference() { translate([0, 0, BASE_THICKNESS / 2]) cube([OUTER_L, OUTER_W, BASE_THICKNESS], center = true); // Pocket cut (leave 1mm floor) translate([0, 0, FLOOR_DEPTH + (BASE_THICKNESS - FLOOR_DEPTH) / 2 + 0.01]) cube([INNER_L, INNER_W, BASE_THICKNESS - FLOOR_DEPTH + 0.02], center = true); } // Walls translate([0, 0, BASE_THICKNESS + WALL_HEIGHT / 2]) difference() { cube([OUTER_L, OUTER_W, WALL_HEIGHT], center = true); cube([INNER_L, INNER_W, WALL_HEIGHT + 1], center = true); } // 4 alignment pins at corners for (sx = [-1, 1]) for (sy = [-1, 1]) translate([ sx * (INNER_L / 2 - PIN_INSET), sy * (INNER_W / 2 - PIN_INSET), BASE_THICKNESS + PIN_HEIGHT / 2 ]) cylinder(h = PIN_HEIGHT, d = PIN_DIAMETER, center = true, $fn = 24); } ================================================ FILE: hardware/scad/tip_rack_holder.scad ================================================ // Tip rack holder — tray with walls for standard 96-tip rack // Port of hardware/cad/tip_rack_holder.py // Regenerate: openscad -o hardware/stl/tip_rack_holder.stl hardware/scad/tip_rack_holder.scad // Parameters (mm) RACK_LENGTH = 122.0; RACK_WIDTH = 80.0; WALL_THICKNESS = 2.0; WALL_HEIGHT = 10.0; BASE_THICKNESS = 3.0; CLEARANCE = 0.5; // Derived INNER_L = RACK_LENGTH + CLEARANCE * 2; // 123.0 INNER_W = RACK_WIDTH + CLEARANCE * 2; // 81.0 OUTER_L = INNER_L + WALL_THICKNESS * 2; // 127.0 OUTER_W = INNER_W + WALL_THICKNESS * 2; // 85.0 // Base plate translate([0, 0, BASE_THICKNESS / 2]) cube([OUTER_L, OUTER_W, BASE_THICKNESS], center = true); // Walls (outer box minus inner box) translate([0, 0, BASE_THICKNESS + WALL_HEIGHT / 2]) difference() { cube([OUTER_L, OUTER_W, WALL_HEIGHT], center = true); cube([INNER_L, INNER_W, WALL_HEIGHT + 1], center = true); } ================================================ FILE: hardware/scad/tool_changer.scad ================================================ // Tool changer — Berkeley passive tool changer (truncated cone + magnets + dowels) // Port of hardware/cad/tool_changer.py // Regenerate: // openscad -o hardware/stl/tool_cone_robot.stl -D 'PART="robot"' hardware/scad/tool_changer.scad // openscad -o hardware/stl/tool_cone_pipette.stl -D 'PART="male"' hardware/scad/tool_changer.scad // openscad -o hardware/stl/tool_cone_gripper.stl -D 'PART="male"' hardware/scad/tool_changer.scad // openscad -o hardware/stl/tool_cone_hook.stl -D 'PART="male"' hardware/scad/tool_changer.scad // Part selector: "robot" (female) or "male" (tool-side) PART = "robot"; // Parameters (mm) — Berkeley design WRIST_SCREW_PATTERN_RADIUS = 9.0; WRIST_SCREW_DIAMETER = 3.2; // M3 clearance CONE_ANGLE_DEG = 10.0; CONE_HEIGHT = 15.0; CONE_TOP_RADIUS = 15.0; CONE_BOTTOM_RADIUS = CONE_TOP_RADIUS - CONE_HEIGHT * tan(CONE_ANGLE_DEG); DOWEL_DIAMETER = 3.0; DOWEL_HEIGHT = 8.0; DOWEL_OFFSET = 10.0; MAGNET_DIAMETER = 5.0; MAGNET_DEPTH = 3.0; MAGNET_OFFSET = 12.0; BASE_THICKNESS = 5.0; BASE_RADIUS = CONE_TOP_RADIUS + 3; // 18.0 MALE_CLEARANCE = 0.3; MALE_DOWEL_CLEARANCE = 0.1; // Truncated cone (frustum) via rotate_extrude of a trapezoid profile module frustum(r_bottom, r_top, h) { rotate_extrude($fn = 64) polygon(points = [ [0, 0], [r_bottom, 0], [r_top, h], [0, h] ]); } // Robot-side adapter (female) — conical pocket, screw holes, dowel holes, magnet pockets module robot_cone() { difference() { // Base cylinder cylinder(h = BASE_THICKNESS, r = BASE_RADIUS, $fn = 64); // Conical pocket (cut from top) translate([0, 0, BASE_THICKNESS - CONE_HEIGHT]) frustum(CONE_BOTTOM_RADIUS, CONE_TOP_RADIUS, CONE_HEIGHT + 0.01); // M3 mounting holes (4x at 90° intervals, offset 45°) for (a = [45, 135, 225, 315]) translate([ WRIST_SCREW_PATTERN_RADIUS * cos(a), WRIST_SCREW_PATTERN_RADIUS * sin(a), -0.5 ]) cylinder(h = BASE_THICKNESS + 1, d = WRIST_SCREW_DIAMETER, $fn = 24); // Dowel pin holes (2x opposing) for (a = [0, 180]) translate([ DOWEL_OFFSET * cos(a), DOWEL_OFFSET * sin(a), BASE_THICKNESS - DOWEL_HEIGHT ]) cylinder(h = DOWEL_HEIGHT + 0.01, d = DOWEL_DIAMETER, $fn = 24); // Magnet pockets (2x opposing, perpendicular to dowels) for (a = [90, 270]) translate([ MAGNET_OFFSET * cos(a), MAGNET_OFFSET * sin(a), BASE_THICKNESS - MAGNET_DEPTH ]) cylinder(h = MAGNET_DEPTH + 0.01, d = MAGNET_DIAMETER, $fn = 24); } } // Tool-side adapter (male) — cone protrusion, dowel pins, magnet pockets module male_cone() { difference() { union() { // Base plate cylinder(h = BASE_THICKNESS, r = BASE_RADIUS, $fn = 64); // Cone protrusion r_top_m = CONE_TOP_RADIUS - MALE_CLEARANCE; r_bottom_m = CONE_BOTTOM_RADIUS - MALE_CLEARANCE; translate([0, 0, BASE_THICKNESS]) frustum(r_bottom_m, r_top_m, CONE_HEIGHT); // Dowel pins (2x opposing) dowel_d_m = DOWEL_DIAMETER - MALE_DOWEL_CLEARANCE * 2; for (a = [0, 180]) translate([ DOWEL_OFFSET * cos(a), DOWEL_OFFSET * sin(a), BASE_THICKNESS + CONE_HEIGHT ]) cylinder(h = DOWEL_HEIGHT, d = dowel_d_m, $fn = 24); } // Magnet pockets (2x opposing, perpendicular to dowels) for (a = [90, 270]) translate([ MAGNET_OFFSET * cos(a), MAGNET_OFFSET * sin(a), BASE_THICKNESS - MAGNET_DEPTH ]) cylinder(h = MAGNET_DEPTH + 0.01, d = MAGNET_DIAMETER, $fn = 24); } } // Render selected part if (PART == "robot") { robot_cone(); } else { male_cone(); } ================================================ FILE: hardware/scad/tool_dock.scad ================================================ // Tool dock — 3-station magnetic parking rack // Port of hardware/cad/tool_dock.py (Berkeley passive tool changer design) // Regenerate: openscad -o hardware/stl/tool_dock_3station.stl hardware/scad/tool_dock.scad // Parameters (mm) SLOT_DIAMETER = 38.0; SLOT_DEPTH = 20.0; SLOT_SPACING = 50.0; // center-to-center NUM_SLOTS = 3; BASE_THICKNESS = 5.0; MAGNET_DIAMETER = 5.0; MAGNET_DEPTH = 3.0; // Derived DOCK_LENGTH = SLOT_SPACING * (NUM_SLOTS - 1) + SLOT_DIAMETER + 10; // 148 DOCK_WIDTH = SLOT_DIAMETER + 10; // 48 TOTAL_H = BASE_THICKNESS + SLOT_DEPTH; difference() { // Solid base block translate([0, 0, TOTAL_H / 2]) cube([DOCK_LENGTH, DOCK_WIDTH, TOTAL_H], center = true); // Cut cylindrical slots for (i = [0 : NUM_SLOTS - 1]) { x_pos = (i - 1) * SLOT_SPACING; // Slot pocket (cut from top) translate([x_pos, 0, BASE_THICKNESS + SLOT_DEPTH / 2 + 0.01]) cylinder(h = SLOT_DEPTH + 0.02, d = SLOT_DIAMETER, center = true, $fn = 48); // Magnet pocket (at bottom of slot) translate([x_pos, 0, BASE_THICKNESS - MAGNET_DEPTH / 2]) cylinder(h = MAGNET_DEPTH + 0.01, d = MAGNET_DIAMETER, center = true, $fn = 24); } } ================================================ FILE: hardware/slicer/validate.py ================================================ """Validate STL printability via PrusaSlicer CLI. Slices each STL and reports overhang/support warnings. PrusaSlicer is optional — graceful skip if unavailable. Usage: python hardware/slicer/validate.py --all python hardware/slicer/validate.py hardware/stl/plate_holder.stl python hardware/slicer/validate.py --all --profile tpu """ from __future__ import annotations import argparse import shutil import subprocess import sys import tempfile from pathlib import Path SLICER_CMD = "prusa-slicer" TIMEOUT_SEC = 120 STL_DIR = Path(__file__).resolve().parent.parent / "stl" PROFILE_DIR = Path(__file__).resolve().parent / "profiles" # Part-to-profile mapping (default: PLA+) TPU_PARTS = {"gripper_tips_tpu.stl"} OVERHANG_KEYWORDS = [ "overhang", "unsupported", "bridge", "support enforcer", "empty layer", ] def get_profile(stl_name: str, override: str | None = None) -> Path: """Return the slicer profile path for a given STL.""" if override == "tpu" or stl_name in TPU_PARTS: return PROFILE_DIR / "tpu_95a_02mm.ini" return PROFILE_DIR / "pla_plus_02mm.ini" def find_slicer() -> str | None: """Return slicer binary path or None.""" return shutil.which(SLICER_CMD) def validate_stl(stl_path: Path, profile: Path) -> dict: """Slice an STL and parse output for printability issues.""" result = { "file": stl_path.name, "profile": profile.stem, "warnings": [], "status": "PASS", "error": None, } with tempfile.TemporaryDirectory() as tmp: gcode_out = Path(tmp) / (stl_path.stem + ".gcode") cmd = [ SLICER_CMD, "--export-gcode", "--load", str(profile), "--output", str(gcode_out), str(stl_path), ] try: proc = subprocess.run( cmd, capture_output=True, text=True, timeout=TIMEOUT_SEC, ) output = (proc.stdout + "\n" + proc.stderr).lower() # Parse warnings for keyword in OVERHANG_KEYWORDS: if keyword in output: result["warnings"].append(keyword) if proc.returncode != 0: result["status"] = "FAIL" result["error"] = proc.stderr.strip()[:200] elif result["warnings"]: result["status"] = "WARN" except subprocess.TimeoutExpired: result["status"] = "SKIP" result["error"] = f"Timeout after {TIMEOUT_SEC}s" except FileNotFoundError: result["status"] = "SKIP" result["error"] = "Slicer binary not found" return result def collect_stls(paths: list[str]) -> list[Path]: """Collect STL files from args or --all.""" stls = [] for p in paths: path = Path(p) if path.is_file(): stls.append(path) elif path.is_dir(): stls.extend(sorted(path.glob("*.stl"))) return stls def print_report(results: list[dict]) -> int: """Print validation report table. Returns exit code.""" if not results: print("No STL files found to validate.") return 1 # Header print(f"\n{'Part':<35} {'Profile':<18} {'Warnings':<25} {'Status'}") print("-" * 85) exit_code = 0 for r in results: warnings = ", ".join(r["warnings"]) if r["warnings"] else "none" status = r["status"] error_suffix = f" ({r['error']})" if r["error"] else "" print(f"{r['file']:<35} {r['profile']:<18} {warnings:<25} {status}{error_suffix}") if status == "FAIL": exit_code = 1 passed = sum(1 for r in results if r["status"] == "PASS") total = len(results) print(f"\n{passed}/{total} parts passed printability check.\n") return exit_code def main() -> int: parser = argparse.ArgumentParser(description="Validate STL printability via slicer") parser.add_argument("files", nargs="*", help="STL files to validate") parser.add_argument("--all", action="store_true", help="Validate all STLs in hardware/stl/") parser.add_argument("--profile", choices=["pla", "tpu"], help="Force profile override") args = parser.parse_args() if not find_slicer(): print(f"SKIP: {SLICER_CMD} not found. Install via: make setup_slicer") return 0 if args.all: stls = sorted(STL_DIR.glob("*.stl")) elif args.files: stls = collect_stls(args.files) else: parser.print_help() return 1 if not stls: print(f"No STL files found in {STL_DIR}. Run: make render_parts") return 1 results = [validate_stl(stl, get_profile(stl.name, args.profile)) for stl in stls] return print_report(results) if __name__ == "__main__": sys.exit(main()) ================================================ FILE: hardware/slicer/profiles/pla_plus_02mm.ini ================================================ # PrusaSlicer profile: PLA+ 0.2mm layer height # Export from PrusaSlicer GUI, then load via: prusa-slicer --load this_file.ini # Minimal settings for printability validation (not production gcode) [print] layer_height = 0.2 first_layer_height = 0.2 perimeters = 2 fill_density = 15% support_material = 0 detect_thin_wall = 1 overhangs = 1 [filament] filament_type = PLA temperature = 210 bed_temperature = 60 [printer] nozzle_diameter = 0.4 bed_shape = 0x0,220x0,220x220,0x220 ================================================ FILE: hardware/slicer/profiles/tpu_95a_02mm.ini ================================================ # PrusaSlicer profile: TPU 95A 0.2mm layer height # For gripper_tips_tpu.stl only # Minimal settings for printability validation (not production gcode) [print] layer_height = 0.2 first_layer_height = 0.2 perimeters = 3 fill_density = 20% support_material = 0 detect_thin_wall = 1 overhangs = 1 [filament] filament_type = FLEX temperature = 230 bed_temperature = 50 [printer] nozzle_diameter = 0.4 bed_shape = 0x0,220x0,220x220,0x220 ================================================ FILE: hardware/stl/README.md ================================================ --- title: STL Files Index purpose: Index of 3D-printable parts with status, print settings, assembly, and source scripts created: 2026-03-27 updated: 2026-03-30 --- # STL Files Generated from `../parts.json` manifest via `../render.py` (CadQuery preferred, OpenSCAD fallback). ```bash make setup_cad # Install CadQuery (preferred) make setup_scad # Install OpenSCAD (fallback) make render_parts # Generate STL + SVG from parts.json make setup_slicer # Install PrusaSlicer (optional) make check_prints # Check printability make render_all # Generate + validate ``` **How it works:** `hardware/parts.json` defines all parts (names, filenames, scripts, build functions). `hardware/render.py` reads the manifest and dispatches to CadQuery (imports `build_func`, exports STL+SVG) or OpenSCAD (CLI calls with `scad_args`). Adding a part means editing `parts.json` and writing the script. **Why CadQuery + slicer?** CadQuery generates parametric STLs with isometric wireframe SVGs. PrusaSlicer validates FDM printability (overhangs, unsupported regions, gravity failures) as fast CLI feedback. **Status:** EXPERIMENTAL = draft dimensions, untested on hardware. ## System Overview ![Workspace Layout](../svg/system_overview.svg) ## Print Settings | Setting | Default | Exception | |---------|---------|-----------| | Material | PLA+ | `gripper_tips_tpu.stl` → TPU 95A | | Nozzle | 0.4mm | — | | Layer height | 0.2mm | — | | Infill | 15% | — | | Supports | >45° | — | ## Parts & Assembly ### Tool Changer System Passive tool changing based on [Berkeley design](https://goldberg.berkeley.edu/pubs/CASE2018-ron-tool-changer-submitted.pdf) (truncated cone + dowel pins + magnets). | Part | Preview | |------|---------| | Robot-side cone (female) | ![tool_cone_robot](../svg/tool_cone_robot.svg) | | Tool-side cone — pipette | ![tool_cone_pipette](../svg/tool_cone_pipette.svg) | | Tool-side cone — gripper | ![tool_cone_gripper](../svg/tool_cone_gripper.svg) | | Tool-side cone — hook | ![tool_cone_hook](../svg/tool_cone_hook.svg) | | 3-station dock | ![tool_dock](../svg/tool_dock_3station.svg) | **Assembly order:** 1. **`tool_cone_robot.stl`** — Mount on SO-101 wrist (motor 5 horn, 4× M3 screws). Stays on arm permanently. 2. **`tool_cone_pipette/gripper/hook.stl`** — Attach one to each tool. Glue or screw to tool base. 3. **`tool_dock_3station.stl`** — Fix to workspace. Insert 5mm neodymium magnets in each slot bottom. **Tool change sequence:** Approach dock → insert tool → retract → move to new slot → push onto cone → retract with new tool. ### Pipette Setup | Part | Preview | |------|---------| | Pipette mount | ![pipette_mount](../svg/pipette_mount_so101.svg) | 1. **`pipette_mount_so101.stl`** — Clamp around [digital-pipette-v2](https://github.com/ac-rad/digital-pipette-v2) barrel. Tighten with 2× M3 screws. 2. Attach `tool_cone_pipette.stl` to mount base (4× M3 or glue). 3. **`tip_rack_holder.stl`** — Place on workspace, insert tip rack. Arm picks tips by pressing pipette into rack. | Part | Preview | |------|---------| | Tip rack holder | ![tip_rack](../svg/tip_rack_holder.svg) | ### Plate Handling | Part | Preview | |------|---------| | 96-well plate holder | ![plate_holder](../svg/96well_plate_holder.svg) | 1. **`96well_plate_holder.stl`** — Place at known position. 4 alignment pins locate the plate. ### Fridge Operations | Part | Preview | |------|---------| | Fridge hook | ![fridge_hook](../svg/fridge_hook_tool.svg) | 1. **`fridge_hook_tool.stl`** — Attach `tool_cone_hook.stl` to flat mount face. Hook fits ~20mm bar handles. 2. Arm equips hook from dock → approaches fridge → hooks handle → pulls door open. ### Gripper Enhancement | Part | Preview | |------|---------| | Gripper tips (TPU) | ![gripper_tips](../svg/gripper_tips_tpu.svg) | 1. **`gripper_tips_tpu.stl`** — Press-fit or glue onto SO-101 gripper fingers. Print in TPU 95A. ## Parts Table | STL File | SVG | Source | Description | |----------|-----|--------|-------------| | `tool_cone_robot.stl` | [svg](../svg/tool_cone_robot.svg) | `tool_changer.py` | Female cone — mounts on SO-101 wrist | | `tool_cone_pipette.stl` | [svg](../svg/tool_cone_pipette.svg) | `tool_changer.py` | Male cone — pipette tool base | | `tool_cone_gripper.stl` | [svg](../svg/tool_cone_gripper.svg) | `tool_changer.py` | Male cone — gripper tool base | | `tool_cone_hook.stl` | [svg](../svg/tool_cone_hook.svg) | `tool_changer.py` | Male cone — fridge hook tool base | | `tool_dock_3station.stl` | [svg](../svg/tool_dock_3station.svg) | `tool_dock.py` | 3-slot parking rack with magnet pockets | | `pipette_mount_so101.stl` | [svg](../svg/pipette_mount_so101.svg) | `pipette_mount.py` | Barrel clamp for digital-pipette-v2 | | `96well_plate_holder.stl` | [svg](../svg/96well_plate_holder.svg) | `plate_holder.py` | SBS plate holder with alignment pins | | `fridge_hook_tool.stl` | [svg](../svg/fridge_hook_tool.svg) | `fridge_hook.py` | Hook for fridge door handle | | `tip_rack_holder.stl` | [svg](../svg/tip_rack_holder.svg) | `tip_rack_holder.py` | Tip rack tray | | `gripper_tips_tpu.stl` | [svg](../svg/gripper_tips_tpu.svg) | `gripper_tips.py` | Compliant fingertips (TPU 95A) | ## Hardware Needed (Non-Printed) - 5mm × 3mm neodymium magnets (3 for dock, 4 for cone pairs) - M3 × 8mm screws (4 for wrist mount, 2 per pipette clamp) - Glue (CA or epoxy) for cone-to-tool bonding ================================================ FILE: scripts/coordinate_cmd.py ================================================ """Send coordinate-based commands to an SO-101 arm. Usage: python scripts/coordinate_cmd.py --well A1 --action aspirate python scripts/coordinate_cmd.py --well B3 --action dispense python scripts/coordinate_cmd.py --park """ from __future__ import annotations import argparse import logging from biolab.arms import ArmConfig, DualArmConfig, DualArmController from biolab.plate import parse_well_name logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s") logger = logging.getLogger(__name__) def _make_stub_controller() -> DualArmController: """Create a stub controller for coordinate testing.""" config = DualArmConfig( arm_a=ArmConfig(arm_id="arm_a", port="/dev/null", role="follower"), arm_b=ArmConfig(arm_id="arm_b", port="/dev/null", role="follower"), ) ctrl = DualArmController(config) ctrl.connect() return ctrl def main() -> None: """Parse command and send to arm.""" parser = argparse.ArgumentParser(description="Coordinate-based arm commands") parser.add_argument("--well", help="Target well (e.g., A1, H12)") parser.add_argument("--action", choices=["aspirate", "dispense", "move"], default="move") parser.add_argument("--volume", type=float, default=100.0, help="Volume in uL") parser.add_argument("--park", action="store_true", help="Park arm") parser.add_argument("--arm", default="arm_a", help="Which arm to control") parser.add_argument("--config", default="configs/arms.yaml", help="Arms config path") args = parser.parse_args() controller = _make_stub_controller() if args.park: logger.info("Parking %s", args.arm) controller.park_all() controller.disconnect() return if not args.well: parser.error("--well is required unless --park is specified") well = parse_well_name(args.well) logger.info( "Target: %s (%.2f, %.2f mm) — action: %s", well.name, well.x_mm, well.y_mm, args.action, ) controller.send_to_well(args.arm, args.well) controller.disconnect() if __name__ == "__main__": main() ================================================ FILE: scripts/run_demo.py ================================================ """End-to-end demo orchestrator for so101-biolab-automation. Usage: python scripts/run_demo.py # full demo (all use cases) python scripts/run_demo.py --use-case uc1_single --well A1 --volume 50 python scripts/run_demo.py --use-case uc1_row --row A python scripts/run_demo.py --use-case uc1_col --col 1 python scripts/run_demo.py --use-case uc1_full python scripts/run_demo.py --use-case uc2 python scripts/run_demo.py --use-case uc3 """ from __future__ import annotations import argparse import logging from biolab.workflow import ( create_workflow_context, uc1_col, uc1_full_plate, uc1_row, uc1_single_well, uc2_fridge_open_grab_move, uc3_tool_cycle, uc4_demo_all, ) logging.basicConfig(level=logging.INFO, format="%(asctime)s %(name)s %(levelname)s %(message)s") logger = logging.getLogger(__name__) USE_CASES = ["all", "uc1_single", "uc1_row", "uc1_col", "uc1_full", "uc2", "uc3"] def main() -> None: """Run the demo orchestrator.""" parser = argparse.ArgumentParser(description="so101-biolab-automation demo") parser.add_argument("--use-case", choices=USE_CASES, default="all") parser.add_argument("--mode", choices=["full", "eval", "teleop"], default="full") parser.add_argument("--well", default="A1", help="Target well for uc1_single") parser.add_argument("--row", default="A", help="Target row for uc1_row") parser.add_argument("--col", type=int, default=1, help="Target column for uc1_col") parser.add_argument("--volume", type=float, default=50.0, help="Volume in µL") parser.add_argument("--arm", default="arm_a", help="Which arm to use") args = parser.parse_args() logger.info("Starting demo: use_case=%s", args.use_case) arm, pipette, changer, layout = create_workflow_context() try: if args.use_case == "all" or args.mode == "full": uc4_demo_all(arm, pipette, changer, layout, args.arm) elif args.use_case == "uc1_single": uc1_single_well(arm, pipette, layout, args.arm, args.well, args.volume) elif args.use_case == "uc1_row": uc1_row(arm, pipette, layout, args.arm, args.row, args.volume) elif args.use_case == "uc1_col": uc1_col(arm, pipette, layout, args.arm, args.col, args.volume) elif args.use_case == "uc1_full": uc1_full_plate(arm, pipette, layout, args.arm, args.volume) elif args.use_case == "uc2": uc2_fridge_open_grab_move(arm, changer, args.arm) elif args.use_case == "uc3": uc3_tool_cycle(arm, changer, args.arm) finally: arm.disconnect() logger.info("Demo complete.") if __name__ == "__main__": main() ================================================ FILE: src/biolab/__init__.py ================================================ """so101-biolab-automation: Dual SO-101 arm bio-lab automation.""" ================================================ FILE: src/biolab/arms.py ================================================ """Dual SO-101 arm controller wrapping LeRobot API. Manages two follower arms and optional leader arm for teleoperation. """ from __future__ import annotations import logging from dataclasses import dataclass, field from pathlib import Path from typing import Any import yaml from biolab.plate import parse_well_name from biolab.safety import PARK_POSITION logger = logging.getLogger(__name__) @dataclass class ArmConfig: """Configuration for a single SO-101 arm.""" arm_id: str port: str role: str # "follower" or "leader" cameras: dict[str, Any] = field(default_factory=dict) @dataclass class DualArmConfig: """Configuration for the dual-arm setup.""" arm_a: ArmConfig arm_b: ArmConfig leader: ArmConfig | None = None @classmethod def from_yaml(cls, path: str | Path) -> DualArmConfig: """Load configuration from YAML file. Args: path: Path to arms.yaml config file. Returns: DualArmConfig instance. """ with open(path) as f: data = yaml.safe_load(f) arm_a = ArmConfig(**data["arm_a"]) arm_b = ArmConfig(**data["arm_b"]) leader = ArmConfig(**data["leader"]) if "leader" in data else None return cls(arm_a=arm_a, arm_b=arm_b, leader=leader) class DualArmController: """Controls two SO-101 follower arms with optional leader for teaching. Usage: config = DualArmConfig.from_yaml("configs/arms.yaml") controller = DualArmController(config) controller.connect() controller.send_to_well("arm_a", "A1") controller.park_all() controller.disconnect() """ def __init__(self, config: DualArmConfig) -> None: self.config = config self._connected = False self._stub_mode = False self._robots: dict[str, Any] = {} @property def arm_ids(self) -> list[str]: """IDs of configured follower arms.""" return [ cfg.arm_id for cfg in [self.config.arm_a, self.config.arm_b] if cfg.role == "follower" ] def connect(self) -> None: """Connect to all configured arms via LeRobot.""" try: from lerobot.robots.so_follower import SO101Follower, SO101FollowerConfig except ImportError: logger.warning("lerobot not installed — running in stub mode") self._stub_mode = True self._connected = True return for arm_cfg in [self.config.arm_a, self.config.arm_b]: if arm_cfg.role != "follower": continue robot_config = SO101FollowerConfig( port=arm_cfg.port, id=arm_cfg.arm_id, cameras=arm_cfg.cameras, ) robot = SO101Follower(robot_config) robot.connect() self._robots[arm_cfg.arm_id] = robot logger.info("Connected arm %s on %s", arm_cfg.arm_id, arm_cfg.port) self._connected = True def disconnect(self) -> None: """Disconnect all arms.""" for arm_id, robot in self._robots.items(): robot.disconnect() logger.info("Disconnected arm %s", arm_id) self._robots.clear() self._connected = False self._stub_mode = False def get_observation(self, arm_id: str) -> dict[str, Any]: """Read current joint positions and camera frames from an arm. Args: arm_id: Which arm to read from. Returns: Dict with joint positions and camera data. Raises: ValueError: If arm_id is not a configured follower arm. """ if self._stub_mode: if arm_id not in self.arm_ids: raise ValueError(f"Unknown arm: {arm_id}") return {"joints": [], "stub": True} if arm_id not in self._robots: raise ValueError(f"Unknown arm: {arm_id}") return self._robots[arm_id].get_observation() def send_action(self, arm_id: str, action: Any) -> None: """Send a joint-space action to an arm. Args: arm_id: Which arm to control. action: Action tensor (joint positions). Raises: ValueError: If arm_id is not a configured follower arm. """ if self._stub_mode: if arm_id not in self.arm_ids: raise ValueError(f"Unknown arm: {arm_id}") logger.debug("Stub send_action(%s, %s)", arm_id, action) return if arm_id not in self._robots: raise ValueError(f"Unknown arm: {arm_id}") self._robots[arm_id].send_action(action) def send_to_well(self, arm_id: str, well_name: str) -> None: """Move an arm to a 96-well plate position. Args: arm_id: Which arm to move. well_name: Well name like 'A1', 'H12'. Raises: ValueError: If well_name is invalid or arm_id unknown. """ well = parse_well_name(well_name) logger.info("Moving %s to well %s (%.2f, %.2f mm)", arm_id, well.name, well.x_mm, well.y_mm) # Stub: use zero joints. Real IK mapping deferred to MVP. stub_joints = [0.0] * 6 self.send_action(arm_id, stub_joints) def park_all(self) -> None: """Move all follower arms to the park (safe) position.""" for arm_id in self.arm_ids: logger.info("Parking arm %s", arm_id) self.send_action(arm_id, list(PARK_POSITION)) ================================================ FILE: src/biolab/camera.py ================================================ """Multi-camera pipeline for workspace monitoring. Captures from overhead + wrist cameras, provides frames for: - WebRTC streaming to remote dashboard - LeRobot dataset recording """ from __future__ import annotations import logging from dataclasses import dataclass from typing import Any logger = logging.getLogger(__name__) @dataclass class CameraConfig: """Configuration for a single camera.""" name: str device_index: int # /dev/videoN or index width: int = 640 height: int = 480 fps: int = 30 class CameraPipeline: """Manages multiple cameras for the biolab workspace. Usage: pipeline = CameraPipeline([ CameraConfig("overhead", 0), CameraConfig("wrist_a", 2), CameraConfig("wrist_b", 4), ]) pipeline.start() frames = pipeline.get_frames() # {"overhead": ndarray, ...} pipeline.stop() """ def __init__(self, cameras: list[CameraConfig]) -> None: self.cameras = {cam.name: cam for cam in cameras} self._captures: dict[str, Any] = {} def start(self) -> None: """Open all camera devices. Requires cv2 (opencv-python).""" try: import cv2 except ImportError: logger.warning("cv2 not available — cameras disabled") return for name, cfg in self.cameras.items(): cap = cv2.VideoCapture(cfg.device_index) cap.set(cv2.CAP_PROP_FRAME_WIDTH, cfg.width) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, cfg.height) cap.set(cv2.CAP_PROP_FPS, cfg.fps) if not cap.isOpened(): logger.warning("Failed to open camera %s (device %d)", name, cfg.device_index) continue self._captures[name] = cap logger.info( "Camera %s opened (device %d, %dx%d@%dfps)", name, cfg.device_index, cfg.width, cfg.height, cfg.fps, ) def stop(self) -> None: """Release all camera devices.""" for name, cap in self._captures.items(): cap.release() logger.info("Camera %s released", name) self._captures.clear() def get_frame(self, name: str) -> Any | None: """Capture a single frame from a named camera. Args: name: Camera name. Returns: BGR image as numpy array, or None if capture failed. """ cap = self._captures.get(name) if cap is None: return None ret, frame = cap.read() return frame if ret else None def get_frames(self) -> dict[str, Any]: """Capture frames from all active cameras. Returns: Dict mapping camera name to BGR image array. """ frames = {} for name in self._captures: frame = self.get_frame(name) if frame is not None: frames[name] = frame return frames ================================================ FILE: src/biolab/pipette.py ================================================ """Digital pipette control via Arduino serial interface. Reference: https://github.com/ac-rad/digital-pipette-v2 Controls a linear actuator to aspirate/dispense liquid through a syringe-based pipette. """ from __future__ import annotations import logging import time from dataclasses import dataclass logger = logging.getLogger(__name__) # Actuator positions (0-1023 range for 5cm stroke linear actuator) ACTUATOR_MIN = 0 # Fully retracted (max suction) ACTUATOR_MAX = 1023 # Fully extended (no suction) DEFAULT_ASPIRATE_SPEED = 200 # ms delay between steps DEFAULT_DISPENSE_SPEED = 150 # ms delay between steps @dataclass class PipetteConfig: """Configuration for the digital pipette.""" serial_port: str = "/dev/ttyUSB0" baud_rate: int = 9600 max_volume_ul: float = 200.0 actuator_stroke_mm: float = 50.0 class DigitalPipette: """Controls the digital pipette v2 via Arduino serial. Usage: pipette = DigitalPipette(PipetteConfig(serial_port="/dev/ttyUSB0")) pipette.connect() pipette.aspirate(100.0) # Aspirate 100 µL pipette.dispense(100.0) # Dispense 100 µL pipette.disconnect() """ def __init__(self, config: PipetteConfig) -> None: self.config = config self._serial = None self._current_position = ACTUATOR_MAX # Start fully extended (empty) self._current_fill: float = 0.0 # Tracked in µL def connect(self) -> None: """Open serial connection to Arduino controller.""" try: import serial self._serial = serial.Serial( self.config.serial_port, self.config.baud_rate, timeout=2, ) time.sleep(2) # Wait for Arduino reset logger.info("Pipette connected on %s", self.config.serial_port) except ImportError: logger.warning("pyserial not installed — running in stub mode") except Exception as exc: # noqa: BLE001 # Reason: SerialException (port missing/inaccessible) degrades to stub mode logger.warning("Serial connection failed (%s) — running in stub mode", exc) def disconnect(self) -> None: """Close serial connection.""" if self._serial: self._serial.close() self._serial = None def _volume_to_steps(self, volume_ul: float) -> int: """Convert volume in µL to actuator step count. Args: volume_ul: Volume in microliters. Returns: Number of actuator steps. """ fraction = volume_ul / self.config.max_volume_ul return int(fraction * ACTUATOR_MAX) def aspirate(self, volume_ul: float) -> None: """Aspirate (draw up) a volume of liquid. Args: volume_ul: Volume to aspirate in microliters. Raises: ValueError: If volume exceeds capacity. """ if volume_ul <= 0: raise ValueError("Volume must be positive") if volume_ul > self.config.max_volume_ul: raise ValueError(f"Volume {volume_ul} µL exceeds max {self.config.max_volume_ul} µL") if self._current_fill + volume_ul > self.config.max_volume_ul: raise ValueError( f"Cannot aspirate {volume_ul} µL: would exceed capacity " f"(current fill {self._current_fill} µL, max {self.config.max_volume_ul} µL)" ) steps = self._volume_to_steps(volume_ul) target = max(self._current_position - steps, ACTUATOR_MIN) self._move_to(target) self._current_fill += volume_ul logger.info("Aspirated %.1f µL (position: %d)", volume_ul, self._current_position) def dispense(self, volume_ul: float) -> None: """Dispense (push out) a volume of liquid. Args: volume_ul: Volume to dispense in microliters. Raises: ValueError: If volume exceeds current contents. """ if volume_ul <= 0: raise ValueError("Volume must be positive") if volume_ul > self._current_fill: raise ValueError( f"Cannot dispense {volume_ul} µL: exceeds current fill of {self._current_fill} µL" ) steps = self._volume_to_steps(volume_ul) target = min(self._current_position + steps, ACTUATOR_MAX) self._move_to(target) self._current_fill -= volume_ul logger.info("Dispensed %.1f µL (position: %d)", volume_ul, self._current_position) def eject_tip(self) -> None: """Signal tip ejection (move to eject position then return).""" logger.info("Ejecting tip") if self._serial: self._serial.write(b"EJECT\n") def _move_to(self, position: int) -> None: """Move actuator to target position. Args: position: Target position (0-1023). """ if self._serial: cmd = f"MOVE {position}\n" self._serial.write(cmd.encode()) self._serial.readline() # Wait for ACK self._current_position = position ================================================ FILE: src/biolab/plate.py ================================================ """96-well microplate coordinate grid (SBS standard). SBS/ANSI standard: 127.76 x 85.48 mm footprint. Well spacing: 9.0 mm center-to-center. A1 origin: top-left corner (14.38 mm from left edge, 11.24 mm from top edge). """ from dataclasses import dataclass # SBS standard dimensions (mm) WELL_SPACING = 9.0 A1_OFFSET_X = 14.38 # mm from left edge to A1 center A1_OFFSET_Y = 11.24 # mm from top edge to A1 center ROWS = "ABCDEFGH" COLS = range(1, 13) @dataclass(frozen=True) class WellPosition: """A single well position on a 96-well plate.""" row: str # A-H col: int # 1-12 x_mm: float # mm from plate origin y_mm: float # mm from plate origin @property def name(self) -> str: """Well name like 'A1', 'H12'.""" return f"{self.row}{self.col}" def well_coordinates(row: str, col: int) -> tuple[float, float]: """Get (x, y) coordinates in mm for a well position. Args: row: Row letter A-H. col: Column number 1-12. Returns: Tuple of (x_mm, y_mm) from plate origin. """ row_idx = ROWS.index(row.upper()) col_idx = col - 1 x = A1_OFFSET_X + col_idx * WELL_SPACING y = A1_OFFSET_Y + row_idx * WELL_SPACING return (x, y) def get_well(row: str, col: int) -> WellPosition: """Get a WellPosition for the given row and column. Args: row: Row letter A-H. col: Column number 1-12. Returns: WellPosition with coordinates. """ x, y = well_coordinates(row, col) return WellPosition(row=row.upper(), col=col, x_mm=x, y_mm=y) def all_wells() -> list[WellPosition]: """Get all 96 well positions in row-major order (A1, A2, ..., H12).""" return [get_well(row, col) for row in ROWS for col in COLS] def parse_well_name(name: str) -> WellPosition: """Parse a well name like 'A1' or 'H12' into a WellPosition. Args: name: Well name string (e.g., 'A1', 'B12'). Returns: WellPosition with coordinates. Raises: ValueError: If the well name is invalid. """ name = name.strip().upper() if len(name) < 2 or name[0] not in ROWS: raise ValueError(f"Invalid well name: {name}") try: col = int(name[1:]) except ValueError: raise ValueError(f"Invalid well name: {name}") if col < 1 or col > 12: raise ValueError(f"Column out of range: {col}") return get_well(name[0], col) ================================================ FILE: src/biolab/safety.py ================================================ """Safety systems: e-stop, watchdog, joint limits. Ensures arms park safely on connection loss or error conditions. """ from __future__ import annotations import logging import threading import time from collections.abc import Callable from dataclasses import dataclass, field logger = logging.getLogger(__name__) # SO-101 joint limits (degrees) — conservative defaults JOINT_LIMITS = { "shoulder_pan": (-150.0, 150.0), "shoulder_lift": (-90.0, 90.0), "elbow_flex": (-120.0, 120.0), "wrist_flex": (-100.0, 100.0), "wrist_roll": (-150.0, 150.0), "gripper": (-10.0, 70.0), } # Park position: arms folded up safely PARK_POSITION = [0.0, -45.0, -90.0, 0.0, 0.0, 0.0] @dataclass class SafetyConfig: """Safety system configuration.""" watchdog_timeout_s: float = 5.0 heartbeat_interval_s: float = 1.0 joint_limits: dict[str, tuple[float, float]] = field(default_factory=lambda: dict(JOINT_LIMITS)) class SafetyMonitor: """Monitors arm safety and triggers emergency stop on violations. Usage: monitor = SafetyMonitor(config, park_callback=controller.park_all) monitor.start() monitor.heartbeat() # Call regularly from remote client monitor.stop() """ def __init__(self, config: SafetyConfig, park_callback: Callable[[], None]) -> None: self.config = config self._park = park_callback self._last_heartbeat = time.monotonic() self._stopped = False self._e_stopped = False self._watchdog_thread: threading.Thread | None = None def start(self) -> None: """Start the watchdog timer.""" self._stopped = False self._last_heartbeat = time.monotonic() self._watchdog_thread = threading.Thread(target=self._watchdog_loop, daemon=True) self._watchdog_thread.start() logger.info("Safety monitor started (timeout=%.1fs)", self.config.watchdog_timeout_s) def stop(self) -> None: """Stop the watchdog timer.""" self._stopped = True if self._watchdog_thread: self._watchdog_thread.join(timeout=2.0) logger.info("Safety monitor stopped") def heartbeat(self) -> None: """Reset the watchdog timer. Call from remote client at regular intervals.""" self._last_heartbeat = time.monotonic() def e_stop(self) -> None: """Trigger emergency stop — park all arms immediately.""" logger.warning("E-STOP triggered") self._e_stopped = True self._park() def reset_e_stop(self) -> None: """Reset e-stop state (requires explicit operator action).""" self._e_stopped = False logger.info("E-STOP reset") @property def is_e_stopped(self) -> bool: """Whether e-stop is currently active.""" return self._e_stopped def check_joint_limits(self, joint_name: str, value: float) -> bool: """Check if a joint value is within safe limits. Args: joint_name: Name of the joint. value: Joint angle in degrees. Returns: True if within limits, False if violated. """ if joint_name not in self.config.joint_limits: return True lo, hi = self.config.joint_limits[joint_name] if value < lo or value > hi: logger.warning( "Joint limit violation: %s=%.1f (limits: %.1f–%.1f)", joint_name, value, lo, hi ) return False return True def _watchdog_loop(self) -> None: """Background thread checking for heartbeat timeout.""" while not self._stopped: elapsed = time.monotonic() - self._last_heartbeat if elapsed > self.config.watchdog_timeout_s and not self._e_stopped: logger.warning("Watchdog timeout (%.1fs) — parking arms", elapsed) self._park() self._e_stopped = True time.sleep(0.5) ================================================ FILE: src/biolab/tool_changer.py ================================================ """Autonomous tool changing for SO-101 arms. Manages a 3-station magnetic dock where arms can swap end-effectors: - Pipette (digital-pipette-v2) - Gripper jaws (default SO-101 gripper) - Fridge hook (custom 3D-printed) Reference: Berkeley passive modular tool changer (3D-printed, magnetic). """ from __future__ import annotations import logging from dataclasses import dataclass from enum import Enum from typing import Any import yaml logger = logging.getLogger(__name__) class Tool(Enum): """Available end-effector tools.""" NONE = "none" PIPETTE = "pipette" GRIPPER = "gripper" FRIDGE_HOOK = "fridge_hook" @dataclass class DockStation: """A single station on the tool dock.""" tool: Tool approach_joints: list[float] # Joint positions to approach the station engage_joints: list[float] # Joint positions to engage/disengage dock_joints: list[float] # Joint positions when docked @dataclass class ToolDockConfig: """Configuration for the 3-station tool dock.""" stations: dict[str, DockStation] @classmethod def from_yaml(cls, path: str) -> ToolDockConfig: """Load dock configuration from YAML. Args: path: Path to tool_dock.yaml. Returns: ToolDockConfig instance. """ with open(path) as f: data = yaml.safe_load(f) stations = {} for name, station_data in data["stations"].items(): stations[name] = DockStation( tool=Tool(station_data["tool"]), approach_joints=station_data["approach_joints"], engage_joints=station_data["engage_joints"], dock_joints=station_data["dock_joints"], ) return cls(stations=stations) class ToolChanger: """Manages tool changes for an SO-101 arm. Usage: changer = ToolChanger(dock_config, arm_controller, "arm_a") changer.change_tool(Tool.PIPETTE) # ... do pipetting work ... changer.change_tool(Tool.GRIPPER) """ def __init__(self, config: ToolDockConfig, arm_controller: Any, arm_id: str) -> None: self.config = config self.arm = arm_controller self.arm_id = arm_id self.current_tool = Tool.GRIPPER # Default: gripper installed def change_tool(self, target: Tool) -> None: """Change the current end-effector tool. Args: target: The tool to switch to. """ if target == self.current_tool: logger.info("Already equipped with %s", target.value) return if self.current_tool != Tool.NONE: self._return_tool(self.current_tool) if target != Tool.NONE: self._pickup_tool(target) self.current_tool = target logger.info("Tool changed to %s", target.value) def _return_tool(self, tool: Tool) -> None: """Return current tool to its dock station. Args: tool: Tool to return. """ station = self._find_station(tool) logger.info("Returning %s to dock", tool.value) # Approach → engage → open gripper → retract self.arm.send_action(self.arm_id, station.approach_joints) self.arm.send_action(self.arm_id, station.engage_joints) self.arm.send_action(self.arm_id, station.dock_joints) # Open gripper to release tool self.arm.send_action(self.arm_id, station.approach_joints) def _pickup_tool(self, tool: Tool) -> None: """Pick up a tool from its dock station. Args: tool: Tool to pick up. """ station = self._find_station(tool) logger.info("Picking up %s from dock", tool.value) # Approach → engage → close gripper → retract self.arm.send_action(self.arm_id, station.approach_joints) self.arm.send_action(self.arm_id, station.dock_joints) self.arm.send_action(self.arm_id, station.engage_joints) # Close gripper to secure tool self.arm.send_action(self.arm_id, station.approach_joints) def _find_station(self, tool: Tool) -> DockStation: """Find the dock station for a given tool. Args: tool: Tool to find. Returns: DockStation for the tool. Raises: ValueError: If no station found for tool. """ for station in self.config.stations.values(): if station.tool == tool: return station raise ValueError(f"No dock station for tool: {tool.value}") ================================================ FILE: src/biolab/workflow.py ================================================ """Workflow orchestration — E2E use cases composing existing modules. Use cases: - UC1: Pipette a 96-well plate (single, row, column, full) - UC2: Open fridge, grab item, move out - UC3: Tool interchange cycle - UC4: Demo mode (all use cases in sequence) """ from __future__ import annotations import logging from dataclasses import dataclass from pathlib import Path import yaml from biolab.arms import DualArmConfig, DualArmController from biolab.pipette import DigitalPipette, PipetteConfig from biolab.plate import ROWS, all_wells, parse_well_name from biolab.tool_changer import Tool, ToolChanger, ToolDockConfig logger = logging.getLogger(__name__) # Fridge operation joint arrays (stub values — calibrate with real hardware) FRIDGE_APPROACH_JOINTS = [45.0, -20.0, -40.0, 0.0, 0.0, 30.0] FRIDGE_HOOK_ENGAGED_JOINTS = [45.0, -10.0, -20.0, 0.0, 0.0, 30.0] FRIDGE_PULL_JOINTS = [60.0, -10.0, -20.0, 0.0, 0.0, 30.0] FRIDGE_RELEASE_JOINTS = [45.0, -20.0, -40.0, 0.0, 0.0, 0.0] FRIDGE_GRAB_JOINTS = [45.0, -15.0, -30.0, 0.0, 0.0, 30.0] @dataclass(frozen=True) class PlateLayout: """Workspace-frame plate layout loaded from configs/plate_layout.yaml. Transforms plate-local coordinates to arm workspace frame. """ origin_x_mm: float origin_y_mm: float origin_z_mm: float safe_z_mm: float approach_z_mm: float aspirate_z_mm: float dispense_z_mm: float trough_x_mm: float trough_y_mm: float trough_z_mm: float @classmethod def from_yaml(cls, path: str | Path) -> PlateLayout: """Load plate layout from YAML config. Args: path: Path to plate_layout.yaml. Returns: PlateLayout with workspace-frame coordinates. """ with open(path) as f: data = yaml.safe_load(f) plate = data["plate"] heights = data["heights"] trough = data.get("reagent_trough", {}) return cls( origin_x_mm=plate["origin_x_mm"], origin_y_mm=plate["origin_y_mm"], origin_z_mm=plate["origin_z_mm"], safe_z_mm=heights["safe_z_mm"], approach_z_mm=heights["approach_z_mm"], aspirate_z_mm=heights["aspirate_z_mm"], dispense_z_mm=heights["dispense_z_mm"], trough_x_mm=trough.get("origin_x_mm", 200.0), trough_y_mm=trough.get("origin_y_mm", 0.0), trough_z_mm=trough.get("origin_z_mm", 25.0), ) def pipette_well( arm: DualArmController, pipette: DigitalPipette, layout: PlateLayout, arm_id: str, source: str, dest: str, volume_ul: float, ) -> None: """Pipette from source to destination well. Core primitive: move arm to source, aspirate, move to dest, dispense. Args: arm: Arm controller (stub or real). pipette: Digital pipette (stub or real). layout: Plate layout with workspace coordinates. arm_id: Which arm to use. source: Source well name or "TROUGH". dest: Destination well name (e.g., "A1"). volume_ul: Volume to transfer in microliters. Raises: ValueError: If dest is an invalid well name. """ # Validate destination (raises ValueError if invalid) dest_well = parse_well_name(dest) # Move to source if source.upper() == "TROUGH": logger.info( "[UC1] Move %s → TROUGH (%.1f, %.1f mm)", arm_id, layout.trough_x_mm, layout.trough_y_mm, ) arm.send_action(arm_id, [0.0] * 6) # Stub: trough position else: arm.send_to_well(arm_id, source) # Aspirate logger.info("[UC1] Aspirate %.1f µL", volume_ul) pipette.aspirate(volume_ul) # Move to destination arm.send_to_well(arm_id, dest) # Dispense logger.info("[UC1] Dispense %.1f µL → %s", volume_ul, dest_well.name) pipette.dispense(volume_ul) def uc1_single_well( arm: DualArmController, pipette: DigitalPipette, layout: PlateLayout, arm_id: str, dest: str, volume_ul: float, ) -> None: """UC1.1: Pipette single well from trough. Args: arm: Arm controller. pipette: Digital pipette. layout: Plate layout. arm_id: Which arm to use. dest: Destination well name (e.g., "A1"). volume_ul: Volume in microliters. """ pipette_well(arm, pipette, layout, arm_id, "TROUGH", dest, volume_ul) def uc1_row( arm: DualArmController, pipette: DigitalPipette, layout: PlateLayout, arm_id: str, row: str, volume_ul: float, ) -> None: """UC1.2: Pipette entire row (12 wells) from trough. Args: arm: Arm controller. pipette: Digital pipette. layout: Plate layout. arm_id: Which arm to use. row: Row letter (A-H). volume_ul: Volume per well in microliters. Raises: ValueError: If row is not A-H. """ row = row.upper() if row not in ROWS: raise ValueError(f"Invalid row: {row}. Must be A-H.") wells = [w for w in all_wells() if w.row == row] logger.info("[UC1] Pipetting row %s (%d wells, %.1f µL each)", row, len(wells), volume_ul) for well in wells: pipette_well(arm, pipette, layout, arm_id, "TROUGH", well.name, volume_ul) def uc1_col( arm: DualArmController, pipette: DigitalPipette, layout: PlateLayout, arm_id: str, col: int, volume_ul: float, ) -> None: """UC1.2: Pipette entire column (8 wells) from trough. Args: arm: Arm controller. pipette: Digital pipette. layout: Plate layout. arm_id: Which arm to use. col: Column number (1-12). volume_ul: Volume per well in microliters. Raises: ValueError: If col is not 1-12. """ if col < 1 or col > 12: raise ValueError(f"Invalid column: {col}. Must be 1-12.") wells = [w for w in all_wells() if w.col == col] logger.info("[UC1] Pipetting column %d (%d wells, %.1f µL each)", col, len(wells), volume_ul) for well in wells: pipette_well(arm, pipette, layout, arm_id, "TROUGH", well.name, volume_ul) def uc1_full_plate( arm: DualArmController, pipette: DigitalPipette, layout: PlateLayout, arm_id: str, volume_ul: float, ) -> None: """UC1.3: Pipette all 96 wells from trough. Each well gets an individual aspirate/dispense cycle. Args: arm: Arm controller. pipette: Digital pipette. layout: Plate layout. arm_id: Which arm to use. volume_ul: Volume per well in microliters. """ wells = all_wells() logger.info("[UC1] Pipetting full plate (%d wells, %.1f µL each)", len(wells), volume_ul) for well in wells: pipette_well(arm, pipette, layout, arm_id, "TROUGH", well.name, volume_ul) def uc2_fridge_open_grab_move( arm: DualArmController, changer: ToolChanger, arm_id: str, ) -> None: """UC2: Open fridge with hook, swap to gripper, grab item, move out. Sequence: 1. Equip fridge hook 2. Approach fridge → hook door → pull open 3. Swap to gripper 4. Reach into fridge → grab item 5. Park (move item to safe position) Args: arm: Arm controller. changer: Tool changer. arm_id: Which arm to use. """ logger.info("[UC2] Starting fridge sequence") # Step 1: equip fridge hook changer.change_tool(Tool.FRIDGE_HOOK) # Step 2: open fridge door logger.info("[UC2] Approach fridge") arm.send_action(arm_id, FRIDGE_APPROACH_JOINTS) logger.info("[UC2] Engage hook — pull door") arm.send_action(arm_id, FRIDGE_HOOK_ENGAGED_JOINTS) arm.send_action(arm_id, FRIDGE_PULL_JOINTS) logger.info("[UC2] Release hook — door open") arm.send_action(arm_id, FRIDGE_RELEASE_JOINTS) # Step 3: swap to gripper changer.change_tool(Tool.GRIPPER) # Step 4: grab item logger.info("[UC2] Grab item from fridge") arm.send_action(arm_id, FRIDGE_GRAB_JOINTS) # Step 5: move to safe position arm.park_all() logger.info("[UC2] Fridge sequence complete — item at park position") def uc3_tool_cycle( arm: DualArmController, changer: ToolChanger, arm_id: str, sequence: list[Tool] | None = None, ) -> None: """UC3: Cycle through tools to demonstrate interchange capability. Args: arm: Arm controller. changer: Tool changer. arm_id: Which arm to use. sequence: Tool sequence to follow. Default: PIPETTE → GRIPPER → FRIDGE_HOOK → GRIPPER. """ if sequence is None: sequence = [Tool.PIPETTE, Tool.GRIPPER, Tool.FRIDGE_HOOK, Tool.GRIPPER] logger.info("[UC3] Starting tool cycle: %s", [t.value for t in sequence]) for tool in sequence: changer.change_tool(tool) logger.info("[UC3] Equipped %s", tool.value) def uc4_demo_all( arm: DualArmController, pipette: DigitalPipette, changer: ToolChanger, layout: PlateLayout, arm_id: str = "arm_a", ) -> None: """UC4: Run all use cases in sequence (demo mode). Runs: UC1.1 → UC1.2 (row A) → UC1.2 (col 1) → UC2 → UC3 → park. Full plate is skipped in demo (too verbose); use uc1_full_plate directly. Args: arm: Arm controller. pipette: Digital pipette. changer: Tool changer. layout: Plate layout. arm_id: Which arm to use. """ # Ensure pipette is equipped for UC1 changer.change_tool(Tool.PIPETTE) logger.info("=== UC1.1: Single well (A1, 50 µL) ===") uc1_single_well(arm, pipette, layout, arm_id, "A1", 50.0) logger.info("=== UC1.2: Row A (25 µL each) ===") uc1_row(arm, pipette, layout, arm_id, "A", 25.0) logger.info("=== UC1.2: Column 1 (20 µL each) ===") uc1_col(arm, pipette, layout, arm_id, 1, 20.0) logger.info("=== UC2: Fridge open-grab-move ===") uc2_fridge_open_grab_move(arm, changer, arm_id) logger.info("=== UC3: Tool cycle ===") uc3_tool_cycle(arm, changer, arm_id) arm.park_all() logger.info("=== Demo complete ===") def create_workflow_context( arm_config_path: str = "configs/arms.yaml", dock_config_path: str = "configs/tool_dock.yaml", layout_path: str = "configs/plate_layout.yaml", arm_id: str = "arm_a", ) -> tuple[DualArmController, DigitalPipette, ToolChanger, PlateLayout]: """Wire up all modules from config files. Returns connected controller, stub pipette, tool changer, and plate layout. All components work in stub mode when hardware is unavailable. Args: arm_config_path: Path to arms YAML config. dock_config_path: Path to tool dock YAML config. layout_path: Path to plate layout YAML config. arm_id: Default arm ID for tool changer. Returns: Tuple of (arm_controller, pipette, tool_changer, plate_layout). """ layout = PlateLayout.from_yaml(layout_path) arm_config = DualArmConfig.from_yaml(arm_config_path) arm = DualArmController(arm_config) arm.connect() pipette = DigitalPipette(PipetteConfig()) pipette.connect() dock_config = ToolDockConfig.from_yaml(dock_config_path) changer = ToolChanger(dock_config, arm, arm_id) logger.info("Workflow context created (stub=%s)", arm._stub_mode) return arm, pipette, changer, layout ================================================ FILE: src/dashboard/__init__.py ================================================ """Remote oversight dashboard for so101-biolab-automation.""" ================================================ FILE: src/dashboard/server.py ================================================ """FastAPI server for remote oversight dashboard. Provides: - WebSocket command channel (pause/resume, e-stop, well targeting, heartbeat, run_workflow) - REST API for arm telemetry and status """ from __future__ import annotations import json import logging import threading from collections.abc import AsyncGenerator from contextlib import asynccontextmanager from typing import Any from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.responses import HTMLResponse from biolab.arms import DualArmConfig, DualArmController from biolab.pipette import DigitalPipette, PipetteConfig from biolab.safety import SafetyConfig, SafetyMonitor from biolab.tool_changer import ToolChanger, ToolDockConfig from biolab.workflow import PlateLayout, uc4_demo_all logger = logging.getLogger(__name__) _mode = "idle" @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: """Create all components on startup, cleanup on shutdown.""" arm_config = DualArmConfig.from_yaml("configs/arms.yaml") controller = DualArmController(arm_config) controller.connect() monitor = SafetyMonitor(SafetyConfig(), park_callback=controller.park_all) monitor.start() dock_config = ToolDockConfig.from_yaml("configs/tool_dock.yaml") changer = ToolChanger(dock_config, controller, "arm_a") pipette = DigitalPipette(PipetteConfig()) pipette.connect() layout = PlateLayout.from_yaml("configs/plate_layout.yaml") app.state.controller = controller app.state.monitor = monitor app.state.changer = changer app.state.pipette = pipette app.state.layout = layout logger.info("Dashboard started — all components active") yield monitor.stop() pipette.disconnect() controller.disconnect() logger.info("Dashboard shutdown") app = FastAPI(title="so101-biolab-automation Dashboard", lifespan=lifespan) @app.get("/", response_class=HTMLResponse) async def index() -> str: """Serve the dashboard page.""" return """ so101-biolab-automation

so101-biolab-automation Dashboard

Camera feeds and controls will be rendered here.

""" @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket) -> None: """WebSocket command channel for remote operator.""" global _mode # noqa: PLW0603 await websocket.accept() logger.info("Remote operator connected") controller: DualArmController = websocket.app.state.controller monitor: SafetyMonitor = websocket.app.state.monitor try: while True: data = await websocket.receive_text() msg = json.loads(data) command = msg.get("command", "") if command == "e_stop": monitor.e_stop() _mode = "e_stopped" elif command == "heartbeat": monitor.heartbeat() elif command == "pause": _mode = "paused" elif command == "resume": _mode = "autonomous" monitor.reset_e_stop() elif command == "target_well": well = msg.get("well", "A1") controller.send_to_well("arm_a", well) elif command == "run_workflow": _mode = "running" threading.Thread(target=_run_workflow, args=(websocket.app,), daemon=True).start() await websocket.send_text(json.dumps({"status": _get_status(controller, monitor)})) except WebSocketDisconnect: logger.info("Remote operator disconnected") @app.get("/api/status") async def get_status() -> dict[str, Any]: """Get current system status.""" return _get_status(app.state.controller, app.state.monitor) def _get_status(controller: DualArmController, monitor: SafetyMonitor) -> dict[str, Any]: """Build status dict from real controller and monitor state.""" return { "mode": _mode, "e_stopped": monitor.is_e_stopped, "connected": controller._connected, "arm_ids": controller.arm_ids, } def _run_workflow(app: FastAPI) -> None: """Run full demo workflow in background thread.""" global _mode # noqa: PLW0603 try: uc4_demo_all( app.state.controller, app.state.pipette, app.state.changer, app.state.layout, "arm_a", ) _mode = "idle" except Exception: logger.exception("Workflow failed") _mode = "error" ================================================ FILE: tests/conftest.py ================================================ """Pytest configuration for so101-biolab-automation. Import resolution handled by pyproject.toml: pythonpath = ["src"] """ ================================================ FILE: tests/test_arms.py ================================================ """Tests for dual arm controller — must work without LeRobot hardware.""" import pytest from biolab.arms import ArmConfig, DualArmConfig, DualArmController @pytest.fixture def stub_config() -> DualArmConfig: """Create a config for stub-mode testing.""" return DualArmConfig( arm_a=ArmConfig(arm_id="arm_a", port="/dev/null", role="follower"), arm_b=ArmConfig(arm_id="arm_b", port="/dev/null", role="follower"), leader=ArmConfig(arm_id="leader", port="/dev/null", role="leader"), ) @pytest.fixture def connected_stub(stub_config: DualArmConfig) -> DualArmController: """Create a connected stub controller.""" ctrl = DualArmController(stub_config) ctrl.connect() return ctrl class TestDualArmController: """Test DualArmController in stub mode (no LeRobot).""" def test_connect_stub_mode(self, stub_config: DualArmConfig) -> None: """connect() succeeds in stub mode without LeRobot.""" ctrl = DualArmController(stub_config) ctrl.connect() assert ctrl._connected is True def test_disconnect_stub_mode(self, connected_stub: DualArmController) -> None: """disconnect() works in stub mode.""" connected_stub.disconnect() assert connected_stub._connected is False def test_get_observation_stub(self, connected_stub: DualArmController) -> None: """get_observation returns stub data when no real robots connected.""" obs = connected_stub.get_observation("arm_a") assert obs["stub"] is True assert "joints" in obs def test_send_action_stub(self, connected_stub: DualArmController) -> None: """send_action is a no-op in stub mode (no crash).""" connected_stub.send_action("arm_a", [0.0] * 6) def test_send_to_well(self, connected_stub: DualArmController) -> None: """send_to_well parses well name and calls send_action.""" connected_stub.send_to_well("arm_a", "A1") def test_send_to_well_invalid(self, connected_stub: DualArmController) -> None: """send_to_well raises ValueError for invalid well name.""" with pytest.raises(ValueError): connected_stub.send_to_well("arm_a", "Z99") def test_park_all(self, connected_stub: DualArmController) -> None: """park_all sends park position to both arms.""" connected_stub.park_all() def test_get_observation_unknown_arm(self, connected_stub: DualArmController) -> None: """get_observation raises ValueError for unknown arm ID.""" with pytest.raises(ValueError, match="Unknown arm"): connected_stub.get_observation("nonexistent") ================================================ FILE: tests/test_camera.py ================================================ """Tests for camera pipeline — must work without cv2/camera hardware.""" from biolab.camera import CameraConfig, CameraPipeline class TestCameraPipeline: """Test CameraPipeline in headless environment (no cv2, no cameras).""" def test_import_without_cv2(self) -> None: """CameraPipeline can be imported even if cv2 is unavailable.""" pipeline = CameraPipeline([]) assert pipeline is not None def test_instantiate_with_configs(self) -> None: """CameraPipeline stores camera configs by name.""" configs = [CameraConfig("overhead", 0), CameraConfig("wrist", 2)] pipeline = CameraPipeline(configs) assert "overhead" in pipeline.cameras assert "wrist" in pipeline.cameras def test_start_without_cv2(self) -> None: """start() gracefully handles missing cv2.""" pipeline = CameraPipeline([CameraConfig("test", 0)]) # cv2 may or may not be installed — either way, start should not crash pipeline.start() pipeline.stop() def test_get_frame_closed_camera(self) -> None: """get_frame returns None for a camera that was never opened.""" pipeline = CameraPipeline([CameraConfig("test", 0)]) assert pipeline.get_frame("test") is None def test_get_frame_unknown_camera(self) -> None: """get_frame returns None for an unknown camera name.""" pipeline = CameraPipeline([]) assert pipeline.get_frame("nonexistent") is None def test_get_frames_empty(self) -> None: """get_frames returns empty dict when no cameras are open.""" pipeline = CameraPipeline([CameraConfig("test", 0)]) assert pipeline.get_frames() == {} def test_stop_idempotent(self) -> None: """stop() can be called multiple times without error.""" pipeline = CameraPipeline([CameraConfig("test", 0)]) pipeline.stop() pipeline.stop() ================================================ FILE: tests/test_config_loading.py ================================================ """Integration tests for config file loading.""" import pytest import yaml from biolab.arms import DualArmConfig from biolab.tool_changer import ToolDockConfig @pytest.mark.integration class TestConfigLoading: """Verify real YAML config files parse correctly.""" def test_arms_yaml_loads(self) -> None: """configs/arms.yaml loads into DualArmConfig.""" config = DualArmConfig.from_yaml("configs/arms.yaml") assert config.arm_a.arm_id == "arm_a" assert config.arm_b.arm_id == "arm_b" assert config.arm_a.role == "follower" assert config.leader is not None assert config.leader.role == "leader" def test_tool_dock_yaml_loads(self) -> None: """configs/tool_dock.yaml loads into ToolDockConfig.""" config = ToolDockConfig.from_yaml("configs/tool_dock.yaml") assert "pipette" in config.stations assert "gripper" in config.stations assert "fridge_hook" in config.stations def test_plate_layout_yaml_loads(self) -> None: """configs/plate_layout.yaml is valid YAML with expected keys.""" with open("configs/plate_layout.yaml") as f: data = yaml.safe_load(f) assert "plate" in data assert "tip_rack" in data assert "heights" in data assert data["plate"]["well_spacing_mm"] == 9.0 def test_arms_yaml_cameras_structure(self) -> None: """Camera config in arms.yaml has expected fields.""" config = DualArmConfig.from_yaml("configs/arms.yaml") assert "wrist" in config.arm_a.cameras cam = config.arm_a.cameras["wrist"] assert cam["type"] == "opencv" ================================================ FILE: tests/test_coordinate_cmd.py ================================================ """Tests for coordinate_cmd.py script.""" from __future__ import annotations import subprocess import sys class TestCoordinateCmd: """Test coordinate command script via subprocess.""" def _run(self, *args: str) -> subprocess.CompletedProcess[str]: return subprocess.run( [sys.executable, "scripts/coordinate_cmd.py", *args], capture_output=True, text=True, timeout=10, ) def test_well_command(self) -> None: """--well A1 exits 0 and logs coordinates.""" result = self._run("--well", "A1") assert result.returncode == 0 assert "A1" in result.stderr # logging goes to stderr def test_park_command(self) -> None: """--park exits 0.""" result = self._run("--park") assert result.returncode == 0 assert "Parking" in result.stderr def test_missing_well_and_park(self) -> None: """No --well and no --park exits non-zero.""" result = self._run() assert result.returncode != 0 def test_invalid_well(self) -> None: """--well Z99 exits non-zero (invalid well name).""" result = self._run("--well", "Z99") assert result.returncode != 0 ================================================ FILE: tests/test_dashboard.py ================================================ """Tests for dashboard server — FastAPI integration.""" from __future__ import annotations import json from starlette.testclient import TestClient from dashboard.server import app class TestDashboardEndpoints: """Test REST endpoints.""" def test_index_returns_html(self) -> None: """GET / returns HTML dashboard page.""" with TestClient(app) as client: resp = client.get("/") assert resp.status_code == 200 assert "so101-biolab-automation" in resp.text def test_status_endpoint(self) -> None: """GET /api/status returns dict with expected keys.""" with TestClient(app) as client: resp = client.get("/api/status") assert resp.status_code == 200 data = resp.json() assert "mode" in data assert "e_stopped" in data assert "connected" in data assert data["connected"] is True def test_status_shows_arm_ids(self) -> None: """GET /api/status includes arm IDs.""" with TestClient(app) as client: data = client.get("/api/status").json() assert "arm_ids" in data assert "arm_a" in data["arm_ids"] assert "arm_b" in data["arm_ids"] class TestDashboardWebSocket: """Test WebSocket command channel.""" def test_e_stop_websocket(self) -> None: """e_stop command sets e_stopped state.""" with TestClient(app) as client: with client.websocket_connect("/ws") as ws: ws.send_text(json.dumps({"command": "e_stop"})) resp = json.loads(ws.receive_text()) assert resp["status"]["e_stopped"] is True assert resp["status"]["mode"] == "e_stopped" def test_heartbeat_websocket(self) -> None: """heartbeat command succeeds without error.""" with TestClient(app) as client: with client.websocket_connect("/ws") as ws: ws.send_text(json.dumps({"command": "heartbeat"})) resp = json.loads(ws.receive_text()) assert "status" in resp def test_target_well_websocket(self) -> None: """target_well command returns status.""" with TestClient(app) as client: with client.websocket_connect("/ws") as ws: ws.send_text(json.dumps({"command": "target_well", "well": "A1"})) resp = json.loads(ws.receive_text()) assert "status" in resp ================================================ FILE: tests/test_pipette.py ================================================ """Tests for DigitalPipette over-aspiration guard and basic operations.""" import pytest from biolab.pipette import DigitalPipette, PipetteConfig @pytest.fixture def pipette() -> DigitalPipette: """Return a pipette in stub mode (no serial hardware).""" p = DigitalPipette(PipetteConfig(serial_port="/dev/ttyUSB_MISSING", max_volume_ul=200.0)) p.connect() return p class TestPipetteAspirate: """Tests for aspirate guard logic.""" def test_aspirate_within_capacity(self, pipette: DigitalPipette) -> None: pipette.aspirate(100.0) assert pipette._current_fill == pytest.approx(100.0) def test_aspirate_exceeds_capacity(self, pipette: DigitalPipette) -> None: with pytest.raises(ValueError, match="exceeds max"): pipette.aspirate(250.0) def test_cumulative_aspirate_exceeds(self, pipette: DigitalPipette) -> None: pipette.aspirate(150.0) with pytest.raises(ValueError, match="exceed"): pipette.aspirate(100.0) # 150 + 100 = 250 > 200 class TestPipetteDispense: """Tests for dispense guard logic.""" def test_dispense_within_fill(self, pipette: DigitalPipette) -> None: pipette.aspirate(100.0) pipette.dispense(50.0) assert pipette._current_fill == pytest.approx(50.0) def test_dispense_exceeds_fill(self, pipette: DigitalPipette) -> None: pipette.aspirate(50.0) with pytest.raises(ValueError, match="exceed"): pipette.dispense(100.0) class TestPipetteConnect: """Tests for connection and stub mode.""" def test_connect_stub_mode(self) -> None: """connect() must not crash when the serial port does not exist.""" p = DigitalPipette(PipetteConfig(serial_port="/dev/ttyUSB_MISSING")) p.connect() # should not raise; falls through to stub mode class TestVolumeToSteps: """Tests for _volume_to_steps boundary values.""" def test_volume_to_steps_zero(self, pipette: DigitalPipette) -> None: assert pipette._volume_to_steps(0.0) == 0 def test_volume_to_steps_max(self, pipette: DigitalPipette) -> None: steps = pipette._volume_to_steps(pipette.config.max_volume_ul) assert steps == 1023 ================================================ FILE: tests/test_plate_coords.py ================================================ """Tests for 96-well plate coordinate calculations.""" import pytest from biolab.plate import ( A1_OFFSET_X, A1_OFFSET_Y, WELL_SPACING, all_wells, get_well, parse_well_name, well_coordinates, ) class TestWellCoordinates: """Test well coordinate calculations against SBS standard.""" def test_a1_coordinates(self) -> None: x, y = well_coordinates("A", 1) assert x == pytest.approx(A1_OFFSET_X) assert y == pytest.approx(A1_OFFSET_Y) def test_a12_coordinates(self) -> None: x, y = well_coordinates("A", 12) assert x == pytest.approx(A1_OFFSET_X + 11 * WELL_SPACING) assert y == pytest.approx(A1_OFFSET_Y) def test_h1_coordinates(self) -> None: x, y = well_coordinates("H", 1) assert x == pytest.approx(A1_OFFSET_X) assert y == pytest.approx(A1_OFFSET_Y + 7 * WELL_SPACING) def test_h12_coordinates(self) -> None: x, y = well_coordinates("H", 12) assert x == pytest.approx(A1_OFFSET_X + 11 * WELL_SPACING) assert y == pytest.approx(A1_OFFSET_Y + 7 * WELL_SPACING) def test_spacing_between_adjacent_wells(self) -> None: x1, y1 = well_coordinates("A", 1) x2, y2 = well_coordinates("A", 2) assert x2 - x1 == pytest.approx(WELL_SPACING) assert y2 == pytest.approx(y1) def test_spacing_between_rows(self) -> None: x1, y1 = well_coordinates("A", 1) x2, y2 = well_coordinates("B", 1) assert x2 == pytest.approx(x1) assert y2 - y1 == pytest.approx(WELL_SPACING) class TestGetWell: """Test WellPosition construction.""" def test_well_name(self) -> None: well = get_well("A", 1) assert well.name == "A1" def test_well_coordinates_match(self) -> None: well = get_well("D", 6) x, y = well_coordinates("D", 6) assert well.x_mm == pytest.approx(x) assert well.y_mm == pytest.approx(y) def test_lowercase_row(self) -> None: well = get_well("a", 1) assert well.row == "A" class TestAllWells: """Test full plate enumeration.""" def test_count(self) -> None: wells = all_wells() assert len(wells) == 96 def test_first_well(self) -> None: wells = all_wells() assert wells[0].name == "A1" def test_last_well(self) -> None: wells = all_wells() assert wells[-1].name == "H12" class TestParseWellName: """Test well name parsing.""" def test_simple(self) -> None: well = parse_well_name("A1") assert well.row == "A" assert well.col == 1 def test_two_digit_column(self) -> None: well = parse_well_name("H12") assert well.row == "H" assert well.col == 12 def test_lowercase(self) -> None: well = parse_well_name("b3") assert well.row == "B" assert well.col == 3 def test_whitespace(self) -> None: well = parse_well_name(" C5 ") assert well.name == "C5" def test_invalid_row(self) -> None: with pytest.raises(ValueError): parse_well_name("Z1") def test_invalid_column(self) -> None: with pytest.raises(ValueError): parse_well_name("A13") def test_empty(self) -> None: with pytest.raises(ValueError): parse_well_name("") ================================================ FILE: tests/test_run_demo.py ================================================ """Tests for run_demo.py script.""" from __future__ import annotations import subprocess import sys class TestRunDemo: """Test demo orchestrator script via subprocess.""" def _run(self, *args: str) -> subprocess.CompletedProcess[str]: return subprocess.run( [sys.executable, "scripts/run_demo.py", *args], capture_output=True, text=True, timeout=10, ) def test_demo_full_mode(self) -> None: """--mode full exits 0 in stub mode.""" result = self._run("--mode", "full") assert result.returncode == 0 assert "Demo complete" in result.stderr def test_demo_eval_mode(self) -> None: """--mode eval exits 0 in stub mode.""" result = self._run("--mode", "eval") assert result.returncode == 0 assert "Demo complete" in result.stderr ================================================ FILE: tests/test_safety.py ================================================ """Tests for safety monitoring.""" import time from biolab.safety import SafetyConfig, SafetyMonitor class TestSafetyMonitor: """Test safety monitor behavior.""" def test_e_stop_calls_park(self) -> None: parked = [] monitor = SafetyMonitor(SafetyConfig(), park_callback=lambda: parked.append(True)) monitor.e_stop() assert len(parked) == 1 assert monitor.is_e_stopped def test_reset_e_stop(self) -> None: monitor = SafetyMonitor(SafetyConfig(), park_callback=lambda: None) monitor.e_stop() assert monitor.is_e_stopped monitor.reset_e_stop() assert not monitor.is_e_stopped def test_joint_limits_within(self) -> None: monitor = SafetyMonitor(SafetyConfig(), park_callback=lambda: None) assert monitor.check_joint_limits("shoulder_pan", 0.0) is True def test_joint_limits_exceeded(self) -> None: monitor = SafetyMonitor(SafetyConfig(), park_callback=lambda: None) assert monitor.check_joint_limits("shoulder_pan", 200.0) is False def test_joint_limits_unknown_joint(self) -> None: monitor = SafetyMonitor(SafetyConfig(), park_callback=lambda: None) assert monitor.check_joint_limits("unknown_joint", 999.0) is True def test_watchdog_triggers_park_on_timeout(self) -> None: parked = [] config = SafetyConfig(watchdog_timeout_s=0.5) monitor = SafetyMonitor(config, park_callback=lambda: parked.append(True)) monitor.start() time.sleep(1.5) monitor.stop() assert len(parked) >= 1 def test_heartbeat_prevents_timeout(self) -> None: parked = [] config = SafetyConfig(watchdog_timeout_s=1.0) monitor = SafetyMonitor(config, park_callback=lambda: parked.append(True)) monitor.start() for _ in range(5): monitor.heartbeat() time.sleep(0.3) monitor.stop() assert len(parked) == 0 ================================================ FILE: tests/test_scad_svg.py ================================================ """Tests for CAD SVG generation quality (CadQuery primary, OpenSCAD fallback). Verifies that SVGs are isometric projections rather than flat top-down outlines. Flat projection of a cylinder yields a circle (few segments); isometric projection yields an ellipse with rich path data. Part lists are driven by hardware/parts.json manifest. """ from __future__ import annotations import json import re from pathlib import Path import pytest HARDWARE_DIR = Path(__file__).resolve().parent.parent / "hardware" SVG_DIR = HARDWARE_DIR / "svg" MANIFEST = json.loads((HARDWARE_DIR / "parts.json").read_text()) # Derive part lists from manifest "shape" field COMPLEX_PARTS = [p["svg"].removesuffix(".svg") for p in MANIFEST if p["shape"] == "complex"] BOX_PARTS = [p["svg"].removesuffix(".svg") for p in MANIFEST if p["shape"] == "box"] def _count_path_points(svg_text: str) -> int: """Count coordinate points across all elements in SVG.""" # Match M/L coordinate pairs in path d="" attributes d_attrs = re.findall(r'd="([^"]*)"', svg_text) total = 0 for d in d_attrs: # Count M, L, and implicit coordinate pairs points = re.findall(r"[-\d.]+,[-\d.]+", d) total += len(points) return total class TestSvgSpatialQuality: """SVGs must show 3D spatial detail, not flat outlines.""" @pytest.mark.parametrize("part", COMPLEX_PARTS) def test_complex_part_has_spatial_detail(self, part: str) -> None: """Isometric SVG of complex parts must have >4 path points. A flat top-down projection of a cylinder is a circle (~4 points in simplified form). An isometric projection shows the full 3D silhouette with many more points. """ svg_path = SVG_DIR / f"{part}.svg" assert svg_path.exists(), f"SVG not found: {svg_path}" svg_text = svg_path.read_text() points = _count_path_points(svg_text) assert points > 4, ( f"{part}.svg has only {points} path points — " f"looks like a flat projection, expected isometric (>4 points)" ) @pytest.mark.parametrize("part", COMPLEX_PARTS + BOX_PARTS) def test_svg_is_valid(self, part: str) -> None: """SVG must contain basic structure.""" svg_path = SVG_DIR / f"{part}.svg" svg_text = svg_path.read_text() assert " None: """Box-shaped parts must show multiple edges (wireframe), not just an outline. CadQuery SVG export renders visible edges with hidden-line removal, producing lines/paths for each visible face edge. A box has >=7 visible edges in isometric view. A flat silhouette has only 4 points. """ svg_path = SVG_DIR / f"{part}.svg" svg_text = svg_path.read_text() # Count distinct drawing elements (line, path, polyline) lines = len(re.findall(r" 1 or _count_path_points(svg_text) > 6, ( f"{part}.svg has only {total_edges} drawing elements with " f"{_count_path_points(svg_text)} path points — " f"needs wireframe rendering (CadQuery SVG), not silhouette" ) def test_all_stl_parts_have_svgs(self) -> None: """Every STL must have a corresponding SVG.""" stl_dir = SVG_DIR.parent / "stl" stls = {p.stem for p in stl_dir.glob("*.stl")} svgs = {p.stem for p in SVG_DIR.glob("*.svg") if p.stem != "system_overview"} missing = stls - svgs assert not missing, f"STLs without SVGs: {missing}" class TestManifest: """Validate hardware/parts.json against files on disk.""" def test_manifest_scad_files_exist(self) -> None: scad_files = {p["scad"] for p in MANIFEST} for scad in scad_files: path = HARDWARE_DIR / scad assert path.exists(), f"Manifest references missing .scad: {scad}" def test_manifest_cad_files_exist(self) -> None: cad_files = {p["cad"] for p in MANIFEST} for cad in cad_files: path = HARDWARE_DIR / cad assert path.exists(), f"Manifest references missing .py: {cad}" def test_manifest_has_required_fields(self) -> None: required = {"name", "stl", "svg", "cad", "build_func", "scad", "shape"} for part in MANIFEST: missing = required - set(part.keys()) assert not missing, f"{part['name']} missing fields: {missing}" def test_manifest_shapes_are_valid(self) -> None: valid = {"complex", "box"} for part in MANIFEST: assert part["shape"] in valid, f"{part['name']} has invalid shape: {part['shape']}" ================================================ FILE: tests/test_slicer_validate.py ================================================ """Tests for hardware/slicer/validate.py — mocked subprocess, no slicer required.""" from __future__ import annotations import importlib import subprocess import sys from pathlib import Path from unittest.mock import patch import pytest # Import module under test (not a package, lives outside src/) sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "hardware" / "slicer")) validate = importlib.import_module("validate") sys.path.pop(0) @pytest.fixture def tmp_stl(tmp_path: Path) -> Path: """Create a minimal dummy STL file.""" stl = tmp_path / "test_part.stl" stl.write_text("solid test\nendsolid test\n") return stl @pytest.fixture def profile_path() -> Path: return validate.PROFILE_DIR / "pla_plus_02mm.ini" class TestGetProfile: def test_default_is_pla(self) -> None: profile = validate.get_profile("plate_holder.stl") assert "pla" in profile.name.lower() def test_tpu_for_gripper_tips(self) -> None: profile = validate.get_profile("gripper_tips_tpu.stl") assert "tpu" in profile.name.lower() def test_override_tpu(self) -> None: profile = validate.get_profile("plate_holder.stl", override="tpu") assert "tpu" in profile.name.lower() class TestValidateStl: @patch("subprocess.run") def test_pass_on_clean_slice( self, mock_run, tmp_stl: Path, profile_path: Path ) -> None: mock_run.return_value = subprocess.CompletedProcess( args=[], returncode=0, stdout="Slicing done\n", stderr="" ) result = validate.validate_stl(tmp_stl, profile_path) assert result["status"] == "PASS" assert result["warnings"] == [] @patch("subprocess.run") def test_warn_on_overhang( self, mock_run, tmp_stl: Path, profile_path: Path ) -> None: mock_run.return_value = subprocess.CompletedProcess( args=[], returncode=0, stdout="Warning: overhang detected at layer 5\n", stderr="", ) result = validate.validate_stl(tmp_stl, profile_path) assert result["status"] == "WARN" assert "overhang" in result["warnings"] @patch("subprocess.run") def test_fail_on_nonzero_exit( self, mock_run, tmp_stl: Path, profile_path: Path ) -> None: mock_run.return_value = subprocess.CompletedProcess( args=[], returncode=1, stdout="", stderr="Error: invalid mesh" ) result = validate.validate_stl(tmp_stl, profile_path) assert result["status"] == "FAIL" assert result["error"] is not None @patch("subprocess.run", side_effect=subprocess.TimeoutExpired(cmd="", timeout=120)) def test_skip_on_timeout( self, mock_run, tmp_stl: Path, profile_path: Path ) -> None: result = validate.validate_stl(tmp_stl, profile_path) assert result["status"] == "SKIP" assert "Timeout" in result["error"] @patch("subprocess.run", side_effect=FileNotFoundError) def test_skip_on_missing_binary( self, mock_run, tmp_stl: Path, profile_path: Path ) -> None: result = validate.validate_stl(tmp_stl, profile_path) assert result["status"] == "SKIP" @patch("subprocess.run") def test_detects_multiple_keywords( self, mock_run, tmp_stl: Path, profile_path: Path ) -> None: mock_run.return_value = subprocess.CompletedProcess( args=[], returncode=0, stdout="overhang at layer 3, bridge detected\n", stderr="unsupported area found", ) result = validate.validate_stl(tmp_stl, profile_path) assert result["status"] == "WARN" assert "overhang" in result["warnings"] assert "bridge" in result["warnings"] assert "unsupported" in result["warnings"] class TestPrintReport: def test_all_pass(self, capsys) -> None: results = [ {"file": "a.stl", "profile": "pla", "warnings": [], "status": "PASS", "error": None} ] code = validate.print_report(results) assert code == 0 assert "1/1" in capsys.readouterr().out def test_fail_returns_nonzero(self) -> None: results = [ {"file": "a.stl", "profile": "pla", "warnings": [], "status": "FAIL", "error": "bad"} ] code = validate.print_report(results) assert code == 1 def test_empty_results(self) -> None: code = validate.print_report([]) assert code == 1 ================================================ FILE: tests/test_tool_changer.py ================================================ """Tests for tool changer logic.""" import pytest from biolab.tool_changer import DockStation, Tool, ToolChanger, ToolDockConfig @pytest.fixture def dock_config() -> ToolDockConfig: """Create a test dock configuration.""" return ToolDockConfig( stations={ "pipette": DockStation( tool=Tool.PIPETTE, approach_joints=[0.0] * 6, engage_joints=[1.0] * 6, dock_joints=[2.0] * 6, ), "gripper": DockStation( tool=Tool.GRIPPER, approach_joints=[10.0] * 6, engage_joints=[11.0] * 6, dock_joints=[12.0] * 6, ), } ) class StubArmController: """Records actions sent to the arm.""" def __init__(self) -> None: self.actions: list[tuple[str, list[float]]] = [] def send_action(self, arm_id: str, action: list[float]) -> None: self.actions.append((arm_id, action)) class TestToolChanger: """Test tool change sequences.""" def test_change_to_same_tool_is_noop(self, dock_config: ToolDockConfig) -> None: stub = StubArmController() changer = ToolChanger(dock_config, stub, "arm_a") changer.current_tool = Tool.GRIPPER changer.change_tool(Tool.GRIPPER) assert len(stub.actions) == 0 def test_change_tool_returns_current_and_picks_new(self, dock_config: ToolDockConfig) -> None: stub = StubArmController() changer = ToolChanger(dock_config, stub, "arm_a") changer.current_tool = Tool.GRIPPER changer.change_tool(Tool.PIPETTE) assert changer.current_tool == Tool.PIPETTE # Return gripper (4 moves) + pickup pipette (4 moves) = 8 actions assert len(stub.actions) == 8 def test_change_to_none_only_returns(self, dock_config: ToolDockConfig) -> None: stub = StubArmController() changer = ToolChanger(dock_config, stub, "arm_a") changer.current_tool = Tool.GRIPPER changer.change_tool(Tool.NONE) assert changer.current_tool == Tool.NONE assert len(stub.actions) == 4 # Return only def test_missing_station_raises(self, dock_config: ToolDockConfig) -> None: stub = StubArmController() changer = ToolChanger(dock_config, stub, "arm_a") changer.current_tool = Tool.NONE with pytest.raises(ValueError, match="fridge_hook"): changer.change_tool(Tool.FRIDGE_HOOK) ================================================ FILE: tests/test_workflow.py ================================================ """Tests for workflow orchestration — E2E use cases in stub mode. TDD: These tests define the expected behavior of workflow.py. All tests work without hardware (stub mode). """ from __future__ import annotations import pytest from biolab.arms import ArmConfig, DualArmConfig, DualArmController from biolab.pipette import DigitalPipette, PipetteConfig from biolab.tool_changer import Tool, ToolChanger, ToolDockConfig from biolab.workflow import ( PlateLayout, create_workflow_context, pipette_well, uc1_col, uc1_full_plate, uc1_row, uc1_single_well, uc2_fridge_open_grab_move, uc3_tool_cycle, uc4_demo_all, ) @pytest.fixture def stub_controller() -> DualArmController: """Connected controller in stub mode (no hardware).""" config = DualArmConfig( arm_a=ArmConfig(arm_id="arm_a", port="/dev/null", role="follower"), arm_b=ArmConfig(arm_id="arm_b", port="/dev/null", role="follower"), ) ctrl = DualArmController(config) ctrl.connect() return ctrl @pytest.fixture def stub_pipette() -> DigitalPipette: """Pipette in stub mode (no serial).""" p = DigitalPipette(PipetteConfig()) p.connect() return p @pytest.fixture def dock_config() -> ToolDockConfig: """Real dock config from YAML.""" return ToolDockConfig.from_yaml("configs/tool_dock.yaml") @pytest.fixture def changer(dock_config: ToolDockConfig, stub_controller: DualArmController) -> ToolChanger: """Tool changer wired to stub controller.""" return ToolChanger(dock_config, stub_controller, "arm_a") @pytest.fixture def layout() -> PlateLayout: """Real plate layout from YAML.""" return PlateLayout.from_yaml("configs/plate_layout.yaml") class TestPlateLayout: """Test PlateLayout config loading.""" @pytest.mark.integration def test_loads_from_yaml(self, layout: PlateLayout) -> None: """PlateLayout loads from configs/plate_layout.yaml.""" assert layout.origin_x_mm == 150.0 @pytest.mark.integration def test_workspace_origin(self, layout: PlateLayout) -> None: """Workspace origin matches config values.""" assert layout.origin_y_mm == -50.0 assert layout.origin_z_mm == 20.0 @pytest.mark.integration def test_heights(self, layout: PlateLayout) -> None: """Height values match config.""" assert layout.safe_z_mm == 80.0 assert layout.approach_z_mm == 40.0 assert layout.aspirate_z_mm == 15.0 assert layout.dispense_z_mm == 25.0 class TestUC1SingleWell: """Test single-well pipetting.""" def test_single_well_completes( self, stub_controller: DualArmController, stub_pipette: DigitalPipette, layout: PlateLayout ) -> None: """pipette_well runs without error in stub mode.""" pipette_well(stub_controller, stub_pipette, layout, "arm_a", "TROUGH", "A1", 50.0) def test_single_well_fill_resets( self, stub_controller: DualArmController, stub_pipette: DigitalPipette, layout: PlateLayout ) -> None: """After aspirate+dispense, pipette fill is 0.""" pipette_well(stub_controller, stub_pipette, layout, "arm_a", "TROUGH", "A1", 50.0) assert stub_pipette._current_fill == 0.0 def test_single_well_invalid_dest( self, stub_controller: DualArmController, stub_pipette: DigitalPipette, layout: PlateLayout ) -> None: """Invalid well name raises ValueError.""" with pytest.raises(ValueError): pipette_well(stub_controller, stub_pipette, layout, "arm_a", "TROUGH", "Z99", 50.0) def test_uc1_single_well_wrapper( self, stub_controller: DualArmController, stub_pipette: DigitalPipette, layout: PlateLayout ) -> None: """uc1_single_well is a convenience wrapper over pipette_well.""" uc1_single_well(stub_controller, stub_pipette, layout, "arm_a", "A1", 50.0) assert stub_pipette._current_fill == 0.0 class TestUC1Row: """Test row pipetting.""" def test_row_a_completes( self, stub_controller: DualArmController, stub_pipette: DigitalPipette, layout: PlateLayout ) -> None: """Row A pipettes 12 wells without error.""" uc1_row(stub_controller, stub_pipette, layout, "arm_a", "A", 25.0) def test_row_fill_resets( self, stub_controller: DualArmController, stub_pipette: DigitalPipette, layout: PlateLayout ) -> None: """After full row, pipette fill is 0.""" uc1_row(stub_controller, stub_pipette, layout, "arm_a", "A", 25.0) assert stub_pipette._current_fill == 0.0 def test_invalid_row_raises( self, stub_controller: DualArmController, stub_pipette: DigitalPipette, layout: PlateLayout ) -> None: """Invalid row letter raises ValueError.""" with pytest.raises(ValueError): uc1_row(stub_controller, stub_pipette, layout, "arm_a", "Z", 25.0) class TestUC1Col: """Test column pipetting.""" def test_col_1_completes( self, stub_controller: DualArmController, stub_pipette: DigitalPipette, layout: PlateLayout ) -> None: """Column 1 pipettes 8 wells without error.""" uc1_col(stub_controller, stub_pipette, layout, "arm_a", 1, 20.0) assert stub_pipette._current_fill == 0.0 class TestUC1FullPlate: """Test full-plate pipetting.""" def test_full_plate_completes( self, stub_controller: DualArmController, stub_pipette: DigitalPipette, layout: PlateLayout ) -> None: """Full plate (96 wells) completes without overflow error.""" uc1_full_plate(stub_controller, stub_pipette, layout, "arm_a", 20.0) def test_full_plate_fill_resets( self, stub_controller: DualArmController, stub_pipette: DigitalPipette, layout: PlateLayout ) -> None: """After full plate, pipette fill is 0.""" uc1_full_plate(stub_controller, stub_pipette, layout, "arm_a", 20.0) assert stub_pipette._current_fill == 0.0 class TestUC2Fridge: """Test fridge open-grab-move sequence.""" def test_fridge_completes( self, stub_controller: DualArmController, changer: ToolChanger ) -> None: """Fridge sequence completes without error.""" uc2_fridge_open_grab_move(stub_controller, changer, "arm_a") def test_fridge_ends_with_gripper( self, stub_controller: DualArmController, changer: ToolChanger ) -> None: """After fridge sequence, tool is GRIPPER.""" uc2_fridge_open_grab_move(stub_controller, changer, "arm_a") assert changer.current_tool == Tool.GRIPPER def test_fridge_uses_hook( self, stub_controller: DualArmController, changer: ToolChanger ) -> None: """Fridge sequence switches to FRIDGE_HOOK during execution.""" # After completion, tool is GRIPPER. But the sequence uses FRIDGE_HOOK. # We verify by checking the function doesn't crash (hook station exists). uc2_fridge_open_grab_move(stub_controller, changer, "arm_a") class TestUC3ToolCycle: """Test tool interchange cycle.""" def test_default_cycle_ends_on_gripper( self, stub_controller: DualArmController, changer: ToolChanger ) -> None: """Default tool cycle ends with GRIPPER equipped.""" uc3_tool_cycle(stub_controller, changer, "arm_a") assert changer.current_tool == Tool.GRIPPER def test_custom_sequence( self, stub_controller: DualArmController, changer: ToolChanger ) -> None: """Custom tool sequence is followed.""" uc3_tool_cycle(stub_controller, changer, "arm_a", [Tool.PIPETTE, Tool.FRIDGE_HOOK]) assert changer.current_tool == Tool.FRIDGE_HOOK def test_same_tool_noop(self, stub_controller: DualArmController, changer: ToolChanger) -> None: """Changing to the already-equipped tool is a no-op.""" changer.current_tool = Tool.GRIPPER uc3_tool_cycle(stub_controller, changer, "arm_a", [Tool.GRIPPER]) assert changer.current_tool == Tool.GRIPPER class TestUC4DemoAll: """Test full demo sequence.""" def test_demo_all_completes( self, stub_controller: DualArmController, stub_pipette: DigitalPipette, changer: ToolChanger, layout: PlateLayout, ) -> None: """Full demo runs without error in stub mode.""" uc4_demo_all(stub_controller, stub_pipette, changer, layout, "arm_a") def test_demo_all_fill_zero( self, stub_controller: DualArmController, stub_pipette: DigitalPipette, changer: ToolChanger, layout: PlateLayout, ) -> None: """After demo, pipette fill is 0.""" uc4_demo_all(stub_controller, stub_pipette, changer, layout, "arm_a") assert stub_pipette._current_fill == 0.0 class TestCreateWorkflowContext: """Test factory function.""" @pytest.mark.integration def test_creates_all_components(self) -> None: """Factory returns 4-tuple with all components.""" arm, pipette, changer, layout = create_workflow_context() assert arm._connected is True assert isinstance(pipette, DigitalPipette) assert isinstance(changer, ToolChanger) assert isinstance(layout, PlateLayout) arm.disconnect() @pytest.mark.integration def test_arm_in_stub_mode(self) -> None: """Factory creates arm in stub mode (no hardware).""" arm, _, _, _ = create_workflow_context() assert arm._stub_mode is True arm.disconnect() ================================================ FILE: .claude/settings.json ================================================ { "env": { "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1", "CLAUDE_CODE_IDE_SKIP_AUTO_INSTALL": "1", "DISABLE_NON_ESSENTIAL_MODEL_CALLS": "1" }, "outputStyle": "default", "spinnerTipsEnabled": true, "prefersReducedMotion": true, "statusLine": { "type": "command", "command": "bash .claude/scripts/statusline.sh" }, "attribution": { "commit": "Co-Authored-By: Claude ", "pr": "Generated with Claude " }, "sandbox": { "enabled": true, "autoAllowBashIfSandboxed": true, "allowUnsandboxedCommands": false, "enableWeakerNestedSandbox": false, "network": { "allowLocalBinding": true } }, "permissions": { "allow": [ "AskUserQuestion", "Bash(date:*)", "Bash(git:add:*)", "Bash(git:diff:*)", "Bash(git:log:*)", "Bash(git:status:*)", "Bash(git:log:--grep:*)", "Bash(git:rev-list:*)", "Bash(jq:*)", "Bash(make:*)", "Bash(tree:*)", "Edit(docs/)", "Edit(tests/)", "Edit(src/**)", "Edit(configs/)", "Edit(scripts/)", "mcp__ide__getDiagnostics", "Read(.claude/skills/)", "Read(.claude/rules/)", "SlashCommand", "Skill" ], "deny": [ "Bash(awk:*)", "Bash(cat:*)", "Bash(find:*)", "Bash(git:push:*)", "Bash(grep:*)", "Bash(head:*)", "Bash(ls:*)", "Bash(source:*)", "Bash(tail:*)", "Bash(touch:*)", "Bash(curl:*)", "Bash(wget:*)", "Edit(.env)", "Read(.env)" ], "ask": [ "Bash(git clean:*)", "Bash(git:commit:*)", "Bash(git reset:*)", "Bash(mv:*)", "Bash(mkdir:*)", "Bash(rm:*)", "Edit(.claude/**)", "Edit(AGENTS.md)", "Edit(CLAUDE.md)", "Edit(Makefile)", "Edit(pyproject.toml)", "Edit(README.md)" ] }, "enabledPlugins": { "context7@claude-plugins-official": true, "code-review@claude-plugins-official": true, "code-simplifier@claude-plugins-official": true, "cc-meta@qte77-claude-code-utils": true, "python-dev@qte77-claude-code-utils": true, "docs-governance@qte77-claude-code-utils": true, "commit-helper@qte77-claude-code-utils": true, "codebase-tools@qte77-claude-code-utils": true }, "extraKnownMarketplaces": { "qte77-claude-code-utils": { "source": { "source": "github", "repo": "qte77/claude-code-utils-plugin" } } } } ================================================ FILE: .claude/rules/compound-learning.md ================================================ # Compound Learning Prevent repeated mistakes by systematically promoting learnings. ## Before Solving a Problem Check AGENT_LEARNINGS.md for prior art. If a matching pattern exists, apply it. ## Promotion Path 1. **1st occurrence** — fix inline, move on 2. **2nd occurrence** — add to AGENT_LEARNINGS.md (pattern + solution) 3. **3rd occurrence** — promote to `.claude/rules/` (always-loaded, prevents recurrence) 4. **Recurring workflow** — extract to `.claude/skills/` (reusable capability) ## When Promoting (step 3) - Verify root cause is the same across occurrences - Write as a constraint ("do X", "never Y"), not a narrative - Remove or link the original AGENT_LEARNINGS.md entry to avoid duplication ================================================ FILE: .claude/rules/context-management.md ================================================ # Context Management (ACE-FCA) Principles for optimal context window utilization. ## Context Quality Equation Quality output = Correct context + Complete context + Minimal noise ## Degradation Hierarchy (worst to best) 1. **Incorrect information** - worst, causes cascading errors (garbage in, garbage out) 2. **Missing information** - leads to assumptions (agent guesses, sometimes wrong) 3. **Excessive noise** - dilutes signal, wastes capacity (truth buried but still there) Better to have less correct info than more info with errors. ## Utilization Target Keep context at **40-60%** capacity. Leave room for: - Model reasoning - Output generation - Error recovery ## Context Pollution Sources (What) These mess up context - compact/summarize immediately: - File searches (glob/grep results) - Code flow traces - Edit applications - Test/build logs - Large JSON blobs from tools ## Workflow Phases Research → Planning → Implementation. Compact after each phase transition. ## Compaction Triggers (When) Use `compacting-context` skill when: - Verbose tool output (logs, JSON, search results) - After completing a phase or milestone - Before starting new complex task ## Subagent Usage Use `researching-codebase` skill to: - Isolate discovery artifacts from main context - Return structured findings only - Prevent search noise pollution ## Output Guidelines - Prefer structured summaries over raw dumps - Extract only relevant portions from large files - Use targeted searches, not broad sweeps ================================================ FILE: .claude/rules/core-principles.md ================================================ # Core Principles **MANDATORY for ALL tasks.** These principles override all other guidance when conflicts arise. ## User-Centric Principles **User Experience, User Joy, User Success** - Every decision optimizes for user value, clarity, and usability. ## Code Quality Principles **KISS (Keep It Simple, Stupid)** - Simplest solution that works. Clear > clever. **DRY (Don't Repeat Yourself)** - Single source of truth. Reference, don't duplicate. **YAGNI (You Aren't Gonna Need It)** - Implement only what's requested. No speculative features. ## Execution Principles **Concise and Focused** - Minimal code/text for task. Touch only task-related code. **Reuse and Extend** - Use existing patterns and dependencies. Don't rebuild. **Prevent Incoherence** - Spot inconsistencies. Validate against existing patterns. **Resolve Ambiguity** - Clarify vague requirements before acting. ## Decision Principles **Rigor and Sufficiency** - Research enough to decide confidently. No more, no less. **High-Impact Quick Wins** - Prioritize must-do tasks. Ship fast, iterate. **Actionable and Concrete** - Specific deliverables. Measurable outcomes. **Root-Cause and First-Principles** - Understand the "why". Solve root problems. ## Before Starting Any Task - [ ] Does this serve user value? - [ ] Is this the simplest approach? - [ ] Am I duplicating existing work? - [ ] Do I actually need this? - [ ] Am I touching only relevant code? - [ ] What's the root cause I'm solving? ## Post-Task Review Before finishing, ask yourself: - **Did we forget anything?** - Check requirements thoroughly - **High-ROI enhancements?** - Suggest opportunities (don't implement) - **Something to delete?** - Remove obsolete/unnecessary code **IMPORTANT**: Do NOT alter files based on this review. Only output suggestions to the user. ## When in Doubt **STOP. Ask the user.** Don't assume, don't over-engineer, don't add complexity. ================================================ FILE: .claude/rules/robotics-safety.md ================================================ # Robotics Safety **Project-specific rules for robotic arm automation.** ## Hardware Safety - **Never bypass SafetyMonitor** — all arm operations must be supervised by watchdog - **Never hardcode joint positions** — use YAML configs (`configs/*.yaml`) - **Never skip stub mode fallback** — every hardware module must degrade gracefully when its dependency is unavailable - **Never send actions without joint limit checks** — validate against JOINT_LIMITS before commanding real hardware ## Arm Operations - **LeRobot API only** — never bypass the `DualArmController` abstraction for arm commands - **Park before disconnect** — always call `park_all()` before `disconnect()` - **One config per parameter** — serial ports, joint limits, dock positions live in YAML, not code ## Pipette Safety - **Track fill state** — never allow aspirate beyond `max_volume_ul` or dispense beyond `_current_fill` - **Validate volumes** — reject zero, negative, or overflow volumes before actuating ## Tool Changing - **Verify tool state** — `changer.current_tool` must reflect physical state - **Return before pickup** — always return current tool to dock before picking up a new one ================================================ FILE: .claude/rules/testing.md ================================================ --- paths: - "tests/**/*.py" --- # Testing Rules - Mock external dependencies (HTTP, file systems, APIs) - Use pytest with arrange/act/assert structure - Mirror src/ structure in tests/ - Use tmp_path for filesystem isolation ================================================ FILE: .claude/scripts/statusline.sh ================================================ #!/bin/bash input=$(cat) # Single jq call extracts all fields (tab-delimited) read -r cwd agent model version cost duration lines_added lines_removed \ tokens_in tokens_out remaining_pct exc_context <<< "$(echo "$input" | jq -r '[ .workspace.current_dir, (.agent.type // "main"), .model.id, (.version // ""), (if .cost.total_cost_usd then (.cost.total_cost_usd * 100 | round / 100 | tostring) + "$" else "" end), (((.cost.total_api_duration_ms // 0) / 1000 / 60 | round | tostring) + "m"), (.cost.total_lines_added // 0 | tostring), (.cost.total_lines_removed // 0 | tostring), (((.context_window.total_input_tokens // 0) / 1000 | floor | tostring) + "k"), (((.context_window.total_output_tokens // 0) / 1000 | floor | tostring) + "k"), (if .context_window.current_usage.input_tokens then (.context_window.context_window_size // 200000) as $win | (.context_window.current_usage.input_tokens // 0) as $in | (.context_window.current_usage.cache_creation_input_tokens // 0) as $cc | (.context_window.current_usage.cache_read_input_tokens // 0) as $cr | (($in + $cc + $cr) / $win * 100) as $used | (100 - $used) | round else .context_window.remaining_percentage // 100 end | tostring), (.exceeds_200k_tokens // false | tostring) ] | join("\t")')" lines_changed="+${lines_added}/-${lines_removed}" tokens="${tokens_in}/${tokens_out}" # Subtract autocompact buffer to get TRUE usable space if [ -n "$CLAUDE_AUTOCOMPACT_PCT_OVERRIDE" ]; then AUTOCOMPACT_BUFFER_PCT=$(awk "BEGIN {print 100 - $CLAUDE_AUTOCOMPACT_PCT_OVERRIDE}") else AUTOCOMPACT_BUFFER_PCT=16.5 fi true_free_pct=$(awk "BEGIN {print $remaining_pct - $AUTOCOMPACT_BUFFER_PCT}") remaining=$(echo "$true_free_pct" | awk '{printf "%.2f", $1/100}' | sed 's/^0\./\./') # Color remaining based on TRUE free space threshold if [ $(awk "BEGIN {print ($true_free_pct <= 10)}") -eq 1 ]; then ctx_color="\033[93;41m" # Bright yellow fg, red bg - CRITICAL elif [ $(awk "BEGIN {print ($true_free_pct <= 20)}") -eq 1 ]; then ctx_color="\033[91;48;5;237m" # Bright red fg, dark gray bg - WARNING elif [ $(awk "BEGIN {print ($true_free_pct <= 35)}") -eq 1 ]; then ctx_color="\033[93m" # Yellow fg - CAUTION else ctx_color="\033[0;32m" # Normal green fg - OK fi user=$(whoami) time=$(date +%H:%M:%S) if git rev-parse --git-dir >/dev/null 2>&1; then branch=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null) else branch="" fi printf "\033[0;31magent:%s \033[0;33mmodel:%s \033[2mver:%s \033[0;34mcost:%s \033[0;36mdur:%s\n\033[0;32mlines:%s \033[2mtokens(i/o):%s ${ctx_color}ctx(free):%s\033[0m \033[0;31m>200k:%s\033[0m\n\033[2mdir:%s \033[0;36mbranch:%s \033[0;32muser:%s \033[0;35mtime:%s\033[0m" "$agent" "$model" "$version" "$cost" "$duration" "$lines_changed" "$tokens" "$remaining" "$exc_context" "$(basename "$cwd")" "$branch" "$user" "$time" ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "name": "so101-biolab-automation", "image": "mcr.microsoft.com/vscode/devcontainers/python:3.12", "features": { "ghcr.io/devcontainers/features/node:1": {} }, "customizations": { "vscode": { "extensions": [ "anthropic.claude-code", "charliermarsh.ruff", "editorconfig.editorconfig", "gruntfuggly.todo-tree", "mhutchie.git-graph", "redhat.vscode-yaml", "tamasfe.even-better-toml", "github.copilot", "github.vscode-github-actions", "ms-python.debugpy", "ms-python.python", "ms-python.vscode-pylance", "ms-vscode.makefile-tools", "wakatime.vscode-wakatime" ], "settings": { "dotfiles.repository": "qte77/dotfiles", "dotfiles.installCommand": "install.sh", "dotfiles.targetPath": "~/dotfiles", "makefile.configureOnOpen": false, "python.defaultInterpreterPath": "./.venv/bin/python" } } }, "containerEnv": { "UV_LINK_MODE": "copy", "UV_CACHE_DIR": "/tmp/claude-1000/uv-cache", "RTK_TELEMETRY_DISABLED": "1" }, "postCreateCommand": "make setup_all || true" } ================================================ FILE: .github/dependabot.yaml ================================================ --- # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" ... ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ # Summary Closes ## Type of Change - [ ] `feat` — new feature - [ ] `fix` — bug fix - [ ] `docs` — documentation only - [ ] `refactor` — no functional change - [ ] `test` — test additions or fixes - [ ] `ci` — CI/CD changes - [ ] `build` — build system or dependency changes - [ ] `chore` — tooling, config, maintenance - [ ] **Breaking change** — add `!` after commit type ## Self-Review - [ ] I have reviewed my own diff and removed debug/dead code - [ ] Commit messages follow [`.gitmessage`](../.gitmessage) format ## Testing - [ ] `make validate` passes (lint + type check + tests) - [ ] New functionality has corresponding tests - [ ] Hardware-dependent tests gated with `@pytest.mark.hardware` ## Documentation - [ ] [`CHANGELOG.md`](../CHANGELOG.md) updated under `## [Unreleased]` - [ ] Docstrings added/updated for new/modified functions - [ ] `AGENT_LEARNINGS.md` updated if new pattern discovered ## Security - [ ] No hardcoded secrets, API keys, or credentials - [ ] Serial command inputs validated - [ ] WebSocket command inputs validated ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Report a problem with so101-biolab-automation title: '' labels: bug assignees: '' --- ## Description ## Steps to Reproduce 1. Run `...` 2. ... ## Expected Behavior ## Actual Behavior ## Environment - OS: - Python version: - Hardware: (SO-101 arms / simulation / stub mode) - Installation method (`uv sync`, Docker, etc.): ## Additional Context ================================================ FILE: .github/ISSUE_TEMPLATE/config.yaml ================================================ blank_issues_enabled: false contact_links: - name: Documentation url: https://github.com/qte77/so101-biolab-automation/blob/main/README.md about: Check the documentation before opening an issue ================================================ FILE: .github/ISSUE_TEMPLATE/question.md ================================================ --- name: Question about: Ask a question not covered by the documentation title: '' labels: question assignees: '' --- **Have you checked the docs?** - [ ] [README.md](https://github.com/qte77/so101-biolab-automation/blob/main/README.md) - [ ] [Architecture](https://github.com/qte77/so101-biolab-automation/blob/main/docs/architecture.md) - [ ] [Demo Scenarios](https://github.com/qte77/so101-biolab-automation/blob/main/docs/demo-scenarios.md) - [ ] [CONTRIBUTING.md](https://github.com/qte77/so101-biolab-automation/blob/main/CONTRIBUTING.md) ## Question ================================================ FILE: .github/templates/llms.txt.tpl ================================================ # ${PROJECT_NAME} > ${PROJECT_DESC} ## Documentation - [README](${BLOB}/README.md) - [AGENTS](${BLOB}/AGENTS.md) - [CONTRIBUTING](${BLOB}/CONTRIBUTING.md) - [Architecture](${BLOB}/docs/architecture.md) - [User Stories](${BLOB}/docs/UserStory.md) - [Demo Scenarios](${BLOB}/docs/demo-scenarios.md) - [Hardware BOM](${BLOB}/docs/hardware/BOM.md) - [CHANGELOG](${BLOB}/CHANGELOG.md) ## Source - [biolab/arms.py](${BLOB}/src/biolab/arms.py): Dual SO-101 arm controller (LeRobot wrapper) - [biolab/pipette.py](${BLOB}/src/biolab/pipette.py): Digital pipette serial control - [biolab/plate.py](${BLOB}/src/biolab/plate.py): 96-well coordinate grid (SBS standard) - [biolab/tool_changer.py](${BLOB}/src/biolab/tool_changer.py): Autonomous tool changing - [biolab/camera.py](${BLOB}/src/biolab/camera.py): Multi-camera pipeline - [biolab/safety.py](${BLOB}/src/biolab/safety.py): E-stop, watchdog, joint limits - [dashboard/server.py](${BLOB}/src/dashboard/server.py): FastAPI remote oversight ## Configuration - [configs/arms.yaml](${BLOB}/configs/arms.yaml): Arm port mappings and motor IDs - [configs/plate_layout.yaml](${BLOB}/configs/plate_layout.yaml): Well coordinates and heights - [configs/tool_dock.yaml](${BLOB}/configs/tool_dock.yaml): Tool dock station positions ================================================ FILE: .github/workflows/codeql.yaml ================================================ --- name: CodeQL on: push: schedule: - cron: "27 11 * * 0" workflow_dispatch: jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write steps: - name: Checkout repository uses: actions/checkout@v6 - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: languages: python - name: Autobuild uses: github/codeql-action/autobuild@v4 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 ... ================================================ FILE: .github/workflows/generate-sbom.yaml ================================================ name: Generate SBOM on: push: branches: [main] schedule: - cron: "0 0 * * 0" workflow_dispatch: permissions: contents: write env: SBOM_DIR: docs/SBOM jobs: generate-sbom: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6 - name: Install uv uses: astral-sh/setup-uv@v7 with: enable-cache: true cache-dependency-glob: "uv.lock" - name: Set up Python run: uv python install 3.12 - name: Install dependencies run: uv sync - name: Create SBOM output directory run: mkdir -p "$SBOM_DIR" - name: Export GitHub dependency graph SBOM continue-on-error: true env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO: ${{ github.repository }} run: gh api "repos/$REPO/dependency-graph/sbom" --jq '.sbom' > "$SBOM_DIR/github-depgraph.spdx.json" - name: Install Syft uses: anchore/sbom-action/download-syft@v0 - name: Generate Syft SPDX SBOM run: syft dir:. -o spdx-json="$SBOM_DIR/syft-scan.spdx.json" - name: Commit and push if changed run: | git config user.name "github-actions" git config user.email "github-actions@github.com" git add "$SBOM_DIR/" git diff --cached --quiet && echo "No changes to SBOM" && exit 0 git commit -m "docs: update SBOM" git push ================================================ FILE: .github/workflows/links-fail-fast.yaml ================================================ --- name: Link Checker on: workflow_dispatch: push: branches-ignore: [main] pull_request: types: [closed] branches: [main] schedule: - cron: "00 00 * * 0" jobs: linkChecker: runs-on: ubuntu-latest permissions: issues: write steps: - uses: actions/checkout@v6 - name: Link Checker id: lychee uses: lycheeverse/lychee-action@v2 with: args: "--config .lychee.toml ." - name: Create Issue From File if: steps.lychee.outputs.exit_code != 0 uses: peter-evans/create-issue-from-file@v6 with: title: lychee Link Checker Report content-filepath: ./lychee/out.md labels: report, automated issue ... ================================================ FILE: .github/workflows/pytest.yaml ================================================ name: pytest on: workflow_dispatch: pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6 - name: Install uv uses: astral-sh/setup-uv@v7 with: enable-cache: true cache-dependency-glob: "uv.lock" - name: Set up Python run: uv python install 3.12 - name: Install dependencies run: uv sync --group test - name: Run tests run: uv run pytest ================================================ FILE: .github/workflows/ruff.yaml ================================================ --- name: ruff on: push: pull_request: types: [closed] branches: [main] schedule: - cron: "0 0 * * 0" workflow_dispatch: jobs: ruff: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: astral-sh/ruff-action@v3 ... ================================================ FILE: .github/workflows/write-llms-txt.yaml ================================================ name: Write repo llms.txt on: push: branches: [main] paths: - ".github/templates/llms.txt.tpl" - "src/**" - "README.md" - "CONTRIBUTING.md" - "AGENTS.md" workflow_dispatch: permissions: contents: write jobs: generate-file: runs-on: ubuntu-latest env: REPO: ${{ github.repository }} steps: - name: Checkout repo uses: actions/checkout@v6 - name: Validate template links exist run: | stale=0 for path in $(grep -oE '\$\{BLOB\}/[^)]+' .github/templates/llms.txt.tpl | sed 's|\${BLOB}/||'); do if [ ! -f "$path" ]; then echo "::error::Stale link in template: $path" stale=1 fi done exit $stale - name: Generate llms.txt from template env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | BLOB="https://github.com/${REPO}/blob/main" PROJECT_NAME=$(sed -n 's/^# //p' README.md | head -1) PROJECT_DESC=$(curl -sf -H "Authorization: token ${GH_TOKEN}" \ "https://api.github.com/repos/${REPO}" | jq -r '.description // empty') export BLOB PROJECT_NAME PROJECT_DESC mkdir -p docs envsubst '${BLOB} ${PROJECT_NAME} ${PROJECT_DESC}' < .github/templates/llms.txt.tpl > docs/llms.txt - name: Commit and push if changed run: | git config user.name "github-actions" git config user.email "github-actions@github.com" git add docs/llms.txt git diff --cached --quiet && echo "No changes to llms.txt" && exit 0 git commit -m "docs: update llms.txt index" git push