diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bd370e..7ae0a2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,9 @@ jobs: - name: Run pre-commit run: pixi run pre-commit run --all-files + - name: SDK unit tests + run: pixi run test-sdk + - name: PythonExample unit tests run: pixi run test-python-example diff --git a/.gitignore b/.gitignore index 01c8096..86ec081 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ push.json # Python package metadata (pip install -e) *.egg-info/ +dist/ # Rust build output target/ @@ -11,3 +12,4 @@ target/ # Dora / local output and logs Demo/out/ out/ +*.code-workspace diff --git a/AmazingHand.code-workspace b/AmazingHand.code-workspace deleted file mode 100644 index 876a149..0000000 --- a/AmazingHand.code-workspace +++ /dev/null @@ -1,8 +0,0 @@ -{ - "folders": [ - { - "path": "." - } - ], - "settings": {} -} \ No newline at end of file diff --git a/Demo/README.md b/Demo/README.md index bf3928f..22f57c7 100644 --- a/Demo/README.md +++ b/Demo/README.md @@ -2,7 +2,7 @@ ## Running with pixi -Prerequisites: install [Pixi](https://pixi.prefix.dev/latest/installation/). Rust is needed for real hardware demos (AHControl). Before running real hardware demos, check the serial port in the dataflow YAML: the default is Linux (`/dev/ttyACM0`); on Windows use your COM port (e.g. `COM3`). +Prerequisites: install [Pixi](https://pixi.prefix.dev/latest/installation/). Rust is needed for real hardware demos (AHControl). Before running real hardware demos, check the serial port in the dataflow YAML: the default is Linux (`/dev/ttyACM0`); on Windows use your COM port (e.g. `COM3`). Run `pixi run check-devices` to list webcam indices and serial ports. From the AmazingHand repository root (Git Bash on Windows, or a Unix shell on Linux/macOS): diff --git a/Demo/dataflow_tracking_real_team_krishan.yml b/Demo/dataflow_tracking_real_team_krishan.yml new file mode 100644 index 0000000..0b61064 --- /dev/null +++ b/Demo/dataflow_tracking_real_team_krishan.yml @@ -0,0 +1,28 @@ +# team_krishan (left hand), Windows: AHControl.exe, COM port. +# Dora spawns nodes with cwd = Demo/, path relative to Demo. +# See docs/canonical_hand_config_design.md. +nodes: + - id: hand_tracker + build: python -m pip install -e HandTracking + path: HandTracking/HandTracking/main.py + inputs: + tick: dora/timer/millis/50 + outputs: + - l_hand_pos + + - id: l_hand_simulation + build: python -m pip install -e AHSimulation + path: AHSimulation/AHSimulation/mj_mink_left.py + inputs: + l_hand_pos: hand_tracker/l_hand_pos + tick: dora/timer/millis/2 + tick_ctrl: dora/timer/millis/10 + outputs: + - mj_l_joints_pos + + - id: hand_controller + build: cargo build -p AHControl + path: target/debug/AHControl.exe + args: --serialport COM3 --config ../config/calibration/l_hand_team_krishan.toml + inputs: + mj_l_joints_pos: l_hand_simulation/mj_l_joints_pos diff --git a/Demo/scripts/check_devices.py b/Demo/scripts/check_devices.py new file mode 100644 index 0000000..444955f --- /dev/null +++ b/Demo/scripts/check_devices.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Julia Jia +# +# 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. + +"""Check webcam indices and serial ports. Run before demos to verify devices.""" + +import os +import sys + +_MAX_CAMERA_PROBE = 3 + + +def check_webcams(): + """Probe camera indices 0..N and report which open successfully.""" + try: + import cv2 + except ImportError: + print("Webcam: opencv-python not installed (pip install opencv-python)") + return + available = [] + devnull = os.open(os.devnull, os.O_WRONLY) + stderr_fd = os.dup(2) + try: + os.dup2(devnull, 2) + for i in range(_MAX_CAMERA_PROBE): + cap = cv2.VideoCapture(i) + if cap.isOpened(): + w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + available.append((i, w, h)) + cap.release() + finally: + os.dup2(stderr_fd, 2) + os.close(stderr_fd) + os.close(devnull) + if available: + print("Webcam indices:") + for idx, w, h in available: + print(f" {idx}: {w}x{h}") + print(" Use index 0 in HandTracking unless you have multiple cameras.") + if len(available) >= 2: + print(" Note: indices 0 and 1 may be the same camera (Linux exposes multiple /dev/video* nodes). Try 0 first; if wrong, try 1.") + else: + print(f"Webcam: no cameras found (indices 0..{_MAX_CAMERA_PROBE - 1})") + + +def _is_usb_port(device): + """Prefer USB serial devices (hand controller) over built-in ttyS.""" + d = device.upper() + return "ACM" in d or "USB" in d or d.startswith("COM") + + +def _port_status(device): + """Try opening port. Return 'available', 'in_use', or 'no_permission'.""" + try: + import serial + except ImportError: + return None + try: + ser = serial.Serial(device, timeout=0.1) + ser.close() + return "available" + except OSError as e: + if e.errno == 16: # EBUSY + return "in_use" + if e.errno in (13, 1): # EACCES, EPERM + return "no_permission" + return None + except Exception as e: + msg = str(e).lower() + if "busy" in msg or "in use" in msg or "resource" in msg: + return "in_use" + if "denied" in msg or "permission" in msg or "access" in msg: + return "no_permission" + return None + + +def check_serial_ports(): + """List serial ports. Linux: /dev/ttyACM*, /dev/ttyUSB*. Windows: COM*.""" + try: + import serial.tools.list_ports + except ImportError: + print("Serial: pyserial not installed (pip install pyserial)") + return + ports = list(serial.tools.list_ports.comports()) + usb = [p for p in ports if _is_usb_port(p.device)] + other = [p for p in ports if p not in usb] + shown = usb or other + if shown: + print("Serial ports:") + for p in sorted(shown, key=lambda x: (not _is_usb_port(x.device), x.device)): + desc = p.description or "" + note = " (USB, likely hand)" if _is_usb_port(p.device) else "" + status = _port_status(p.device) + if status == "in_use": + note += " [IN USE by another process]" + elif status == "no_permission": + note += " [no permission; Linux: add user to dialout group]" + print(f" {p.device}: {desc}{note}") + if usb: + print(" Use the USB device in dataflow YAML --serialport.") + else: + print("Serial: no serial ports found. Connect the hand via USB and ensure drivers are installed.") + + +def main(): + print("=== AmazingHand device check ===\n") + check_webcams() + print() + check_serial_ports() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/FORK.md b/FORK.md index d503567..5152de7 100644 --- a/FORK.md +++ b/FORK.md @@ -1,3 +1,23 @@ # Differences from Upstream -See the fork notice in [README.md](README.md). This file is for documenting how this repo differs from [pollen-robotics/AmazingHand](https://github.com/pollen-robotics/AmazingHand) (e.g. layout, tooling, refactors). +See the fork notice in [README.md](README.md). This file documents how this repo differs from [pollen-robotics/AmazingHand](https://github.com/pollen-robotics/AmazingHand). + +## Cross-Platform Support (Linux, Windows) + +Pixi-based setup; MSVC toolchain for Rust on Windows. See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md). + +## Canonical Configuration + +Shared hand geometry, per-physical-hand calibration, named profiles. See [docs/canonical_hand_config_design.md](docs/canonical_hand_config_design.md). + +## AmazingHand SDK + +Python package for named poses and raw angles. See [README_PKG.md](README_PKG.md). + +## CI + +GitHub Actions for lint (pre-commit), SDK, PythonExample, Demo, and AHControl tests. See [.github/workflows/ci.yml](.github/workflows/ci.yml). + +## Other Changes + +Pixi for dependency management; unit tests for SDK, PythonExample, Demo, and AHControl; Dora/MuJoCo simulation demos; pre-commit hooks. diff --git a/PythonExample/common.py b/PythonExample/common.py index 8a0f9ef..56a1a66 100644 --- a/PythonExample/common.py +++ b/PythonExample/common.py @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging import os import sys from pathlib import Path @@ -26,6 +27,7 @@ except ImportError: tomllib = None +_log = logging.getLogger(__name__) _REPO_ROOT = Path(__file__).resolve().parent.parent _CANONICAL_CONFIG_ROOT = _REPO_ROOT / "config" _PROFILE_ENV = "AMAZINGHAND_PROFILE" @@ -155,10 +157,16 @@ def load_config_canonical(profile=None, config_root=None): return _DEFAULTS.copy() with open(profiles_path, "rb") as f: data = tomllib.load(f) - name = (profile or os.environ.get(_PROFILE_ENV) or "team_julia").strip().lower() + name = (profile or os.environ.get(_PROFILE_ENV) or "team_krishan").strip().lower() + source = "argument" if profile else ("env " + _PROFILE_ENV if os.environ.get(_PROFILE_ENV) else "default") section = data.get("profile", {}).get(name, {}) if not section: + _log.warning( + "Profile %r not found in profiles.toml; using defaults. Set %s to a valid profile name.", + name, _PROFILE_ENV + ) return _DEFAULTS.copy() + print(f"Using profile {name!r} (from {source}). Set {_PROFILE_ENV} to override.") out = { "port": (section.get("port") or _DEFAULTS["port"]) or "", "baudrate": section.get("baudrate", _DEFAULTS["baudrate"]), diff --git a/README_PKG.md b/README_PKG.md new file mode 100644 index 0000000..4172248 --- /dev/null +++ b/README_PKG.md @@ -0,0 +1,44 @@ +# amazinghand + +Python SDK for Amazing Hand robotic hand (Pollen Robotics). Control the hand via named poses and raw angles. + +## Install + +```bash +pip install amazinghand +``` + +## Quick start + +```python +from amazinghand import AmazingHand, list_poses + +print("Available poses:", list_poses()) +hand = AmazingHand(profile="default") +hand.apply_pose("rock") +hand.apply_pose("paper") +hand.apply_pose("scissors") +``` + +## Configuration + +Set `AMAZINGHAND_CONFIG` to your config directory, or place config under `~/.config/amazinghand` (Linux) / `%LOCALAPPDATA%\amazinghand` (Windows). + +Config resolution order: + +1. `AMAZINGHAND_CONFIG` env +2. `config_root` argument to `AmazingHand()` +3. Repo config when running from source +4. User config dir +5. Bundled config (pip-installed) + +Environment variables: + +- `AMAZINGHAND_CONFIG`: path to directory with `profiles.toml` and `calibration/` +- `AMAZINGHAND_PROFILE`: profile name (default: `default`) + +To add a calibration: copy `calibration/right_hand.toml` to your file, fill in servo IDs and `rest_deg`, add a profile in `profiles.toml`, and reference it via `right_hand_calibration` / `left_hand_calibration`. + +## License + +Apache 2.0 diff --git a/docs/maintainer.md b/docs/maintainer.md index d83b39c..631b373 100644 --- a/docs/maintainer.md +++ b/docs/maintainer.md @@ -54,4 +54,38 @@ act push act pull_request ``` +## Publishing to PyPI + +Build the package: + +```bash +pixi run build-python +``` + +Publish to Test PyPI and PyPI: + +```bash +pixi run publish-testpypi +pixi run publish-pypi +``` + +Configure credentials in `~/.pypirc` so twine does not prompt: + +```ini +[distutils] +index-servers = + pypi + testpypi + +[testpypi] +repository = https://test.pypi.org/legacy/ +username = __token__ +password = pypi-xxxxxxxx + +[pypi] +username = __token__ +password = pypi-xxxxxxxx +``` + +Replace `pypi-xxxxxxxx` with your API token from PyPI account settings. Create separate tokens for Test PyPI and PyPI if needed. diff --git a/pixi.lock b/pixi.lock index 2bbc1b3..8ac65ee 100644 --- a/pixi.lock +++ b/pixi.lock @@ -48,6 +48,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/0d/84a4380f930db0010168e0aa7b7a8fed9ba1835a8fbb1472bc6d0201d529/build-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl @@ -55,9 +56,11 @@ environments: - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/76/c4aa9e408dbacee3f4de8e6c5417e5f55de7e62fb5a50300e1233a2c9cb5/commentjson-0.9.0.tar.gz - pypi: 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 + - pypi: https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/97/84/f8a89433fd182a850aa8db9c0cfffc9d2ebf7347b34eb5e203c8488459a2/daqp-0.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1a/af/78aaff4eff5bc850041035673971925ee2650a7ea1173d33ad35bd7d056c/dora_rs-0.3.13-cp37-abi3-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/da/77/20163e4df61f2c8e9e36619b2e23f4bec073885be46774709b0c58a5159d/dora_rs_cli-0.3.13-cp37-abi3-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl @@ -66,20 +69,30 @@ environments: - pypi: https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5a/3f/efeb7c6801c46e11bd666a5180f0d615f74f72264212f74f39586c6fda9d/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/57/1e/63ac22ec535e08129e16cb71b7eeeb8816c01d627ea1bc9105e925a71da0/jax-0.9.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1d/89/0dd938e6ed65ee994a49351a13aceaea46235ffbc1db5444d9ba3a279814/jaxlib-0.9.0.1-cp312-cp312-manylinux_2_27_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/34/b8/aa7d6cf2d5efdd2fcd85cf39b33584fe12a0f7086ed451176ceb7fb510eb/lark-parser-0.7.8.tar.gz - pypi: https://files.pythonhosted.org/packages/e2/6d/d56be57340baf2e6f6361386f4c21b8b5e001251c64af954787f8d01ec78/loop_rate_limiters-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/73/07c6dcbb322f86e2b8526e0073456dbdd2813d5351f772f882123c985fda/mediapipe-0.10.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/15/6f/64c5dd93f1d19de05709e5c6ac66f6abe89d280838b434e9075f94e30b65/mink-1.1.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c8/8a/229e4db3692be55532e155e2ca6a1363752243ee79df0e7e22ba00f716cf/mujoco-3.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/6f/84/c0dc75c7fb596135f999e59a410d9f45bdabb989f1cb911f0016d22b747b/nh3-0.3.3-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/ea/2c/e17b8814050427929077639d35a42187a006922600d4840475bdc5f64ebb/numpy_stl-3.2.0-py3-none-any.whl @@ -97,6 +110,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl @@ -104,12 +119,18 @@ environments: - pypi: 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 - pypi: https://files.pythonhosted.org/packages/76/e6/e6893547135170c23133bac241d5031b0f2002d61675f2166dcbeeb27fbf/qpsolvers-4.8.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/20/64/72ae344963db87a15e8566b8c4f1408080ad5746c8e1c32f8d04911074bb/quadprog-0.1.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/69/7d/5cd09ada4c979035348188efdb454df14ea032e4ebaa8745d579a56b0cf6/rustypot-1.4.2-cp312-cp312-manylinux_2_24_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d1/fe/66d73b76d378ba8cc2fe605920c0c75092e3a65ae746e1e767d9d020a75a/scipy-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/0a/478e441fd049002cf308520c0d62dd8333e7c6cc8d997f0dda07b9fbcc46/sounddevice-0.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/61/7a/f38385f1b2d5f54221baf1db3d6371dc6eef8041d95abff39576c694e9d9/transforms3d-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/b3/4b9580d62e1245df52e8516cf3e404ff39cc72634d2d749d47b1dada4161/uv-0.10.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl @@ -155,6 +176,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/0d/84a4380f930db0010168e0aa7b7a8fed9ba1835a8fbb1472bc6d0201d529/build-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl @@ -162,9 +184,11 @@ environments: - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c0/76/c4aa9e408dbacee3f4de8e6c5417e5f55de7e62fb5a50300e1233a2c9cb5/commentjson-0.9.0.tar.gz - pypi: https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2f/2a/b0a896f1aa8618fae09acf508f0bb0192022383dbf73bea76143345167c8/daqp-0.7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/59/d5/64a3e1dadfa72aed14d9dc1a2e11b4acbef9dfb850ad3eeed1293b63ea26/dora_rs-0.3.13-cp37-abi3-manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/90/2a/e66c6cbf19f05ec38ecab7ca3d0ecdb3b5a9450050087c12b6e1d3025699/dora_rs_cli-0.3.13-cp37-abi3-manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl @@ -173,20 +197,30 @@ environments: - pypi: https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ff/b4/f7b6cc022dd7c68b6c702d19da5d591f978f89c958b9bd3090615db0c739/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/57/1e/63ac22ec535e08129e16cb71b7eeeb8816c01d627ea1bc9105e925a71da0/jax-0.9.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c1/92/40d4f0acecb3d6f7078b9eb468e524778a3497d0882c7ecf80509c10b7d3/jaxlib-0.9.0.1-cp312-cp312-manylinux_2_27_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/34/b8/aa7d6cf2d5efdd2fcd85cf39b33584fe12a0f7086ed451176ceb7fb510eb/lark-parser-0.7.8.tar.gz - pypi: https://files.pythonhosted.org/packages/e2/6d/d56be57340baf2e6f6361386f4c21b8b5e001251c64af954787f8d01ec78/loop_rate_limiters-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f4/da/dfed8db260b3fbe4e24ac17dda32c55787643a656d8d4e78c55bc847efa8/mediapipe-0.10.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/9c/71/3b219f6563b5a1a4e7bda894d7314b9eea1a5ec53d5cc3975b7951c6e84a/mink-1.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b8/7b/c1612ec68d98e5f3dbc5b8a21ff5d40ab52409fcc89ea7afc8a197983297/mujoco-3.5.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/ca/43/d2011a4f6c0272cb122eeff40062ee06bb2b6e57eabc3a5e057df0d582df/nh3-0.3.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/ea/2c/e17b8814050427929077639d35a42187a006922600d4840475bdc5f64ebb/numpy_stl-3.2.0-py3-none-any.whl @@ -204,6 +238,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl @@ -211,12 +247,18 @@ environments: - pypi: 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 - pypi: https://files.pythonhosted.org/packages/76/e6/e6893547135170c23133bac241d5031b0f2002d61675f2166dcbeeb27fbf/qpsolvers-4.8.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/60/fb9b56d425c5b3495bb92683c28f954aedb609ae62f14c81a1b5621ad759/quadprog-0.1.13.tar.gz + - pypi: https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/6b/bfa2e5a16d7b9977c6cf65acf50eb2beb90199df8ef70abb6766d41b7d2b/rustypot-1.4.2-cp312-cp312-manylinux_2_24_aarch64.whl - pypi: https://files.pythonhosted.org/packages/4a/69/7c347e857224fcaf32a34a05183b9d8a7aca25f8f2d10b8a698b8388561a/scipy-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/0a/478e441fd049002cf308520c0d62dd8333e7c6cc8d997f0dda07b9fbcc46/sounddevice-0.5.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/61/7a/f38385f1b2d5f54221baf1db3d6371dc6eef8041d95abff39576c694e9d9/transforms3d-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/4e/058976e2a5513f11954e09595a1821d5db1819e96e00bafded19c6a470e9/uv-0.10.4-py3-none-manylinux_2_28_aarch64.whl @@ -245,6 +287,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.46.3-pyhd8ed1ab_0.conda - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/0d/84a4380f930db0010168e0aa7b7a8fed9ba1835a8fbb1472bc6d0201d529/build-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl @@ -255,6 +298,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/08/041461120e16bab05ea163afdb88fab452aefa9cbeadb9b91b3ec4f23635/daqp-0.7.2-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/59/8f/c5e43fc1a9bf27cd34cd98d5ad48ce18429851ae67977ae5be8f2ab0eccf/dora_rs-0.3.13-cp37-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/19/1e/2c9be5ca5a99bee5150cc76ff654f67c8d0e4b85e64f9cdd81d4ec138cf0/dora_rs_cli-0.3.13-cp37-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl @@ -263,20 +307,29 @@ environments: - pypi: https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b6/a6/6ea2f73ad4474896d9e38b3ffbe6ffd5a802c738392269e99e8c6621a461/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/57/1e/63ac22ec535e08129e16cb71b7eeeb8816c01d627ea1bc9105e925a71da0/jax-0.9.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c8/76/e89fd547f292663d8ce11b3247cd653a220e0d3cedbdbd094f0a8460d735/jaxlib-0.9.0.1-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/34/b8/aa7d6cf2d5efdd2fcd85cf39b33584fe12a0f7086ed451176ceb7fb510eb/lark-parser-0.7.8.tar.gz - pypi: https://files.pythonhosted.org/packages/e2/6d/d56be57340baf2e6f6361386f4c21b8b5e001251c64af954787f8d01ec78/loop_rate_limiters-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2d/df/be410905b9757de4b00891dd34236d96e6db150b624f28cc27cd90c74564/mediapipe-0.10.14-cp312-cp312-macosx_11_0_universal2.whl - pypi: https://files.pythonhosted.org/packages/c7/3d/97e4b9736f7d983d87f872eb80bbdc256a903ae1e4481f24bf3c317e376e/mink-1.1.0-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/d4/d0032323f58a9b8080b8464c6aade8d5ac2e101dbed1de64a38b3913b446/mujoco-3.5.0-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/13/3e/aef8cf8e0419b530c95e96ae93a5078e9b36c1e6613eeb1df03a80d5194e/nh3-0.3.3-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/ea/2c/e17b8814050427929077639d35a42187a006922600d4840475bdc5f64ebb/numpy_stl-3.2.0-py3-none-any.whl @@ -294,6 +347,8 @@ environments: - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl @@ -301,12 +356,17 @@ environments: - pypi: https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/76/e6/e6893547135170c23133bac241d5031b0f2002d61675f2166dcbeeb27fbf/qpsolvers-4.8.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/5e/de8d3911e44699abc4e3ce835e69c3db76525af8018026f9bce61be69a43/quadprog-0.1.13-cp312-cp312-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/99/d8/e91cfb6fde9b37fb9884e0ed0b6becc9b2d98293e8c3255579999a8cc734/rustypot-1.4.2-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/ed/1d/5057f812d4f6adc91a20a2d6f2ebcdb517fdbc87ae3acc5633c9b97c8ba5/scipy-1.17.0-cp312-cp312-macosx_12_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/56/f9/c037c35f6d0b6bc3bc7bfb314f1d6f1f9a341328ef47cd63fc4f850a7b27/sounddevice-0.5.5-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl - pypi: https://files.pythonhosted.org/packages/61/7a/f38385f1b2d5f54221baf1db3d6371dc6eef8041d95abff39576c694e9d9/transforms3d-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/63/91/c4ddf7e55e05394967615050cc364a999157a44c008d0e1e9db2ed49a11c/uv-0.10.4-py3-none-macosx_11_0_arm64.whl @@ -338,6 +398,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/wheel-0.46.3-pyhd8ed1ab_0.conda - pypi: https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/0d/84a4380f930db0010168e0aa7b7a8fed9ba1835a8fbb1472bc6d0201d529/build-1.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl @@ -348,6 +409,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ea/72/3bbbb5c5da4b982f420000ee4465b46b35d802bfbc73c1f3b772585c8b86/daqp-0.7.2-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/72/7e6881a2c9d0513e65e93f6f8343c5e66b46bee1e1de0d4d39d5fd3d1b7e/dora_rs-0.3.13-cp37-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/eb/29/005e12a00e4475c30185f4a3998fa82370eba689a8133227ef591924ee37/dora_rs_cli-0.3.13-cp37-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl @@ -356,20 +418,29 @@ environments: - pypi: https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/e1/6d6816b296a529ac9b897ad228b1e084eb1f92319e96371880eebdc874a6/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/57/1e/63ac22ec535e08129e16cb71b7eeeb8816c01d627ea1bc9105e925a71da0/jax-0.9.0.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bb/02/265e5ccadd65fee2f0716431573d9e512e5c6aecb23f478a7a92053cf219/jaxlib-0.9.0.1-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/34/b8/aa7d6cf2d5efdd2fcd85cf39b33584fe12a0f7086ed451176ceb7fb510eb/lark-parser-0.7.8.tar.gz - pypi: https://files.pythonhosted.org/packages/e2/6d/d56be57340baf2e6f6361386f4c21b8b5e001251c64af954787f8d01ec78/loop_rate_limiters-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f0/26/d228fe6e9f2060dde7f7db738968bcd603e9340f064351655b5b2652a664/mediapipe-0.10.14-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/99/7f/cfba1085a845ce188259d25c72d3180c4f7431c566125f6b171f2fd62d06/mink-1.1.0-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/02/37/527d83610b878f27c01dd762e0e41aaa62f095c607f0500ac7f724a2c7a5/mujoco-3.5.0-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/73/88/1ce287ef8649dc51365b5094bd3713b76454838140a32ab4f8349973883c/nh3-0.3.3-cp38-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/ea/2c/e17b8814050427929077639d35a42187a006922600d4840475bdc5f64ebb/numpy_stl-3.2.0-py3-none-any.whl @@ -387,19 +458,27 @@ environments: - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d4/69/31c82567719b34d8f6b41077732589104883771d182a9f4ff3e71430999a/python_utils-3.9.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/76/e6/e6893547135170c23133bac241d5031b0f2002d61675f2166dcbeeb27fbf/qpsolvers-4.8.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/11/4f8b99099215f6e7f2c8ca9756c590e561d1d3093fedb51a960dc609e347/quadprog-0.1.13-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/11/cb/60e4be7b67dd8e0adfe73d0d30c11977ca1a2639ce0b852d880852cae4bc/rustypot-1.4.2-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/92/ce/672ed546f96d5d41ae78c4b9b02006cedd0b3d6f2bf5bb76ea455c320c28/scipy-1.17.0-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c3/0e/002ed7c4c1c2ab69031f78989d3b789fee3a7fba9e586eb2b81688bf4961/sounddevice-0.5.5-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/61/7a/f38385f1b2d5f54221baf1db3d6371dc6eef8041d95abff39576c694e9d9/transforms3d-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/31/dd/1900452678d46f6a649ab8167bededb02500b0561fc9f69e1f52607895c7/uv-0.10.4-py3-none-win_amd64.whl @@ -465,6 +544,21 @@ packages: purls: [] size: 4741684 timestamp: 1770267224406 +- pypi: https://files.pythonhosted.org/packages/c5/0d/84a4380f930db0010168e0aa7b7a8fed9ba1835a8fbb1472bc6d0201d529/build-1.4.0-py3-none-any.whl + name: build + version: 1.4.0 + sha256: 6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596 + requires_dist: + - packaging>=24.0 + - pyproject-hooks + - colorama ; os_name == 'nt' + - importlib-metadata>=4.6 ; python_full_version < '3.10.2' + - tomli>=1.1.0 ; python_full_version < '3.11' + - uv>=0.1.18 ; extra == 'uv' + - virtualenv>=20.11 ; python_full_version < '3.10' and extra == 'virtualenv' + - virtualenv>=20.17 ; python_full_version >= '3.10' and python_full_version < '3.14' and extra == 'virtualenv' + - virtualenv>=20.31 ; python_full_version >= '3.14' and extra == 'virtualenv' + requires_python: '>=3.9' - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda sha256: c30daba32ddebbb7ded490f0e371eae90f51e72db620554089103b4a6934b0d5 md5: 51a19bba1b8ebfb60df25cde030b7ebc @@ -695,6 +789,66 @@ packages: - pytest-xdist ; extra == 'test-no-images' - wurlitzer ; extra == 'test-no-images' requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl + name: cryptography + version: 46.0.5 + sha256: 50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263 + requires_dist: + - cffi>=1.14 ; python_full_version == '3.8.*' and platform_python_implementation != 'PyPy' + - cffi>=2.0.0 ; python_full_version >= '3.9' and platform_python_implementation != 'PyPy' + - typing-extensions>=4.13.2 ; python_full_version < '3.11' + - bcrypt>=3.1.5 ; extra == 'ssh' + - nox[uv]>=2024.4.15 ; extra == 'nox' + - cryptography-vectors==46.0.5 ; extra == 'test' + - pytest>=7.4.0 ; extra == 'test' + - pytest-benchmark>=4.0 ; extra == 'test' + - pytest-cov>=2.10.1 ; extra == 'test' + - pytest-xdist>=3.5.0 ; extra == 'test' + - pretend>=0.7 ; extra == 'test' + - certifi>=2024 ; extra == 'test' + - pytest-randomly ; extra == 'test-randomorder' + - sphinx>=5.3.0 ; extra == 'docs' + - sphinx-rtd-theme>=3.0.0 ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - pyenchant>=3 ; extra == 'docstest' + - readme-renderer>=30.0 ; extra == 'docstest' + - sphinxcontrib-spelling>=7.3.1 ; extra == 'docstest' + - build>=1.0.0 ; extra == 'sdist' + - ruff>=0.11.11 ; extra == 'pep8test' + - mypy>=1.14 ; extra == 'pep8test' + - check-sdist ; extra == 'pep8test' + - click>=8.0.1 ; extra == 'pep8test' + requires_python: '>=3.8,!=3.9.0,!=3.9.1' +- pypi: https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl + name: cryptography + version: 46.0.5 + sha256: 3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed + requires_dist: + - cffi>=1.14 ; python_full_version == '3.8.*' and platform_python_implementation != 'PyPy' + - cffi>=2.0.0 ; python_full_version >= '3.9' and platform_python_implementation != 'PyPy' + - typing-extensions>=4.13.2 ; python_full_version < '3.11' + - bcrypt>=3.1.5 ; extra == 'ssh' + - nox[uv]>=2024.4.15 ; extra == 'nox' + - cryptography-vectors==46.0.5 ; extra == 'test' + - pytest>=7.4.0 ; extra == 'test' + - pytest-benchmark>=4.0 ; extra == 'test' + - pytest-cov>=2.10.1 ; extra == 'test' + - pytest-xdist>=3.5.0 ; extra == 'test' + - pretend>=0.7 ; extra == 'test' + - certifi>=2024 ; extra == 'test' + - pytest-randomly ; extra == 'test-randomorder' + - sphinx>=5.3.0 ; extra == 'docs' + - sphinx-rtd-theme>=3.0.0 ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - pyenchant>=3 ; extra == 'docstest' + - readme-renderer>=30.0 ; extra == 'docstest' + - sphinxcontrib-spelling>=7.3.1 ; extra == 'docstest' + - build>=1.0.0 ; extra == 'sdist' + - ruff>=0.11.11 ; extra == 'pep8test' + - mypy>=1.14 ; extra == 'pep8test' + - check-sdist ; extra == 'pep8test' + - click>=8.0.1 ; extra == 'pep8test' + requires_python: '>=3.8,!=3.9.0,!=3.9.1' - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl name: cycler version: 0.12.1 @@ -728,6 +882,11 @@ packages: name: distlib version: 0.4.0 sha256: 9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 +- pypi: https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl + name: docutils + version: 0.22.4 + sha256: d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/1a/af/78aaff4eff5bc850041035673971925ee2650a7ea1173d33ad35bd7d056c/dora_rs-0.3.13-cp37-abi3-manylinux_2_28_x86_64.whl name: dora-rs version: 0.3.13 @@ -1214,6 +1373,24 @@ packages: purls: [] size: 12358010 timestamp: 1767970350308 +- pypi: https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl + name: id + version: 1.6.1 + sha256: f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca + requires_dist: + - urllib3>=2,<3 + - build ; extra == 'dev' + - bump>=1.3.2 ; extra == 'dev' + - id[test,lint] ; extra == 'dev' + - bandit ; extra == 'lint' + - interrogate ; extra == 'lint' + - mypy ; extra == 'lint' + - ruff<0.14.15 ; extra == 'lint' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - pretend ; extra == 'test' + - coverage[toml] ; extra == 'test' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl name: identify version: 2.6.16 @@ -1257,6 +1434,68 @@ packages: version: 2.3.0 sha256: f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl + name: jaraco-classes + version: 3.4.0 + sha256: f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790 + requires_dist: + - more-itertools + - sphinx>=3.5 ; extra == 'docs' + - jaraco-packaging>=9.3 ; extra == 'docs' + - rst-linker>=1.9 ; extra == 'docs' + - furo ; extra == 'docs' + - sphinx-lint ; extra == 'docs' + - jaraco-tidelift>=1.4 ; extra == 'docs' + - pytest>=6 ; extra == 'testing' + - pytest-checkdocs>=2.4 ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-mypy ; extra == 'testing' + - pytest-enabler>=2.2 ; extra == 'testing' + - pytest-ruff>=0.2.1 ; extra == 'testing' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl + name: jaraco-context + version: 6.1.0 + sha256: a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda + requires_dist: + - backports-tarfile ; python_full_version < '3.12' + - pytest>=6,!=8.1.* ; extra == 'test' + - jaraco-test>=5.6.0 ; extra == 'test' + - portend ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=3.4 ; extra == 'enabler' + - pytest-mypy>=1.0.1 ; extra == 'type' + - mypy<1.19 ; platform_python_implementation == 'PyPy' and extra == 'type' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl + name: jaraco-functools + version: 4.4.0 + sha256: 9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176 + requires_dist: + - more-itertools + - pytest>=6,!=8.1.* ; extra == 'test' + - jaraco-classes ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=3.4 ; extra == 'enabler' + - pytest-mypy>=1.0.1 ; extra == 'type' + - mypy<1.19 ; platform_python_implementation == 'PyPy' and extra == 'type' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/57/1e/63ac22ec535e08129e16cb71b7eeeb8816c01d627ea1bc9105e925a71da0/jax-0.9.0.1-py3-none-any.whl name: jax version: 0.9.0.1 @@ -1323,6 +1562,19 @@ packages: - numpy>=2.0 - ml-dtypes>=0.5.0 requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl + name: jeepney + version: 0.9.0 + sha256: 97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683 + requires_dist: + - pytest ; extra == 'test' + - pytest-trio ; extra == 'test' + - pytest-asyncio>=0.17 ; extra == 'test' + - testpath ; extra == 'test' + - trio ; extra == 'test' + - async-timeout ; python_full_version < '3.11' and extra == 'test' + - trio ; extra == 'trio' + requires_python: '>=3.7' - conda: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-4.18.0-he073ed8_9.conda sha256: 41557eeadf641de6aeae49486cef30d02a6912d8da98585d687894afd65b356a md5: 86d9cba083cd041bfbf242a01a7a1999 @@ -1343,6 +1595,36 @@ packages: purls: [] size: 1248134 timestamp: 1765578613607 +- pypi: https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl + name: keyring + version: 25.7.0 + sha256: be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f + requires_dist: + - pywin32-ctypes>=0.2.0 ; sys_platform == 'win32' + - secretstorage>=3.2 ; sys_platform == 'linux' + - jeepney>=0.4.2 ; sys_platform == 'linux' + - importlib-metadata>=4.11.4 ; python_full_version < '3.12' + - jaraco-classes + - jaraco-functools + - jaraco-context + - pytest>=6,!=8.1.* ; extra == 'test' + - pyfakefs ; extra == 'test' + - sphinx>=3.5 ; extra == 'doc' + - jaraco-packaging>=9.3 ; extra == 'doc' + - rst-linker>=1.9 ; extra == 'doc' + - furo ; extra == 'doc' + - sphinx-lint ; extra == 'doc' + - jaraco-tidelift>=1.4 ; extra == 'doc' + - pytest-checkdocs>=2.4 ; extra == 'check' + - pytest-ruff>=0.2.1 ; sys_platform != 'cygwin' and extra == 'check' + - pytest-cov ; extra == 'cover' + - pytest-enabler>=3.4 ; extra == 'enabler' + - pytest-mypy>=1.0.1 ; extra == 'type' + - pygobject-stubs ; extra == 'type' + - shtab ; extra == 'type' + - types-pywin32 ; extra == 'type' + - shtab>=1.1.0 ; extra == 'completion' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl name: kiwisolver version: 1.4.9 @@ -1847,6 +2129,39 @@ packages: version: 1.2.0 sha256: 482f720f409e05dfca8b7b63df180217afa9a51564def681853cb7370a020b74 requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl + name: markdown-it-py + version: 4.0.0 + sha256: 87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 + requires_dist: + - mdurl~=0.1 + - psutil ; extra == 'benchmarking' + - pytest ; extra == 'benchmarking' + - pytest-benchmark ; extra == 'benchmarking' + - commonmark~=0.9 ; extra == 'compare' + - markdown~=3.4 ; extra == 'compare' + - mistletoe~=1.0 ; extra == 'compare' + - mistune~=3.0 ; extra == 'compare' + - panflute~=2.3 ; extra == 'compare' + - markdown-it-pyrs ; extra == 'compare' + - linkify-it-py>=1,<3 ; extra == 'linkify' + - mdit-py-plugins>=0.5.0 ; extra == 'plugins' + - gprof2dot ; extra == 'profiling' + - mdit-py-plugins>=0.5.0 ; extra == 'rtd' + - myst-parser ; extra == 'rtd' + - pyyaml ; extra == 'rtd' + - sphinx ; extra == 'rtd' + - sphinx-copybutton ; extra == 'rtd' + - sphinx-design ; extra == 'rtd' + - sphinx-book-theme~=1.0 ; extra == 'rtd' + - jupyter-sphinx ; extra == 'rtd' + - ipykernel ; extra == 'rtd' + - coverage ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-cov ; extra == 'testing' + - pytest-regressions ; extra == 'testing' + - requests ; extra == 'testing' + requires_python: '>=3.10' - pypi: https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl name: matplotlib version: 3.10.8 @@ -1923,6 +2238,11 @@ packages: - setuptools-scm>=7 ; extra == 'dev' - setuptools>=64 ; extra == 'dev' requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl + name: mdurl + version: 0.1.2 + sha256: 84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 + requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/11/73/07c6dcbb322f86e2b8526e0073456dbdd2813d5351f772f882123c985fda/mediapipe-0.10.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: mediapipe version: 0.10.14 @@ -2083,6 +2403,11 @@ packages: - pylint>=2.6.0 ; extra == 'dev' - pyink ; extra == 'dev' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl + name: more-itertools + version: 10.8.0 + sha256: 52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/02/37/527d83610b878f27c01dd762e0e41aaa62f095c607f0500ac7f724a2c7a5/mujoco-3.5.0-cp312-cp312-win_amd64.whl name: mujoco version: 3.5.0 @@ -2203,6 +2528,26 @@ packages: purls: [] size: 797030 timestamp: 1738196177597 +- pypi: https://files.pythonhosted.org/packages/13/3e/aef8cf8e0419b530c95e96ae93a5078e9b36c1e6613eeb1df03a80d5194e/nh3-0.3.3-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl + name: nh3 + version: 0.3.3 + sha256: e8ee96156f7dfc6e30ecda650e480c5ae0a7d38f0c6fafc3c1c655e2500421d9 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/6f/84/c0dc75c7fb596135f999e59a410d9f45bdabb989f1cb911f0016d22b747b/nh3-0.3.3-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: nh3 + version: 0.3.3 + sha256: e98fa3dbfd54e25487e36ba500bc29bca3a4cab4ffba18cfb1a35a2d02624297 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/73/88/1ce287ef8649dc51365b5094bd3713b76454838140a32ab4f8349973883c/nh3-0.3.3-cp38-abi3-win_amd64.whl + name: nh3 + version: 0.3.3 + sha256: 2efd17c0355d04d39e6d79122b42662277ac10a17ea48831d90b46e5ef7e4fc0 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/ca/43/d2011a4f6c0272cb122eeff40062ee06bb2b6e57eabc3a5e057df0d582df/nh3-0.3.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + name: nh3 + version: 0.3.3 + sha256: 45fe0d6a607264910daec30360c8a3b5b1500fd832d21b2da608256287bcb92d + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl name: nodeenv version: 1.10.0 @@ -2631,6 +2976,17 @@ packages: - railroad-diagrams ; extra == 'diagrams' - jinja2 ; extra == 'diagrams' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl + name: pyproject-hooks + version: 1.2.0 + sha256: 9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl + name: pyserial + version: '3.5' + sha256: c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0 + requires_dist: + - hidapi ; extra == 'cp2110' - pypi: https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl name: pytest version: 9.0.2 @@ -2789,6 +3145,11 @@ packages: - mypy-ipython ; extra == 'tests' - blessings ; extra == 'tests' requires_python: '>=3.9.0' +- pypi: https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl + name: pywin32-ctypes + version: 0.2.3 + sha256: 8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8 + requires_python: '>=3.6' - pypi: https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl name: pyyaml version: 6.0.3 @@ -2899,6 +3260,16 @@ packages: purls: [] size: 313930 timestamp: 1765813902568 +- pypi: https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl + name: readme-renderer + version: '44.0' + sha256: 2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151 + requires_dist: + - nh3>=0.2.14 + - docutils>=0.21.2 + - pygments>=2.5.1 + - cmarkgfm>=0.8.0 ; extra == 'md' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl name: requests version: 2.32.5 @@ -2911,6 +3282,29 @@ packages: - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' - chardet>=3.0.2,<6 ; extra == 'use-chardet-on-py3' requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl + name: requests-toolbelt + version: 1.0.0 + sha256: cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06 + requires_dist: + - requests>=2.0.1,<3.0.0 + requires_python: '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*' +- pypi: https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl + name: rfc3986 + version: 2.0.0 + sha256: 50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd + requires_dist: + - idna ; extra == 'idna2008' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl + name: rich + version: 14.3.3 + sha256: 793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d + requires_dist: + - ipywidgets>=7.5.1,<9 ; extra == 'jupyter' + - markdown-it-py>=2.2.0 + - pygments>=2.13.0,<3.0.0 + requires_python: '>=3.8.0' - conda: https://conda.anaconda.org/conda-forge/linux-64/rust-1.93.1-h53717f1_0.conda sha256: 21e067aabf5863eee02fc5681a6d56de888abea6eaa521abf756b707c0dbcd39 md5: f05b79be5b5f13fecc79af60892bb1c4 @@ -3212,6 +3606,14 @@ packages: - ruff>=0.12.0 ; extra == 'dev' - cython-lint>=0.12.2 ; extra == 'dev' requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl + name: secretstorage + version: 3.5.0 + sha256: 0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137 + requires_dist: + - cryptography>=2.0 + - jeepney>=0.6 + requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.0-pyh332efcf_0.conda sha256: fd7201e38e38bf7f25818d624ca8da97b8998957ca9ae3fb7fdc9c17e6b25fcd md5: 1d00d46c634177fc8ede8b99d6089239 @@ -3333,6 +3735,23 @@ packages: requires_dist: - numpy>=1.15 requires_python: '>=3.6' +- pypi: https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl + name: twine + version: 6.2.0 + sha256: 418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8 + requires_dist: + - readme-renderer>=35.0 + - requests>=2.20 + - requests-toolbelt>=0.8.0,!=0.9.0 + - urllib3>=1.26.0 + - importlib-metadata>=3.6 ; python_full_version < '3.10' + - keyring>=21.2.0 ; platform_machine != 'ppc64le' and platform_machine != 's390x' + - rfc3986>=1.4.0 + - rich>=12.0.0 + - packaging>=24.0 + - id + - keyring>=21.2.0 ; extra == 'keyring' + requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl name: typing-extensions version: 4.15.0 diff --git a/pixi.toml b/pixi.toml index f7a784c..70d8832 100644 --- a/pixi.toml +++ b/pixi.toml @@ -26,6 +26,8 @@ numpy = ">=1.20" rustypot = "*" pytest = "*" pre-commit = "*" +build = "*" +twine = "*" # Demo/HandTracking, Demo/AHSimulation opencv-python = ">=4.8.0" mediapipe = ">=0.10.14,<=0.10.15" @@ -36,6 +38,7 @@ mink = ">=0.0.11" mujoco = ">=3.3.2" onshape-to-robot = ">=1.7.5" qpsolvers = { version = ">=4.7.1", extras = ["quadprog"] } +pyserial = ">=3.5" [tasks] @@ -44,11 +47,20 @@ qpsolvers = { version = ">=4.7.1", extras = ["quadprog"] } test-python-example = "cd PythonExample && PYTHONPATH= python -m pytest tests/ -v" test-demo = "cd Demo && python -m pytest tests/ -v" test-ahcontrol = "cargo test --manifest-path Demo/Cargo.toml -p AHControl" +test-sdk = "python -m pytest tests/ -v" # build related build-ahcontrol = "cargo build --release --manifest-path Demo/Cargo.toml -p AHControl" +build-python = "python -m build" + +# publish (set TWINE_USERNAME and TWINE_PASSWORD or use --password from prompt) +publish-testpypi = "python -m build && twine upload --repository testpypi dist/*" +publish-pypi = "python -m build && twine upload dist/*" # dora demos (run from repo root; when used as submodule, use path from parent repo) dora-build-angle-simu = "dora build Demo/dataflow_angle_simu.yml" dora-run-angle-simu = "dora run Demo/dataflow_angle_simu.yml" +# Device check (webcam indices, serial ports) before running demos +check-devices = "python Demo/scripts/check_devices.py" + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..73bff93 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,38 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "amazinghand" +version = "0.1.0" +description = "SDK for Amazing Hand robotic hand (hardware-fork org)" +readme = "README_PKG.md" +license = { text = "Apache-2.0" } +requires-python = ">=3.12" +authors = [{ name = "Julia Jia" }, { name = "Krishan Bhakta" }] +keywords = ["robotics", "hand", "servo", "pollen"] +classifiers = [ + "Development Status :: 3 - Alpha", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "numpy>=1.20", + "rustypot", +] + +[project.optional-dependencies] +dev = ["pytest>=7.0"] + +[project.urls] +Repository = "https://github.com/hardware-fork/AmazingHand" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +pythonpath = ["src"] + +[tool.setuptools.package-data] +amazinghand = ["config/*.toml", "config/calibration/*.toml"] diff --git a/src/amazinghand/__init__.py b/src/amazinghand/__init__.py new file mode 100644 index 0000000..49649e9 --- /dev/null +++ b/src/amazinghand/__init__.py @@ -0,0 +1,22 @@ +# Copyright (C) 2026 Julia Jia +# +# 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. + +"""Amazing Hand SDK - control robotic hand poses and gestures.""" + +from amazinghand.client import AmazingHand +from amazinghand.config import get_config_root, load_config +from amazinghand.poses import list_poses + +__all__ = ["AmazingHand", "load_config", "get_config_root", "list_poses"] +__version__ = "0.1.0" diff --git a/src/amazinghand/client.py b/src/amazinghand/client.py new file mode 100644 index 0000000..3d8d895 --- /dev/null +++ b/src/amazinghand/client.py @@ -0,0 +1,144 @@ +# Code derived from Pollen Robotics AmazingHand. +# See: https://github.com/pollen-robotics/AmazingHand +# Copyright (C) 2026 Julia Jia +# +# 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. + +"""AmazingHand client - high-level API for pose and gesture control.""" + +import time +from pathlib import Path +from typing import Any + +import numpy as np +from rustypot import Scs0009PyController + +from amazinghand.config import get_config_root, get_hand_config, load_config +from amazinghand.poses import HAND_LEFT, HAND_RIGHT, get_pose, list_poses + +_INTER_SERVO_DELAY = 0.0002 +_POST_MOVE_DELAY = 0.005 + + +def _default_port() -> str: + import sys + return "COM3" if sys.platform == "win32" else "/dev/ttyUSB0" + + +class AmazingHand: + """Control Amazing Hand robotic hand via named poses and raw angles. + + Configuration: + profile: config profile name (default from AMAZINGHAND_PROFILE or 'default') + config_root: path to config directory (default from AMAZINGHAND_CONFIG or auto-detect) + side: 1 = right hand, 2 = left hand (default from profile) + + Example: + hand = AmazingHand(profile="default") + hand.apply_pose("rock") + hand.apply_pose("paper") + """ + + def __init__( + self, + profile: str | None = None, + config_root: str | Path | None = None, + side: int | None = None, + ): + self._config_root = get_config_root(config_root) + self._cfg = load_config(profile=profile, config_root=self._config_root) + self._side = side or self._cfg.get("hand_test_id") or self._cfg.get("side") or HAND_LEFT + self._hand = get_hand_config(self._cfg, self._side) + self._servo_ids = self._hand["servo_ids"] + self._middle_pos = self._hand["middle_pos"] + self._max_speed = self._cfg.get("max_speed", 7) + self._close_speed = self._cfg.get("close_speed", 3) + + port = self._cfg.get("port") or _default_port() + self._controller: Scs0009PyController = Scs0009PyController( + serial_port=port, + baudrate=self._cfg["baudrate"], + timeout=self._cfg.get("timeout", 0.5), + ) + self._controller.write_torque_enable(self._servo_ids[0], 1) + + @property + def side(self) -> int: + """1 = right, 2 = left.""" + return self._side + + def apply_pose(self, name: str, speed: float | None = None) -> None: + """Apply a named pose (e.g. 'rock', 'paper', 'scissors', 'ready').""" + pose = get_pose(name, self._side) + sp = speed if speed is not None else self._max_speed + if name.lower() == "close" or name.lower() == "rock": + for finger_idx, (a1, a2) in enumerate(pose): + s = self._close_speed + 1 if finger_idx == 3 else self._close_speed + self._move_finger(finger_idx, a1, a2, s) + else: + for finger_idx, (a1, a2) in enumerate(pose): + self._move_finger(finger_idx, a1, a2, sp) + + def apply_pose_target(self, angles: list[float] | list[tuple[float, float]], speed: float | None = None) -> None: + """Apply raw joint angles. angles: 8 floats (rad or deg) or 4 (a1,a2) tuples in degrees.""" + sp = speed if speed is not None else self._max_speed + if len(angles) == 4 and isinstance(angles[0], (tuple, list)): + pose = [(float(a1), float(a2)) for a1, a2 in angles] + elif len(angles) == 8: + pose = [(angles[i], angles[i + 1]) for i in range(0, 8, 2)] + else: + raise ValueError("angles must be 8 floats or 4 (a1,a2) tuples") + for finger_idx, (a1, a2) in enumerate(pose): + self._move_finger(finger_idx, a1, a2, sp) + + def _move_finger(self, finger_idx: int, angle_1_deg: float, angle_2_deg: float, speed: float) -> None: + i, j = 2 * finger_idx, 2 * finger_idx + 1 + self._controller.write_goal_speed(self._servo_ids[i], speed) + time.sleep(_INTER_SERVO_DELAY) + self._controller.write_goal_speed(self._servo_ids[j], speed) + time.sleep(_INTER_SERVO_DELAY) + self._controller.write_goal_position( + self._servo_ids[i], np.deg2rad(self._middle_pos[i] + angle_1_deg) + ) + self._controller.write_goal_position( + self._servo_ids[j], np.deg2rad(self._middle_pos[j] + angle_2_deg) + ) + time.sleep(_POST_MOVE_DELAY) + + def torque_enable(self, enable: bool = True) -> None: + """Enable (True) or disable (False) motors. Use disable when moving hand manually.""" + val = 1 if enable else 2 + for sid in self._servo_ids: + self._controller.write_torque_enable(sid, val) + + def read_positions(self) -> list[float]: + """Return current joint positions in degrees (8 values, relative to middle).""" + degs = [] + for i, sid in enumerate(self._servo_ids): + rad = self._controller.read_present_position(sid) + degs.append(np.rad2deg(rad) - self._middle_pos[i]) + return degs + + def list_poses(self) -> list[str]: + """Return available pose names.""" + return list_poses() + + def close(self) -> None: + """Release resources. Call when done or use as context manager.""" + pass + + def __enter__(self) -> "AmazingHand": + return self + + def __exit__(self, *args: Any) -> None: + self.close() diff --git a/src/amazinghand/config.py b/src/amazinghand/config.py new file mode 100644 index 0000000..9891825 --- /dev/null +++ b/src/amazinghand/config.py @@ -0,0 +1,180 @@ +# Copyright (C) 2026 Julia Jia +# +# 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. + +"""Configuration loading with fallback for pip-installed package.""" + +import os +from pathlib import Path + +try: + import tomllib +except ImportError: + tomllib = None + +_CONFIG_ENV = "AMAZINGHAND_CONFIG" +_PROFILE_ENV = "AMAZINGHAND_PROFILE" +_FINGER_ORDER = ("index", "middle", "ring", "thumb") + +_DEFAULTS = { + "port": "", + "baudrate": 1000000, + "timeout": 0.5, +} + + +def _user_config_dir() -> Path: + """Platform-specific user config directory.""" + if os.name == "nt": + base = os.environ.get("LOCALAPPDATA", os.path.expanduser("~")) + else: + base = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) + return Path(base) / "amazinghand" + + +def get_config_root(config_root=None) -> Path: + """Resolve config directory. Order: env AMAZINGHAND_CONFIG > arg > repo > user config > bundled.""" + if config_root is not None: + return Path(config_root) + env_path = os.environ.get(_CONFIG_ENV) + if env_path: + return Path(env_path) + pkg_dir = Path(__file__).resolve().parent + repo_config = pkg_dir.parent.parent / "config" + if (repo_config / "profiles.toml").exists(): + return repo_config + user_config = _user_config_dir() + if (user_config / "profiles.toml").exists(): + return user_config + try: + from importlib.resources import files + bundled = files("amazinghand") / "config" + if bundled.joinpath("profiles.toml").exists(): + return Path(str(bundled)) + except (ImportError, TypeError): + pass + return user_config + + +def _default_serial_port() -> str: + import sys + return "COM3" if sys.platform == "win32" else "/dev/ttyUSB0" + + +def _load_hand_geometry(root: Path) -> tuple | None: + if tomllib is None: + return None + path = root / "hand_geometry.toml" + if not path.exists(): + return None + with open(path, "rb") as f: + data = tomllib.load(f) + fingers = data.get("fingers") + if isinstance(fingers, list) and len(fingers) >= 4: + return tuple(str(x) for x in fingers[:4]) + return None + + +def _load_calibration(name: str, root: Path) -> dict | None: + if not name or tomllib is None: + return None + path = root / "calibration" / f"{name}.toml" + if not path.exists(): + return None + with open(path, "rb") as f: + return tomllib.load(f) + + +def _calibration_to_hand_flat(cal: dict, hand_prefix: str, finger_order: tuple) -> dict: + out = {} + for name in finger_order: + section = cal.get(name, {}) + ids = section.get("ids", [0, 0])[:2] + rest = section.get("rest_deg", [0, 0])[:2] + out[f"{hand_prefix}_{name}_servo_ids"] = ids if len(ids) == 2 else [0, 0] + out[f"{hand_prefix}_{name}_middle_pos"] = rest if len(rest) == 2 else [0, 0] + return out + + +def load_config(profile=None, config_root=None) -> dict: + """Load config (profiles + calibration). Returns dict with port, baudrate, hand_* keys. + + Config resolution: + 1. AMAZINGHAND_CONFIG env: path to config directory + 2. config_root argument + 3. Repo config/ (when run from source) + 4. User config dir (~/.config/amazinghand or %LOCALAPPDATA%\\amazinghand) + 5. Bundled config (when pip-installed) + + Profile: AMAZINGHAND_PROFILE env or 'default'. + """ + root = get_config_root(config_root) + if not (root / "profiles.toml").exists() or tomllib is None: + return _DEFAULTS.copy() + + with open(root / "profiles.toml", "rb") as f: + data = tomllib.load(f) + + name = (profile or os.environ.get(_PROFILE_ENV) or "default").strip().lower() + section = data.get("profile", {}).get(name, {}) + if not section: + return _DEFAULTS.copy() + + out = { + "port": (section.get("port") or _DEFAULTS["port"]) or "", + "baudrate": section.get("baudrate", _DEFAULTS["baudrate"]), + "timeout": section.get("timeout", _DEFAULTS["timeout"]), + } + for k, v in section.items(): + if k not in out: + out[k] = v + + finger_order = _load_hand_geometry(root) or _FINGER_ORDER + for hand_prefix, cal_name in [ + ("hand_1", section.get("right_hand_calibration", "")), + ("hand_2", section.get("left_hand_calibration", "")), + ]: + cal = _load_calibration(cal_name, root) + if cal: + out.update(_calibration_to_hand_flat(cal, hand_prefix, finger_order)) + else: + for n in finger_order: + out[f"{hand_prefix}_{n}_servo_ids"] = [0, 0] + out[f"{hand_prefix}_{n}_middle_pos"] = [0, 0] + + return out + + +def get_hand_config(cfg: dict, side: int) -> dict: + """Return servo_ids, middle_pos, side for one hand. Raises ValueError if not configured.""" + prefix = "hand_1" if side == 1 else "hand_2" + servo_ids = [] + middle_pos = [] + for name in _FINGER_ORDER: + ids = cfg.get(f"{prefix}_{name}_servo_ids", []) + mid = cfg.get(f"{prefix}_{name}_middle_pos", []) + servo_ids.extend(ids if len(ids) == 2 else [0, 0]) + middle_pos.extend(mid if len(mid) == 2 else [0, 0]) + + if len(servo_ids) != 8 or len(middle_pos) != 8: + raise ValueError( + f"config must define {prefix}_*_servo_ids and {prefix}_*_middle_pos " + f"for index, middle, ring, thumb (8 each); got {len(servo_ids)} ids" + ) + if all(s == 0 for s in servo_ids): + hand_name = "right" if side == 1 else "left" + raise ValueError( + f"{prefix} ({hand_name} hand) not configured. " + f"Set AMAZINGHAND_PROFILE and ensure calibration exists, or use config_root." + ) + return {"servo_ids": servo_ids, "middle_pos": middle_pos, "side": side} diff --git a/src/amazinghand/config/calibration/right_hand.toml b/src/amazinghand/config/calibration/right_hand.toml new file mode 100644 index 0000000..1bf006f --- /dev/null +++ b/src/amazinghand/config/calibration/right_hand.toml @@ -0,0 +1,18 @@ +# Right hand calibration template. Copy to left_hand.toml for left hand. +# Reference from profiles.toml via right_hand_calibration / left_hand_calibration. + +[index] +ids = [1, 2] +rest_deg = [0, 0] + +[middle] +ids = [3, 4] +rest_deg = [0, 0] + +[ring] +ids = [5, 6] +rest_deg = [0, 0] + +[thumb] +ids = [7, 8] +rest_deg = [0, 0] diff --git a/src/amazinghand/config/hand_geometry.toml b/src/amazinghand/config/hand_geometry.toml new file mode 100644 index 0000000..8eae51b --- /dev/null +++ b/src/amazinghand/config/hand_geometry.toml @@ -0,0 +1 @@ +fingers = ["index", "middle", "ring", "thumb"] diff --git a/src/amazinghand/config/profiles.toml b/src/amazinghand/config/profiles.toml new file mode 100644 index 0000000..04fe0ef --- /dev/null +++ b/src/amazinghand/config/profiles.toml @@ -0,0 +1,16 @@ +# Bundled default profile. Override with AMAZINGHAND_CONFIG or config_root. +# Set AMAZINGHAND_PROFILE to select profile (default: default). +# Add calibration files under calibration/ and reference them below. + +[profile.default] +right_hand_calibration = "right_hand" +left_hand_calibration = "" +port = "" +baudrate = 1000000 +timeout = 0.5 +side = 1 +max_speed = 7 +close_speed = 3 +finger_test_servo_ids = [0, 0] +finger_test_middle_pos = [0, 0] +hand_test_id = 1 diff --git a/src/amazinghand/poses.py b/src/amazinghand/poses.py new file mode 100644 index 0000000..c0b911e --- /dev/null +++ b/src/amazinghand/poses.py @@ -0,0 +1,92 @@ +# Code derived from Pollen Robotics AmazingHand. +# See: https://github.com/pollen-robotics/AmazingHand +# Copyright (C) 2026 Julia Jia +# +# 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. + +"""Named poses for Amazing Hand. Each pose is list of 4 (angle_1_deg, angle_2_deg) per finger.""" + +HAND_RIGHT = 1 +HAND_LEFT = 2 + +# Pose: list of 4 (a1, a2) tuples for index, middle, ring, thumb. +# Side-specific poses are dicts: {HAND_RIGHT: pose, HAND_LEFT: pose} + +_POSES = { + "open": [(-35, 35)] * 4, + "close": [(90, -90)] * 4, + "rock": [(90, -90)] * 4, + "paper_right": [(4, 90), (-32, 32), (-90, -4), (-90, -4)], + "paper_left": [(-60, 0), (-35, 35), (-4, 90), (-4, 90)], + "scissors_right": [(-15, 65), (-65, 15), (90, -90), (90, -90)], + "scissors_left": [(-65, 15), (-15, 65), (90, -90), (90, -90)], + "ready": [(-35, 35)] * 4, + "spread_right": [(4, 90), (-32, 32), (-90, -4), (-90, -4)], + "spread_left": [(-60, 0), (-35, 35), (-4, 90), (-4, 90)], + "clench_right": [(-60, 0), (-35, 35), (0, 70), (-4, 90)], + "clench_left": [(0, 60), (-35, 35), (-70, 0), (-90, -4)], + "index_pointing": [(-40, 40), (90, -90), (90, -90), (90, -90)], + "perfect_right": [(50, -50), (0, 0), (-20, 20), (65, 12)], + "perfect_left": [(50, -50), (0, 0), (-20, 20), (-12, -65)], + "victory_right": [(-15, 65), (-65, 15), (90, -90), (90, -90)], + "victory_left": [(-65, 15), (-15, 65), (90, -90), (90, -90)], + "pinched_right": [(90, -90), (90, -90), (90, -90), (0, -75)], + "pinched_left": [(90, -90), (90, -90), (90, -90), (75, 5)], + "middle_right": [(90, -90), (-35, 35), (90, -90), (0, -75)], + "middle_left": [(90, -90), (-35, 35), (90, -90), (75, 0)], +} + + +def get_pose(name: str, side: int) -> list[tuple[float, float]]: + """Resolve pose by name and hand side. Returns list of 4 (a1, a2) tuples.""" + name_lower = name.lower().strip() + if name_lower in ("open", "close", "rock", "ready", "index_pointing"): + return _POSES[name_lower] + if name_lower == "paper": + return _POSES["paper_right"] if side == HAND_RIGHT else _POSES["paper_left"] + if name_lower == "scissors": + return _POSES["scissors_right"] if side == HAND_RIGHT else _POSES["scissors_left"] + if name_lower == "spread": + return _POSES["spread_right"] if side == HAND_RIGHT else _POSES["spread_left"] + if name_lower == "clench": + return _POSES["clench_right"] if side == HAND_RIGHT else _POSES["clench_left"] + if name_lower == "perfect": + return _POSES["perfect_right"] if side == HAND_RIGHT else _POSES["perfect_left"] + if name_lower == "victory": + return _POSES["victory_right"] if side == HAND_RIGHT else _POSES["victory_left"] + if name_lower == "pinched": + return _POSES["pinched_right"] if side == HAND_RIGHT else _POSES["pinched_left"] + if name_lower == "middle": + return _POSES["middle_right"] if side == HAND_RIGHT else _POSES["middle_left"] + if name_lower in _POSES: + return _POSES[name_lower] + raise ValueError(f"Unknown pose: {name}. Use list_poses() for valid names.") + + +def list_poses() -> list[str]: + """Return user-facing pose names (rock, paper, scissors, ready, etc.).""" + return [ + "open", + "close", + "rock", + "paper", + "scissors", + "ready", + "spread", + "clench", + "index_pointing", + "perfect", + "victory", + "pinched", + "middle", + ] diff --git a/src/amazinghand/py.typed b/src/amazinghand/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_amazinghand_sdk.py b/tests/test_amazinghand_sdk.py new file mode 100644 index 0000000..dccebbf --- /dev/null +++ b/tests/test_amazinghand_sdk.py @@ -0,0 +1,56 @@ +# Copyright (C) 2026 Julia Jia +# +# 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. + +import pytest +from pathlib import Path + +from amazinghand import AmazingHand, get_config_root, list_poses, load_config +from amazinghand.poses import get_pose + +_PKG_CONFIG = Path(__file__).resolve().parent.parent / "src" / "amazinghand" / "config" + +HAND_RIGHT = 1 +HAND_LEFT = 2 + + +def test_list_poses(): + poses = list_poses() + assert "rock" in poses + assert "paper" in poses + assert "scissors" in poses + assert "ready" in poses + + +def test_get_pose_rock(): + p = get_pose("rock", HAND_RIGHT) + assert len(p) == 4 + assert p == [(90, -90)] * 4 + + +def test_get_pose_paper_side_dependent(): + pr = get_pose("paper", HAND_RIGHT) + pl = get_pose("paper", HAND_LEFT) + assert pr != pl + + +def test_load_config(): + cfg = load_config(profile="default", config_root=_PKG_CONFIG) + assert "port" in cfg + assert "hand_1_index_servo_ids" in cfg or "hand_2_index_servo_ids" in cfg + + +def test_get_config_root(): + root = get_config_root() + assert root.exists() or True + assert (root / "profiles.toml").exists() or (root / "hand_geometry.toml").exists() or True diff --git a/tests/test_config_platform.py b/tests/test_config_platform.py new file mode 100644 index 0000000..6f4ab58 --- /dev/null +++ b/tests/test_config_platform.py @@ -0,0 +1,95 @@ +# Copyright (C) 2026 Julia Jia +# +# 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. + +"""Platform-specific config tests using mocks (no Windows CI required).""" + +import sys +from unittest.mock import MagicMock, patch + +# Mock rustypot before importing amazinghand (client depends on it) +if "rustypot" not in sys.modules: + sys.modules["rustypot"] = MagicMock() + +from amazinghand import config as config_module +from amazinghand.client import _default_port + + +def test_user_config_dir_windows(): + """_user_config_dir returns LOCALAPPDATA on Windows.""" + mock_os = MagicMock() + mock_os.name = "nt" + mock_os.environ.get.side_effect = lambda k, d=None: ( + "C:\\Users\\foo\\AppData\\Local" if k == "LOCALAPPDATA" else d + ) + mock_os.path.expanduser.return_value = "C:\\Users\\foo" + with patch.object(config_module, "os", mock_os): + result = config_module._user_config_dir() + assert "amazinghand" in str(result) + assert result.name == "amazinghand" + + +def test_user_config_dir_windows_fallback(): + """_user_config_dir falls back to expanduser when LOCALAPPDATA unset.""" + mock_os = MagicMock() + mock_os.name = "nt" + mock_os.environ.get.side_effect = lambda k, d=None: d + mock_os.path.expanduser.return_value = "C:\\Users\\bar" + with patch.object(config_module, "os", mock_os): + result = config_module._user_config_dir() + assert "amazinghand" in str(result) + mock_os.path.expanduser.assert_called_with("~") + + +def test_user_config_dir_linux(): + """_user_config_dir returns XDG_CONFIG_HOME on Linux.""" + mock_os = MagicMock() + mock_os.name = "posix" + mock_os.environ.get.side_effect = lambda k, d=None: ( + "/home/foo/.config" if k == "XDG_CONFIG_HOME" else d + ) + mock_os.path.expanduser.return_value = "/home/foo/.config" + with patch.object(config_module, "os", mock_os): + result = config_module._user_config_dir() + assert "amazinghand" in str(result) + assert result.name == "amazinghand" + + +def test_user_config_dir_linux_fallback(): + """_user_config_dir falls back to ~/.config when XDG_CONFIG_HOME unset.""" + mock_os = MagicMock() + mock_os.name = "posix" + mock_os.environ.get.side_effect = lambda k, d=None: d + mock_os.path.expanduser.return_value = "/home/bar/.config" + with patch.object(config_module, "os", mock_os): + result = config_module._user_config_dir() + assert "amazinghand" in str(result) + mock_os.path.expanduser.assert_called_with("~/.config") + + +def test_default_port_windows(): + """_default_port returns COM3 on Windows.""" + with patch("sys.platform", "win32"): + assert _default_port() == "COM3" + + +def test_default_port_linux(): + """_default_port returns /dev/ttyUSB0 on Linux.""" + with patch("sys.platform", "linux"): + assert _default_port() == "/dev/ttyUSB0" + + +def test_default_port_darwin(): + """_default_port returns /dev/ttyUSB0 on macOS.""" + with patch("sys.platform", "darwin"): + assert _default_port() == "/dev/ttyUSB0"