From 7781f39154e3f587eaf3b9cbe20436e261d4a7f2 Mon Sep 17 00:00:00 2001 From: Piotr Duda Date: Wed, 18 Mar 2026 08:34:30 +0100 Subject: [PATCH 01/11] Remove stale files + smarter CI --- .github/workflows/ci.yml | 14 ++++++++++++++ .github/workflows/compat.yml | 12 ++++++++++++ CHANGELOG.md | 32 -------------------------------- 3 files changed, 26 insertions(+), 32 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eef4186..c9d68b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,22 @@ name: CI on: push: branches: [ main, devel ] + paths: + - 'wildedge/**' + - 'tests/**' + - 'scripts/**' + - 'pyproject.toml' + - 'uv.lock' + - '.github/workflows/ci.yml' pull_request: branches: [ main, devel ] + paths: + - 'wildedge/**' + - 'tests/**' + - 'scripts/**' + - 'pyproject.toml' + - 'uv.lock' + - '.github/workflows/ci.yml' jobs: test: diff --git a/.github/workflows/compat.yml b/.github/workflows/compat.yml index 30cb542..392a299 100644 --- a/.github/workflows/compat.yml +++ b/.github/workflows/compat.yml @@ -3,9 +3,21 @@ name: Compatibility on: push: branches: [ main, devel ] + paths: + - 'wildedge/**' + - 'tests/compat/**' + - 'scripts/compat_matrix.py' + - 'pyproject.toml' + - '.github/workflows/compat.yml' pull_request: branches: [ main, devel ] types: [opened, synchronize, reopened, labeled] + paths: + - 'wildedge/**' + - 'tests/compat/**' + - 'scripts/compat_matrix.py' + - 'pyproject.toml' + - '.github/workflows/compat.yml' schedule: - cron: "0 3 * * *" workflow_dispatch: diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index cd804b0..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,32 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [0.1.0] - 2026-03-05 - -### Added -- Initial release of WildEdge Python SDK -- Support for ONNX Runtime, GGUF/llama.cpp, timm, PyTorch, and Keras integrations -- Automatic latency and error tracking without capturing inputs/outputs -- Event batching and transmission to WildEdge servers -- Configuration via DSN and environment variables -- Comprehensive test suite with tox support for multiple Python versions -- CI/CD pipeline with GitHub Actions - -### Changed -- N/A - -### Deprecated -- N/A - -### Removed -- N/A - -### Fixed -- N/A - -### Security -- N/A \ No newline at end of file From 9b95783f86390a62dbb82446c7dd3c93715e79f2 Mon Sep 17 00:00:00 2001 From: Piotr Duda Date: Thu, 19 Mar 2026 08:44:47 +0100 Subject: [PATCH 02/11] Smarter CI + coverage updates for the compatibility matrix (#21) --- .github/workflows/compat.yml | 34 +++++++++- scripts/compat_matrix.py | 44 ++++++++++++ scripts/run_compat_local.py | 86 +++++++++++++++--------- tests/compat/test_gguf_compat.py | 9 +++ tests/compat/test_mlx_compat.py | 17 +++++ tests/compat/test_openai_compat.py | 9 +++ tests/compat/test_transformers_compat.py | 25 +++++++ tests/compat/test_ultralytics_compat.py | 9 +++ 8 files changed, 199 insertions(+), 34 deletions(-) create mode 100644 tests/compat/test_gguf_compat.py create mode 100644 tests/compat/test_mlx_compat.py create mode 100644 tests/compat/test_openai_compat.py create mode 100644 tests/compat/test_transformers_compat.py create mode 100644 tests/compat/test_ultralytics_compat.py diff --git a/.github/workflows/compat.yml b/.github/workflows/compat.yml index 392a299..a411710 100644 --- a/.github/workflows/compat.yml +++ b/.github/workflows/compat.yml @@ -30,7 +30,7 @@ jobs: fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13"] - integration: ["onnx", "torch", "timm", "tensorflow"] + integration: ["onnx", "torch", "timm", "tensorflow", "gguf", "openai", "transformers", "ultralytics"] version-set: ["min", "current"] exclude: - python-version: "3.13" @@ -44,7 +44,7 @@ jobs: - name: Install project run: | python -m pip install --upgrade pip - pip install -e . pytest + pip install -e . pytest pytest-asyncio pytest-mock - name: Install compatibility dependencies run: | python scripts/compat_matrix.py deps \ @@ -55,6 +55,34 @@ jobs: - name: Run compatibility test run: python -m pytest "tests/compat/test_${{ matrix.integration }}_compat.py" -q + compat-mlx: + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: macos-14 + strategy: + fail-fast: false + matrix: + python-version: ["3.12", "3.13"] + version-set: ["current"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install project + run: | + python -m pip install --upgrade pip + pip install -e . pytest pytest-asyncio pytest-mock + - name: Install compatibility dependencies + run: | + python scripts/compat_matrix.py deps \ + --integration "mlx" \ + --version-set "${{ matrix.version-set }}" \ + --python-version "${{ matrix.python-version }}" > compat-requirements.txt + pip install -r compat-requirements.txt + - name: Run compatibility test + run: python -m pytest tests/compat/test_mlx_compat.py -q + compat-canary-314: if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'compat') runs-on: ubuntu-latest @@ -75,7 +103,7 @@ jobs: - name: Install project run: | python -m pip install --upgrade pip - pip install -e . pytest + pip install -e . pytest pytest-asyncio pytest-mock - name: Install compatibility dependencies run: | python scripts/compat_matrix.py deps \ diff --git a/scripts/compat_matrix.py b/scripts/compat_matrix.py index 18f48dd..610df72 100644 --- a/scripts/compat_matrix.py +++ b/scripts/compat_matrix.py @@ -4,6 +4,7 @@ from __future__ import annotations import argparse +import json import sys MATRIX = { @@ -33,6 +34,25 @@ "min": ["tensorflow==2.16.1", "keras==3.3.3", "numpy==1.26.4"], "current": ["tensorflow==2.18.0", "keras==3.8.0", "numpy==2.0.2"], }, + "gguf": { + "min": ["llama-cpp-python==0.2.90", "numpy==1.26.4"], + "current": ["llama-cpp-python==0.3.4", "numpy==2.1.3"], + }, + "openai": { + "min": ["openai==1.30.0"], + "current": ["openai==1.61.0"], + }, + "transformers": { + "min": ["transformers==4.40.0", "torch==2.4.1", "numpy==1.26.4"], + "current": ["transformers==4.47.0", "torch==2.6.0", "numpy==2.1.3"], + }, + "ultralytics": { + "min": ["ultralytics==8.0.0", "numpy==1.26.4"], + "current": ["ultralytics==8.3.4", "numpy==2.1.3"], + }, + "mlx": { + "current": ["mlx==0.22.0", "mlx-lm==0.21.0"], + }, } SUPPORTED_PYTHON = { @@ -40,6 +60,11 @@ "torch": ["3.10", "3.11", "3.12", "3.13", "3.14"], "timm": ["3.10", "3.11", "3.12", "3.13", "3.14"], "tensorflow": ["3.10", "3.11", "3.12"], + "gguf": ["3.10", "3.11", "3.12", "3.13"], + "openai": ["3.10", "3.11", "3.12", "3.13"], + "transformers": ["3.10", "3.11", "3.12", "3.13"], + "ultralytics": ["3.10", "3.11", "3.12", "3.13"], + "mlx": ["3.12", "3.13"], } # Interpreter-specific overrides where upstream wheels are unavailable for older pins. @@ -58,6 +83,11 @@ "3.14": ["torch==2.10.0", "numpy==2.1.3"], }, }, + "transformers": { + "min": { + "3.13": ["transformers==4.45.0", "torch==2.5.0", "numpy==2.1.3"], + } + }, "timm": { "min": { "3.13": [ @@ -113,6 +143,17 @@ def print_deps(integration: str, version_set: str, python_version: str) -> int: return 0 +def print_rows() -> int: + rows = [ + {"integration": integration, "version_set": version_set, "python_version": py} + for integration, sets in MATRIX.items() + for version_set in sets + for py in SUPPORTED_PYTHON[integration] + ] + print(json.dumps(rows)) + return 0 + + def print_table() -> int: print("| Integration | Version set | Dependencies | Supported Python |") print("|---|---|---|---|") @@ -140,11 +181,14 @@ def main() -> int: deps.add_argument("--version-set", required=True, choices=["min", "current"]) deps.add_argument("--python-version", required=True) + sub.add_parser("rows") sub.add_parser("table") args = parser.parse_args() if args.cmd == "deps": return print_deps(args.integration, args.version_set, args.python_version) + if args.cmd == "rows": + return print_rows() if args.cmd == "table": return print_table() return 2 diff --git a/scripts/run_compat_local.py b/scripts/run_compat_local.py index 8ab3846..44fe82f 100755 --- a/scripts/run_compat_local.py +++ b/scripts/run_compat_local.py @@ -4,8 +4,12 @@ from __future__ import annotations import argparse +import json +import os import subprocess import sys +import tempfile +from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import dataclass from pathlib import Path @@ -19,18 +23,18 @@ class Row: version_set: str -WORKFLOW_ROWS: list[Row] = [ - # compat job - *[ - Row(py, integration, version_set) - for py in ("3.10", "3.11", "3.12", "3.13") - for integration in ("onnx", "torch", "timm", "tensorflow") - for version_set in ("min", "current") - if not (py == "3.13" and integration == "tensorflow") - ], - # compat-canary-314 job - *[Row("3.14", integration, "current") for integration in ("torch", "timm")], -] +def load_rows() -> list[Row]: + result = subprocess.run( + [sys.executable, str(REPO_ROOT / "scripts" / "compat_matrix.py"), "rows"], + check=True, + capture_output=True, + text=True, + cwd=REPO_ROOT, + ) + return [Row(**r) for r in json.loads(result.stdout)] + + +WORKFLOW_ROWS: list[Row] = load_rows() UNSUPPORTED_MARKERS = ( @@ -67,10 +71,15 @@ def run_row(row: Row) -> tuple[str, str]: "run", "--python", row.python_version, + "--link-mode=copy", "--with-editable", ".", "--with", "pytest", + "--with", + "pytest-asyncio", + "--with", + "pytest-mock", ] for dep in deps: cmd.extend(["--with", dep]) @@ -84,9 +93,11 @@ def run_row(row: Row) -> tuple[str, str]: ] ) - result = subprocess.run( - cmd, check=False, capture_output=True, text=True, cwd=REPO_ROOT - ) + with tempfile.TemporaryDirectory() as tmpdir: + env = {**os.environ, "UV_PROJECT_ENVIRONMENT": tmpdir} + result = subprocess.run( + cmd, check=False, capture_output=True, text=True, cwd=REPO_ROOT, env=env + ) output = f"{result.stdout}\n{result.stderr}".strip() if result.returncode == 0: @@ -103,29 +114,42 @@ def main() -> int: action="store_true", help="Treat unsupported dependency rows as failures.", ) + parser.add_argument( + "--jobs", + "-j", + type=int, + default=os.cpu_count() or 4, + help="Number of rows to run in parallel (default: cpu count).", + ) args = parser.parse_args() passed = 0 failed = 0 skipped = 0 - for row in WORKFLOW_ROWS: - label = f"{row.python_version} | {row.integration} | {row.version_set}" - print(f"==> {label}") - status, output = run_row(row) - print(status) - if output: - print(output) - print() - - if status == "PASS": - passed += 1 - elif status == "SKIP_UNSUPPORTED": - skipped += 1 - if args.strict_unsupported: + futures = {} + with ThreadPoolExecutor(max_workers=args.jobs) as executor: + for row in WORKFLOW_ROWS: + futures[executor.submit(run_row, row)] = row + + for future in as_completed(futures): + row = futures[future] + label = f"{row.python_version} | {row.integration} | {row.version_set}" + status, output = future.result() + print(f"==> {label}") + print(status) + if output: + print(output) + print() + + if status == "PASS": + passed += 1 + elif status == "SKIP_UNSUPPORTED": + skipped += 1 + if args.strict_unsupported: + failed += 1 + else: failed += 1 - else: - failed += 1 print( "SUMMARY " diff --git a/tests/compat/test_gguf_compat.py b/tests/compat/test_gguf_compat.py new file mode 100644 index 0000000..9512db6 --- /dev/null +++ b/tests/compat/test_gguf_compat.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +import pytest + + +def test_gguf_import_and_instrument(compat_client): + llama_cpp = pytest.importorskip("llama_cpp") + assert hasattr(llama_cpp, "Llama") + compat_client.instrument("gguf") diff --git a/tests/compat/test_mlx_compat.py b/tests/compat/test_mlx_compat.py new file mode 100644 index 0000000..d66db56 --- /dev/null +++ b/tests/compat/test_mlx_compat.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import pytest + + +def test_mlx_import_and_instrumentation(compat_client): + pytest.importorskip("mlx_lm") + mx = pytest.importorskip("mlx.core") + nn = pytest.importorskip("mlx.nn") + + compat_client.instrument("mlx") + + model = nn.Linear(4, 2) + x = mx.ones((3, 4)) + y = model(x) + mx.eval(y) + assert y.shape == (3, 2) diff --git a/tests/compat/test_openai_compat.py b/tests/compat/test_openai_compat.py new file mode 100644 index 0000000..a704734 --- /dev/null +++ b/tests/compat/test_openai_compat.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +import pytest + + +def test_openai_import_and_instrument(compat_client): + openai = pytest.importorskip("openai") + assert getattr(openai, "__version__", None) + compat_client.instrument("openai") diff --git a/tests/compat/test_transformers_compat.py b/tests/compat/test_transformers_compat.py new file mode 100644 index 0000000..01b22cf --- /dev/null +++ b/tests/compat/test_transformers_compat.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import pytest + + +def test_transformers_import_and_instrumentation(compat_client): + torch = pytest.importorskip("torch") + transformers = pytest.importorskip("transformers") + + compat_client.instrument("transformers") + + config = transformers.BertConfig( + hidden_size=32, + num_hidden_layers=2, + num_attention_heads=2, + intermediate_size=64, + num_labels=2, + ) + model = transformers.BertForSequenceClassification(config) + inputs = { + "input_ids": torch.zeros((1, 4), dtype=torch.long), + "attention_mask": torch.ones((1, 4), dtype=torch.long), + } + out = model(**inputs) + assert tuple(out.logits.shape) == (1, 2) diff --git a/tests/compat/test_ultralytics_compat.py b/tests/compat/test_ultralytics_compat.py new file mode 100644 index 0000000..6f6e5e5 --- /dev/null +++ b/tests/compat/test_ultralytics_compat.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +import pytest + + +def test_ultralytics_import_and_instrument(compat_client): + ultralytics = pytest.importorskip("ultralytics") + assert getattr(ultralytics, "__version__", None) + compat_client.instrument("ultralytics") From 60d22663bce810075879b29e8507d8b23bb7c8ec Mon Sep 17 00:00:00 2001 From: Piotr Duda Date: Thu, 19 Mar 2026 10:18:24 +0100 Subject: [PATCH 03/11] Fixes #23 (#24) --- examples/cli/uv.lock | 10 +- examples/django_gemma/uv.lock | 3 +- examples/fastapi_openai/app/__init__.py | 0 examples/fastapi_openai/app/main.py | 54 ++ examples/fastapi_openai/app/static/index.html | 39 + examples/fastapi_openai/demo.sh | 37 + examples/fastapi_openai/pyproject.toml | 13 + examples/fastapi_openai/uv.lock | 844 ++++++++++++++++++ tests/test_autoload.py | 91 +- wildedge/autoload/sitecustomize.py | 15 +- wildedge/constants.py | 1 - 11 files changed, 1086 insertions(+), 21 deletions(-) create mode 100644 examples/fastapi_openai/app/__init__.py create mode 100644 examples/fastapi_openai/app/main.py create mode 100644 examples/fastapi_openai/app/static/index.html create mode 100755 examples/fastapi_openai/demo.sh create mode 100644 examples/fastapi_openai/pyproject.toml create mode 100644 examples/fastapi_openai/uv.lock diff --git a/examples/cli/uv.lock b/examples/cli/uv.lock index b0e3583..6103e3b 100644 --- a/examples/cli/uv.lock +++ b/examples/cli/uv.lock @@ -948,6 +948,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0f/8b/4b61d6e13f7108f36910df9ab4b58fd389cc2520d54d81b88660804aad99/torch-2.10.0-2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:418997cb02d0a0f1497cf6a09f63166f9f5df9f3e16c8a716ab76a72127c714f", size = 79423467, upload-time = "2026-02-10T21:44:48.711Z" }, { url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" }, { url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/16/ee/efbd56687be60ef9af0c9c0ebe106964c07400eade5b0af8902a1d8cd58c/torch-2.10.0-3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a1ff626b884f8c4e897c4c33782bdacdff842a165fee79817b1dd549fdda1321", size = 915510070, upload-time = "2026-03-11T14:16:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/7b562f1808d3f65414cd80a4f7d4bb00979d9355616c034c171249e1a303/torch-2.10.0-3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac5bdcbb074384c66fa160c15b1ead77839e3fe7ed117d667249afce0acabfac", size = 915518691, upload-time = "2026-03-11T14:15:43.147Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/abada41517ce0011775f0f4eacc79659bc9bc6c361e6bfe6f7052a6b9363/torch-2.10.0-3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:98c01b8bb5e3240426dcde1446eed6f40c778091c8544767ef1168fc663a05a6", size = 915622781, upload-time = "2026-03-11T14:17:11.354Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c6/4dfe238342ffdcec5aef1c96c457548762d33c40b45a1ab7033bb26d2ff2/torch-2.10.0-3-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:80b1b5bfe38eb0e9f5ff09f206dcac0a87aadd084230d4a36eea5ec5232c115b", size = 915627275, upload-time = "2026-03-11T14:16:11.325Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/72bf18847f58f877a6a8acf60614b14935e2f156d942483af1ffc081aea0/torch-2.10.0-3-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:46b3574d93a2a8134b3f5475cfb98e2eb46771794c57015f6ad1fb795ec25e49", size = 915523474, upload-time = "2026-03-11T14:17:44.422Z" }, + { url = "https://files.pythonhosted.org/packages/f4/39/590742415c3030551944edc2ddc273ea1fdfe8ffb2780992e824f1ebee98/torch-2.10.0-3-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b1d5e2aba4eb7f8e87fbe04f86442887f9167a35f092afe4c237dfcaaef6e328", size = 915632474, upload-time = "2026-03-11T14:15:13.666Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8e/34949484f764dde5b222b7fe3fede43e4a6f0da9d7f8c370bb617d629ee2/torch-2.10.0-3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:0228d20b06701c05a8f978357f657817a4a63984b0c90745def81c18aedfa591", size = 915523882, upload-time = "2026-03-11T14:14:46.311Z" }, { url = "https://files.pythonhosted.org/packages/0c/1a/c61f36cfd446170ec27b3a4984f072fd06dab6b5d7ce27e11adb35d6c838/torch-2.10.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5276fa790a666ee8becaffff8acb711922252521b28fbce5db7db5cf9cb2026d", size = 145992962, upload-time = "2026-01-21T16:24:14.04Z" }, { url = "https://files.pythonhosted.org/packages/b5/60/6662535354191e2d1555296045b63e4279e5a9dbad49acf55a5d38655a39/torch-2.10.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:aaf663927bcd490ae971469a624c322202a2a1e68936eb952535ca4cd3b90444", size = 915599237, upload-time = "2026-01-21T16:23:25.497Z" }, { url = "https://files.pythonhosted.org/packages/40/b8/66bbe96f0d79be2b5c697b2e0b187ed792a15c6c4b8904613454651db848/torch-2.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:a4be6a2a190b32ff5c8002a0977a25ea60e64f7ba46b1be37093c141d9c49aeb", size = 113720931, upload-time = "2026-01-21T16:24:23.743Z" }, @@ -1088,7 +1095,7 @@ requires-dist = [ [[package]] name = "wildedge-sdk" -version = "0.1.0" +version = "0.1.1" source = { editable = "../../" } [package.metadata] @@ -1097,6 +1104,7 @@ source = { editable = "../../" } dev = [ { name = "coverage" }, { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-asyncio", specifier = ">=0.25" }, { name = "pytest-mock", specifier = ">=3.15.1" }, { name = "ruff", specifier = ">=0.15.4" }, { name = "tox" }, diff --git a/examples/django_gemma/uv.lock b/examples/django_gemma/uv.lock index 1a86ad7..ca6b004 100644 --- a/examples/django_gemma/uv.lock +++ b/examples/django_gemma/uv.lock @@ -728,7 +728,7 @@ requires-dist = [ [[package]] name = "wildedge-sdk" -version = "0.1.0" +version = "0.1.1" source = { editable = "../../" } [package.metadata] @@ -737,6 +737,7 @@ source = { editable = "../../" } dev = [ { name = "coverage" }, { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-asyncio", specifier = ">=0.25" }, { name = "pytest-mock", specifier = ">=3.15.1" }, { name = "ruff", specifier = ">=0.15.4" }, { name = "tox" }, diff --git a/examples/fastapi_openai/app/__init__.py b/examples/fastapi_openai/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/fastapi_openai/app/main.py b/examples/fastapi_openai/app/main.py new file mode 100644 index 0000000..4ba3c5d --- /dev/null +++ b/examples/fastapi_openai/app/main.py @@ -0,0 +1,54 @@ +"""FastAPI + OpenRouter example. + +WildEdge is injected via `wildedge run` before the app starts, so inference +tracking happens automatically for every chat.completions.create call. + +Run with: see demo.sh +Requires: WILDEDGE_DSN and OPENROUTER_API_KEY environment variables. +""" + +import os +import pathlib + +from fastapi import FastAPI +from fastapi.responses import FileResponse +from openai import OpenAI +from pydantic import BaseModel + +STATIC = pathlib.Path(__file__).parent / "static" + +app = FastAPI() + + +@app.get("/") +def index() -> FileResponse: + return FileResponse(STATIC / "index.html") + + +client = OpenAI( + api_key=os.environ["OPENROUTER_API_KEY"], + base_url="https://openrouter.ai/api/v1", +) + + +class ChatRequest(BaseModel): + prompt: str + model: str = "openai/gpt-4o-mini" + + +class ChatResponse(BaseModel): + response: str + model: str + + +@app.post("/chat", response_model=ChatResponse) +def chat(req: ChatRequest) -> ChatResponse: + completion = client.chat.completions.create( + model=req.model, + messages=[{"role": "user", "content": req.prompt}], + max_tokens=512, + ) + return ChatResponse( + response=completion.choices[0].message.content, + model=completion.model, + ) diff --git a/examples/fastapi_openai/app/static/index.html b/examples/fastapi_openai/app/static/index.html new file mode 100644 index 0000000..e10ba6d --- /dev/null +++ b/examples/fastapi_openai/app/static/index.html @@ -0,0 +1,39 @@ + + + + + WildEdge + OpenRouter + + + +

WildEdge + OpenRouter

+ +
+ +

+ +
+
+ + + diff --git a/examples/fastapi_openai/demo.sh b/examples/fastapi_openai/demo.sh new file mode 100755 index 0000000..21a7ca0 --- /dev/null +++ b/examples/fastapi_openai/demo.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ -z "${WILDEDGE_DSN:-}" ]]; then + echo 'Set WILDEDGE_DSN first, e.g. export WILDEDGE_DSN="https://@ingest.wildedge.dev/"' >&2 + exit 1 +fi + +if [[ -z "${OPENROUTER_API_KEY:-}" ]]; then + echo 'Set OPENROUTER_API_KEY first, e.g. export OPENROUTER_API_KEY="sk-or-..."' >&2 + exit 1 +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +uv sync + +uv run wildedge doctor --integrations openai + +# wildedge run execs uvicorn with wildedge/autoload/ prepended to PYTHONPATH. +# sitecustomize.py bootstraps the runtime before the app loads, instrumenting +# the OpenAI client for automatic inference tracking. +# +# --reload spawns a fresh worker process via exec each time code changes. +# The module-level guard in sitecustomize.py ensures that worker bootstraps +# correctly (unlike the old os.environ guard, which propagated across exec +# and blocked the worker's init). +uv run wildedge run \ + --print-startup-report \ + --integrations openai \ + -- uvicorn app.main:app --reload --port 8000 + +# Test with: +# curl -s -X POST http://localhost:8000/chat \ +# -H "Content-Type: application/json" \ +# -d '{"prompt": "What is on-device AI in one sentence?"}' | jq . diff --git a/examples/fastapi_openai/pyproject.toml b/examples/fastapi_openai/pyproject.toml new file mode 100644 index 0000000..06d118c --- /dev/null +++ b/examples/fastapi_openai/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "wildedge-fastapi-openrouter" +version = "0.1.0" +requires-python = ">=3.10" +dependencies = [ + "wildedge-sdk", + "fastapi", + "uvicorn[standard]", + "openai", +] + +[tool.uv.sources] +wildedge-sdk = { path = "../..", editable = true } diff --git a/examples/fastapi_openai/uv.lock b/examples/fastapi_openai/uv.lock new file mode 100644 index 0000000..fff8ef0 --- /dev/null +++ b/examples/fastapi_openai/uv.lock @@ -0,0 +1,844 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fastapi" +version = "0.135.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/7b/f8e0211e9380f7195ba3f3d40c292594fd81ba8ec4629e3854c353aaca45/fastapi-0.135.1.tar.gz", hash = "sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd", size = 394962, upload-time = "2026-03-01T18:18:29.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", size = 204531, upload-time = "2025-10-10T03:54:20.887Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", size = 109408, upload-time = "2025-10-10T03:54:22.455Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", size = 440889, upload-time = "2025-10-10T03:54:23.753Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", size = 440460, upload-time = "2025-10-10T03:54:25.313Z" }, + { url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", size = 425267, upload-time = "2025-10-10T03:54:26.81Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", size = 424429, upload-time = "2025-10-10T03:54:28.174Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", size = 86173, upload-time = "2025-10-10T03:54:29.5Z" }, + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "jiter" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/5a/41da76c5ea07bec1b0472b6b2fdb1b651074d504b19374d7e130e0cdfb25/jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", size = 311164, upload-time = "2026-02-02T12:35:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/40/cb/4a1bf994a3e869f0d39d10e11efb471b76d0ad70ecbfb591427a46c880c2/jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", size = 320296, upload-time = "2026-02-02T12:35:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/09/82/acd71ca9b50ecebadc3979c541cd717cce2fe2bc86236f4fa597565d8f1a/jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", size = 352742, upload-time = "2026-02-02T12:35:21.258Z" }, + { url = "https://files.pythonhosted.org/packages/71/03/d1fc996f3aecfd42eb70922edecfb6dd26421c874503e241153ad41df94f/jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", size = 363145, upload-time = "2026-02-02T12:35:24.653Z" }, + { url = "https://files.pythonhosted.org/packages/f1/61/a30492366378cc7a93088858f8991acd7d959759fe6138c12a4644e58e81/jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", size = 487683, upload-time = "2026-02-02T12:35:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/4223cffa9dbbbc96ed821c5aeb6bca510848c72c02086d1ed3f1da3d58a7/jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", size = 373579, upload-time = "2026-02-02T12:35:27.582Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c9/b0489a01329ab07a83812d9ebcffe7820a38163c6d9e7da644f926ff877c/jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", size = 362904, upload-time = "2026-02-02T12:35:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/05/af/53e561352a44afcba9a9bc67ee1d320b05a370aed8df54eafe714c4e454d/jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", size = 392380, upload-time = "2026-02-02T12:35:30.385Z" }, + { url = "https://files.pythonhosted.org/packages/76/2a/dd805c3afb8ed5b326c5ae49e725d1b1255b9754b1b77dbecdc621b20773/jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", size = 517939, upload-time = "2026-02-02T12:35:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/7b67d76f55b8fe14c937e7640389612f05f9a4145fc28ae128aaa5e62257/jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", size = 551696, upload-time = "2026-02-02T12:35:33.306Z" }, + { url = "https://files.pythonhosted.org/packages/85/9c/57cdd64dac8f4c6ab8f994fe0eb04dc9fd1db102856a4458fcf8a99dfa62/jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894", size = 204592, upload-time = "2026-02-02T12:35:34.58Z" }, + { url = "https://files.pythonhosted.org/packages/a7/38/f4f3ea5788b8a5bae7510a678cdc747eda0c45ffe534f9878ff37e7cf3b3/jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d", size = 206016, upload-time = "2026-02-02T12:35:36.435Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, + { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, + { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, + { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, + { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, + { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, + { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +] + +[[package]] +name = "openai" +version = "2.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/15/203d537e58986b5673e7f232453a2a2f110f22757b15921cbdeea392e520/openai-2.29.0.tar.gz", hash = "sha256:32d09eb2f661b38d3edd7d7e1a2943d1633f572596febe64c0cd370c86d52bec", size = 671128, upload-time = "2026-03-17T17:53:49.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/b1/35b6f9c8cf9318e3dbb7146cc82dab4cf61182a8d5406fc9b50864362895/openai-2.29.0-py3-none-any.whl", hash = "sha256:b7c5de513c3286d17c5e29b92c4c98ceaf0d775244ac8159aeb1bddf840eb42a", size = 1141533, upload-time = "2026-03-17T17:53:47.348Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.42.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0621e62d86c402b4a17ab2be7f7c055d9bd2f638b9e2/uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775", size = 85393, upload-time = "2026-03-16T06:19:50.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, + { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, + { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "wildedge-fastapi-openrouter" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "fastapi" }, + { name = "openai" }, + { name = "uvicorn", extra = ["standard"] }, + { name = "wildedge-sdk" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi" }, + { name = "openai" }, + { name = "uvicorn", extras = ["standard"] }, + { name = "wildedge-sdk", editable = "../../" }, +] + +[[package]] +name = "wildedge-sdk" +version = "0.1.1" +source = { editable = "../../" } + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "coverage" }, + { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-asyncio", specifier = ">=0.25" }, + { name = "pytest-mock", specifier = ">=3.15.1" }, + { name = "ruff", specifier = ">=0.15.4" }, + { name = "tox" }, +] diff --git a/tests/test_autoload.py b/tests/test_autoload.py index 302a3c0..d16c04a 100644 --- a/tests/test_autoload.py +++ b/tests/test_autoload.py @@ -4,11 +4,15 @@ import importlib import importlib.util +import subprocess import sys +import textwrap from unittest.mock import patch import wildedge.autoload.sitecustomize as _sc_mod +_MARKER = _sc_mod._INSTALLED_MARKER + def _reload_sitecustomize(): """Reload sitecustomize module so module-level code re-executes.""" @@ -17,10 +21,14 @@ def _reload_sitecustomize(): return _sc_mod -def test_guard_prevents_double_init(monkeypatch): - """WILDEDGE_AUTOLOAD_ACTIVE set: install_runtime not called.""" - monkeypatch.setenv("WILDEDGE_AUTOLOAD_ACTIVE", "1") +def test_gunicorn_prefork_skips_double_init(monkeypatch): + """Gunicorn pre-fork: sys.modules marker blocks re-init in the same interpreter. + + In gunicorn's fork-only model, workers inherit the parent's sys.modules so + sitecustomize never re-executes. This covers the explicit reload edge case. + """ monkeypatch.setenv("WILDEDGE_AUTOLOAD", "1") + monkeypatch.setitem(sys.modules, _MARKER, True) # type: ignore[arg-type] calls = [] with patch("wildedge.runtime.bootstrap.install_runtime", side_effect=calls.append): @@ -30,8 +38,8 @@ def test_guard_prevents_double_init(monkeypatch): def test_skips_when_no_dsn(monkeypatch): - """No DSN env vars: silent skip.""" - monkeypatch.delenv("WILDEDGE_AUTOLOAD_ACTIVE", raising=False) + """No activation env vars present: silent skip regardless of server.""" + monkeypatch.delitem(sys.modules, _MARKER, raising=False) monkeypatch.delenv("WILDEDGE_AUTOLOAD", raising=False) monkeypatch.delenv("WILDEDGE_DSN", raising=False) @@ -42,9 +50,13 @@ def test_skips_when_no_dsn(monkeypatch): assert calls == [] -def test_autoload_flag_triggers_install(monkeypatch): - """WILDEDGE_AUTOLOAD=1 present: install_runtime called with install_signal_handlers=False.""" - monkeypatch.delenv("WILDEDGE_AUTOLOAD_ACTIVE", raising=False) +def test_waitress_autoload_triggers_install(monkeypatch): + """Waitress (single-process, thread-pool): WILDEDGE_AUTOLOAD=1 bootstraps the runtime. + + Waitress has no forking or reloader subprocess, so sitecustomize runs once + in the server process and install_runtime is called. + """ + monkeypatch.delitem(sys.modules, _MARKER, raising=False) monkeypatch.setenv("WILDEDGE_AUTOLOAD", "1") calls = [] @@ -58,9 +70,13 @@ def fake_install(**kwargs): assert calls == [{"install_signal_handlers": False}] -def test_dsn_triggers_install(monkeypatch): - """WILDEDGE_DSN present: install_runtime called.""" - monkeypatch.delenv("WILDEDGE_AUTOLOAD_ACTIVE", raising=False) +def test_granian_dsn_triggers_install(monkeypatch): + """Granian (direct DSN config): WILDEDGE_DSN alone is sufficient to bootstrap. + + Users running granian without `wildedge run` can set WILDEDGE_DSN and + prepend wildedge/autoload/ to PYTHONPATH to get instrumentation. + """ + monkeypatch.delitem(sys.modules, _MARKER, raising=False) monkeypatch.delenv("WILDEDGE_AUTOLOAD", raising=False) monkeypatch.setenv("WILDEDGE_DSN", "https://secret@ingest.wildedge.dev/key") @@ -77,7 +93,7 @@ def fake_install(**kwargs): def test_bootstrap_exception_is_caught(monkeypatch, capsys): """Exception from install_runtime must not propagate; message written to stderr.""" - monkeypatch.delenv("WILDEDGE_AUTOLOAD_ACTIVE", raising=False) + monkeypatch.delitem(sys.modules, _MARKER, raising=False) monkeypatch.setenv("WILDEDGE_AUTOLOAD", "1") with patch( @@ -95,7 +111,7 @@ def test_chains_existing_sitecustomize(monkeypatch, tmp_path): sc_file = tmp_path / "sitecustomize.py" sc_file.write_text(f"open({str(marker)!r}, 'w').close()\n") - monkeypatch.delenv("WILDEDGE_AUTOLOAD_ACTIVE", raising=False) + monkeypatch.delitem(sys.modules, _MARKER, raising=False) monkeypatch.delenv("WILDEDGE_AUTOLOAD", raising=False) monkeypatch.delenv("WILDEDGE_DSN", raising=False) sys.modules.pop("sitecustomize", None) @@ -111,3 +127,52 @@ def patched_find_spec(name): _sc_mod._load_existing_sitecustomize() assert marker.exists(), "chained sitecustomize.py was not executed" + + +def test_uvicorn_reload_worker_bootstraps(tmp_path): + """Uvicorn --reload: the server worker process bootstraps after the reloader already did. + + Uvicorn's reloader runs sitecustomize in the reloader process, then spawns + the actual server worker via exec (fresh interpreter). The worker must be + instrumented. The old os.environ guard (WILDEDGE_AUTOLOAD_ACTIVE) propagated + across exec and blocked the worker's bootstrap. sys.modules is not inherited + across exec, so the worker re-initialises correctly. + """ + import os + import pathlib + + autoload_dir = str( + pathlib.Path( + importlib.util.find_spec("wildedge.autoload.sitecustomize").origin + ).parent + ) + + # Probe script: verify sitecustomize entered the bootstrap path in the child. + # install_runtime will raise (no DSN) but the marker is set before the call, + # so its presence in sys.modules means the worker reached the bootstrap code. + script = textwrap.dedent("""\ + import sys + import wildedge.autoload.sitecustomize as sc + print("installed:", sc._INSTALLED_MARKER in sys.modules) + """) + script_file = tmp_path / "probe.py" + script_file.write_text(script) + + env = os.environ.copy() + env["WILDEDGE_AUTOLOAD"] = "1" + # Simulate the env state the reloader leaves behind. This is what caused + # the regression with the old os.environ guard. + env["WILDEDGE_AUTOLOAD_ACTIVE"] = "1" + env.pop("WILDEDGE_DSN", None) + + pythonpath = env.get("PYTHONPATH", "") + env["PYTHONPATH"] = autoload_dir + (os.pathsep + pythonpath if pythonpath else "") + + result = subprocess.run( + [sys.executable, str(script_file)], + env=env, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr + assert "installed: True" in result.stdout diff --git a/wildedge/autoload/sitecustomize.py b/wildedge/autoload/sitecustomize.py index c77db79..da87129 100644 --- a/wildedge/autoload/sitecustomize.py +++ b/wildedge/autoload/sitecustomize.py @@ -11,18 +11,23 @@ import os import sys -_GUARD = "WILDEDGE_AUTOLOAD_ACTIVE" _AUTOLOAD = "WILDEDGE_AUTOLOAD" # set to "1" by `wildedge run` _DSN = "WILDEDGE_DSN" # user-configured DSN -# Idempotent: skip if already initialized (e.g. in a forked worker that -# exec'd a subprocess, or if sitecustomize.py is imported twice). -if not os.environ.get(_GUARD): +# sys.modules is used as the guard against re-entry within the same interpreter. +# A module-level variable resets when the module object is re-created (e.g. if +# _load_existing_sitecustomize finds this file via a second sys.path entry in an +# editable install). os.environ would persist but propagates to child processes +# spawned via exec (e.g. uvicorn --reload workers) and blocks their bootstrap. +# sys.modules is process-local and survives module re-execution. +_INSTALLED_MARKER = "wildedge.__autoload_installed__" + +if _INSTALLED_MARKER not in sys.modules: # Activate if the CLI launched this process, or if WILDEDGE_DSN is set # and the user has manually prepended wildedge/autoload/ to PYTHONPATH. if os.environ.get(_AUTOLOAD) or os.environ.get(_DSN): # Set the guard before importing wildedge to prevent re-entry. - os.environ[_GUARD] = "1" + sys.modules[_INSTALLED_MARKER] = True # type: ignore[assignment] try: from wildedge.runtime.bootstrap import install_runtime # noqa: PLC0415 diff --git a/wildedge/constants.py b/wildedge/constants.py index 91a8e28..8c3ebd0 100644 --- a/wildedge/constants.py +++ b/wildedge/constants.py @@ -45,7 +45,6 @@ WILDEDGE_AUTOLOAD = ( "WILDEDGE_AUTOLOAD" # set to "1" by `wildedge run` to activate sitecustomize ) -WILDEDGE_AUTOLOAD_ACTIVE = "WILDEDGE_AUTOLOAD_ACTIVE" # guard against double-init # Runtime validation limits BATCH_SIZE_MIN = 1 From 8b8fe663fad1cee43a3d2e406d5d8ee5be32750e Mon Sep 17 00:00:00 2001 From: Piotr Duda Date: Sat, 21 Mar 2026 15:36:26 +0100 Subject: [PATCH 04/11] Lack of DSN should turn client into nop (#26) --- README.md | 4 ++- docs/configuration.md | 2 +- docs/manual-tracking.md | 2 +- examples/feedback_example.py | 2 +- examples/gguf_example.py | 2 +- examples/gguf_gemma_example.py | 2 +- examples/keras_example.py | 2 +- examples/mlx_example.py | 4 ++- examples/onnx_example.py | 2 +- examples/openai_example.py | 4 ++- examples/pytorch_example.py | 2 +- examples/tensorflow_example.py | 2 +- examples/timm_example.py | 2 +- examples/transformers_example.py | 4 ++- tests/test_client.py | 32 ++++++++++++++++++++++ wildedge/client.py | 47 +++++++++++++++++++++++++++++--- 16 files changed, 97 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index f5a7e14..7c10bae 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,8 @@ client.instrument("transformers", hubs=["huggingface"]) # models loaded after this point are tracked automatically ``` +If no DSN is configured, the client becomes a no-op and logs a warning. + ## Supported integrations **On-device** @@ -90,7 +92,7 @@ For unsupported frameworks, see [Manual tracking](https://github.com/wild-edge/w | Parameter | Default | Description | |---|---|---| -| `dsn` | - | `https://@ingest.wildedge.dev/` (or `WILDEDGE_DSN`) | +| `dsn` | - | `https://@ingest.wildedge.dev/` (or `WILDEDGE_DSN`). If unset, the client is a no-op. | | `app_version` | `None` | Your app's version string | | `app_identity` | `` | Namespace for offline persistence. Set per-app in multi-process workloads (or `WILDEDGE_APP_IDENTITY`) | | `enable_offline_persistence` | `true` | Persist unsent events to disk and replay on restart | diff --git a/docs/configuration.md b/docs/configuration.md index 6244fc8..566738b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -6,7 +6,7 @@ Full reference for all `WildEdge` client parameters. | Parameter | Default | Env var | Description | |---|---|---|---| -| `dsn` | - | `WILDEDGE_DSN` | `https://@ingest.wildedge.dev/` | +| `dsn` | - | `WILDEDGE_DSN` | `https://@ingest.wildedge.dev/`. If unset, the client is a no-op. | | `app_version` | `None` | - | Your app's version string | | `app_identity` | `` | `WILDEDGE_APP_IDENTITY` | Namespace for offline persistence. Set per-app in multi-process workloads | | `enable_offline_persistence` | `true` | - | Persist unsent events to disk and replay on restart | diff --git a/docs/manual-tracking.md b/docs/manual-tracking.md index 20d2fe5..e3b1615 100644 --- a/docs/manual-tracking.md +++ b/docs/manual-tracking.md @@ -25,7 +25,7 @@ Every model needs a handle before you can track events against it. Pass the mode ```python import wildedge -client = wildedge.WildEdge() # set WILDEDGE_DSN env var +client = wildedge.WildEdge() # uses WILDEDGE_DSN if set; otherwise no-op handle = client.register_model( my_model, diff --git a/examples/feedback_example.py b/examples/feedback_example.py index 11f9e7c..4644e9e 100644 --- a/examples/feedback_example.py +++ b/examples/feedback_example.py @@ -22,7 +22,7 @@ CONFIDENCE_THRESHOLD = 0.6 client = wildedge.WildEdge( - app_version="1.0.0", # set WILDEDGE_DSN env var + app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op ) client.instrument("timm") diff --git a/examples/gguf_example.py b/examples/gguf_example.py index 074d535..5af3f25 100644 --- a/examples/gguf_example.py +++ b/examples/gguf_example.py @@ -13,7 +13,7 @@ import wildedge client = wildedge.WildEdge( - app_version="1.0.0", # set WILDEDGE_DSN env var + app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op ) client.instrument("gguf", hubs=["huggingface"]) diff --git a/examples/gguf_gemma_example.py b/examples/gguf_gemma_example.py index f3962cf..94afd36 100644 --- a/examples/gguf_gemma_example.py +++ b/examples/gguf_gemma_example.py @@ -28,7 +28,7 @@ FILE = "gemma-2-2b-it-Q4_K_M.gguf" client = wildedge.WildEdge( - app_version="1.0.0", # set WILDEDGE_DSN env var + app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op ) # --- Download --- diff --git a/examples/keras_example.py b/examples/keras_example.py index b782139..0fc2e1c 100644 --- a/examples/keras_example.py +++ b/examples/keras_example.py @@ -28,7 +28,7 @@ exit(1) client = wildedge.WildEdge( - app_version="1.0.0", # set WILDEDGE_DSN env var + app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op ) # Load a pre-trained MobileNetV2 model using client to track construction and lifecycle diff --git a/examples/mlx_example.py b/examples/mlx_example.py index 2c87a3a..1e70b82 100644 --- a/examples/mlx_example.py +++ b/examples/mlx_example.py @@ -52,7 +52,9 @@ def main() -> None: # instrument() patches mlx_lm.load and mlx_lm.generate; must be called # before any model is loaded. - client = wildedge.WildEdge(app_version="1.0.0") # set WILDEDGE_DSN env var + client = wildedge.WildEdge( + app_version="1.0.0" + ) # uses WILDEDGE_DSN if set; otherwise no-op client.instrument("mlx", hubs=["huggingface"]) print(f"\nLoading {args.model} ...") diff --git a/examples/onnx_example.py b/examples/onnx_example.py index a5a9232..e9c4318 100644 --- a/examples/onnx_example.py +++ b/examples/onnx_example.py @@ -14,7 +14,7 @@ import wildedge client = wildedge.WildEdge( - app_version="1.0.0", # set WILDEDGE_DSN env var + app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op ) client.instrument("onnx", hubs=["huggingface"]) diff --git a/examples/openai_example.py b/examples/openai_example.py index e2e2e87..4515d4e 100644 --- a/examples/openai_example.py +++ b/examples/openai_example.py @@ -18,7 +18,9 @@ import wildedge -client = wildedge.WildEdge(app_version="1.0.0") # set WILDEDGE_DSN env var +client = wildedge.WildEdge( + app_version="1.0.0" +) # uses WILDEDGE_DSN if set; otherwise no-op client.instrument("openai") openai_client = OpenAI() # set OPENAI_API_KEY env var or pass api_key= explicitly diff --git a/examples/pytorch_example.py b/examples/pytorch_example.py index c384b15..8868455 100644 --- a/examples/pytorch_example.py +++ b/examples/pytorch_example.py @@ -13,7 +13,7 @@ import wildedge client = wildedge.WildEdge( - app_version="1.0.0", # set WILDEDGE_DSN env var + app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op ) diff --git a/examples/tensorflow_example.py b/examples/tensorflow_example.py index 41641e2..cd6d1b0 100644 --- a/examples/tensorflow_example.py +++ b/examples/tensorflow_example.py @@ -18,7 +18,7 @@ import wildedge client = wildedge.WildEdge( - app_version="1.0.0", # set WILDEDGE_DSN env var + app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op ) client.instrument("tensorflow") diff --git a/examples/timm_example.py b/examples/timm_example.py index 1a302e7..ffea6c3 100644 --- a/examples/timm_example.py +++ b/examples/timm_example.py @@ -20,7 +20,7 @@ import wildedge client = wildedge.WildEdge( - app_version="1.0.0", # set WILDEDGE_DSN env var + app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op ) client.instrument("timm", hubs=["huggingface", "torchhub"]) diff --git a/examples/transformers_example.py b/examples/transformers_example.py index bac9df4..cff9277 100644 --- a/examples/transformers_example.py +++ b/examples/transformers_example.py @@ -92,7 +92,9 @@ def main() -> None: ) args = parser.parse_args() - client = wildedge.WildEdge(app_version="1.0.0") # set WILDEDGE_DSN env var + client = wildedge.WildEdge( + app_version="1.0.0" + ) # uses WILDEDGE_DSN if set; otherwise no-op client.instrument("transformers", hubs=["huggingface"]) print() diff --git a/tests/test_client.py b/tests/test_client.py index 971c64a..3417ba8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -31,6 +31,38 @@ def test_batch_size_too_low(): WildEdge(dsn="https://test@test.com/key", batch_size=0) +def test_no_dsn_is_noop(monkeypatch, caplog): + from wildedge.client import WildEdge + + monkeypatch.delenv(constants.ENV_DSN, raising=False) + with caplog.at_level("WARNING"): + client = WildEdge() + + assert client.noop is True + assert client.closed is True + assert "no DSN configured" in caplog.text + + +def test_no_dsn_instrument_does_not_raise(monkeypatch): + from wildedge.client import WildEdge + + monkeypatch.delenv(constants.ENV_DSN, raising=False) + client = WildEdge() + + # No-DSN client should ignore instrument() entirely (even for unknown integration). + client.instrument("definitely-not-real") + + +def test_no_dsn_publish_does_not_enqueue(monkeypatch): + from wildedge.client import WildEdge + + monkeypatch.delenv(constants.ENV_DSN, raising=False) + client = WildEdge() + + client.publish({"event_type": "inference", "model_id": "m"}) + client.queue.add.assert_not_called() + + def test_batch_size_too_high(): from wildedge.client import WildEdge diff --git a/wildedge/client.py b/wildedge/client.py index da08f02..da5e052 100644 --- a/wildedge/client.py +++ b/wildedge/client.py @@ -68,6 +68,14 @@ ) +class _NoopConsumer: + def flush(self, timeout: float = 0) -> None: + return None + + def close(self, timeout: float | None = None) -> None: + return None + + def parse_dsn(dsn: str) -> tuple[str, str, str]: """Parse DSN into (secret, host, project_key).""" parsed = urlparse(dsn) @@ -153,10 +161,12 @@ def __init__( ): env = read_client_env(dsn=dsn, debug=debug, app_identity=app_identity) dsn = env.dsn + debug = env.debug + self.noop = False if not dsn: - raise ValueError(ERROR_DSN_REQUIRED) + self._init_noop(debug=debug, device=device) + return api_key, host, project_key = parse_dsn(dsn) - debug = env.debug app_identity = resolve_app_identity( explicit=env.app_identity, project_key=project_key, @@ -258,8 +268,32 @@ def __init__( if debug: logger.debug("wildedge: client initialized (session=%s)", self.session_id) + def _init_noop(self, *, debug: bool, device: DeviceInfo | None) -> None: + self.noop = True + self.debug = debug + self.closed = True + logger.warning( + "wildedge: no DSN configured; client is disabled (events will be dropped)" + ) + self.api_key = None + self.device = device + self.session_id = str(uuid.uuid4()) + self.created_at = datetime.now(timezone.utc) + self.queue = EventQueue( + max_size=1, + policy=QueuePolicy.OPPORTUNISTIC, + persist_to_disk=False, + ) + self.registry = ModelRegistry(persist_path=None) + self.transmitter = None + self.dead_letter_store = None + self.consumer = _NoopConsumer() + self.auto_loaded = set() + self._auto_load_finalizers = {} + self.hub_trackers = {} + def publish(self, event_dict: dict) -> None: - if self.closed: + if self.closed or self.noop: return if self.debug: @@ -472,6 +506,10 @@ def instrument( Each integration or hub tracker is installed at most once per process regardless of how many times ``instrument()`` is called. """ + if self.noop: + if self.debug: + logger.debug("wildedge: instrument skipped (no DSN configured)") + return if integration is None: if not hubs: raise ValueError( @@ -658,7 +696,8 @@ def flush(self, timeout: float = 5.0) -> None: def close(self, timeout: float | None = None) -> None: """Best-effort shutdown; pass timeout to attempt bounded flush first.""" self.closed = True - stop_sampler() + if not self.noop: + stop_sampler() if timeout is None: self.consumer.close() else: From 7bbb380b1d8523869c7418ead601d0909553d2e3 Mon Sep 17 00:00:00 2001 From: Piotr Duda Date: Sat, 21 Mar 2026 16:13:03 +0100 Subject: [PATCH 05/11] More pythonic sdk init (#27) --- README.md | 9 ++++- docs/manual-tracking.md | 5 ++- examples/feedback_example.py | 4 +- examples/gguf_example.py | 5 ++- examples/mlx_example.py | 11 ++--- examples/onnx_example.py | 5 ++- examples/openai_example.py | 10 ++--- examples/tensorflow_example.py | 6 +-- examples/timm_example.py | 5 ++- examples/transformers_example.py | 9 +++-- tests/test_init.py | 69 ++++++++++++++++++++++++++++++++ wildedge/__init__.py | 2 + wildedge/convenience.py | 41 +++++++++++++++++++ 13 files changed, 153 insertions(+), 28 deletions(-) create mode 100644 tests/test_init.py create mode 100644 wildedge/convenience.py diff --git a/README.md b/README.md index 7c10bae..cbe9c81 100644 --- a/README.md +++ b/README.md @@ -47,14 +47,19 @@ Useful flags: ```python import wildedge -client = wildedge.WildEdge(dsn="...") # or WILDEDGE_DSN env var -client.instrument("transformers", hubs=["huggingface"]) +client = wildedge.init( + dsn="...", # or WILDEDGE_DSN env var + integrations=["transformers"], + hubs=["huggingface"], +) # models loaded after this point are tracked automatically ``` If no DSN is configured, the client becomes a no-op and logs a warning. +`init(...)` is a convenience wrapper for `WildEdge(...)` + `instrument(...)`. + ## Supported integrations **On-device** diff --git a/docs/manual-tracking.md b/docs/manual-tracking.md index e3b1615..f90f58a 100644 --- a/docs/manual-tracking.md +++ b/docs/manual-tracking.md @@ -25,7 +25,10 @@ Every model needs a handle before you can track events against it. Pass the mode ```python import wildedge -client = wildedge.WildEdge() # uses WILDEDGE_DSN if set; otherwise no-op +client = wildedge.init() # uses WILDEDGE_DSN if set; otherwise no-op + +# Optional: enable auto-instrumentation alongside manual tracking. +# client = wildedge.init(integrations=["transformers"], hubs=["huggingface"]) handle = client.register_model( my_model, diff --git a/examples/feedback_example.py b/examples/feedback_example.py index 4644e9e..ff06292 100644 --- a/examples/feedback_example.py +++ b/examples/feedback_example.py @@ -21,10 +21,10 @@ CONFIDENCE_THRESHOLD = 0.6 -client = wildedge.WildEdge( +client = wildedge.init( app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op + integrations="timm", ) -client.instrument("timm") model = timm.create_model("resnet18", pretrained=True) model.eval() diff --git a/examples/gguf_example.py b/examples/gguf_example.py index 5af3f25..cf58f88 100644 --- a/examples/gguf_example.py +++ b/examples/gguf_example.py @@ -12,10 +12,11 @@ import wildedge -client = wildedge.WildEdge( +client = wildedge.init( app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op + integrations="gguf", + hubs=["huggingface"], ) -client.instrument("gguf", hubs=["huggingface"]) model_path = hf_hub_download( "bartowski/Llama-3.2-1B-Instruct-GGUF", diff --git a/examples/mlx_example.py b/examples/mlx_example.py index 1e70b82..61906e0 100644 --- a/examples/mlx_example.py +++ b/examples/mlx_example.py @@ -50,12 +50,13 @@ def main() -> None: ) args = parser.parse_args() - # instrument() patches mlx_lm.load and mlx_lm.generate; must be called + # init() constructs the client and instruments mlx; must be called # before any model is loaded. - client = wildedge.WildEdge( - app_version="1.0.0" - ) # uses WILDEDGE_DSN if set; otherwise no-op - client.instrument("mlx", hubs=["huggingface"]) + client = wildedge.init( + app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op + integrations="mlx", + hubs=["huggingface"], + ) print(f"\nLoading {args.model} ...") model, tokenizer = mlx_lm.load(args.model) # load + download tracked automatically diff --git a/examples/onnx_example.py b/examples/onnx_example.py index e9c4318..e7a6f78 100644 --- a/examples/onnx_example.py +++ b/examples/onnx_example.py @@ -13,10 +13,11 @@ import wildedge -client = wildedge.WildEdge( +client = wildedge.init( app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op + integrations="onnx", + hubs=["huggingface"], ) -client.instrument("onnx", hubs=["huggingface"]) model_path = hf_hub_download("Xenova/resnet-50", "onnx/model.onnx") session = ort.InferenceSession(model_path) diff --git a/examples/openai_example.py b/examples/openai_example.py index 4515d4e..d9410e4 100644 --- a/examples/openai_example.py +++ b/examples/openai_example.py @@ -11,17 +11,17 @@ inference tracking happens automatically for every chat.completions.create call. Run with: uv run openai_example.py -Requires: WILDEDGE_DSN and OPENAI_API_KEY environment variables. +Requires: OPENAI_API_KEY environment variable. Set WILDEDGE_DSN to send events. """ from openai import OpenAI import wildedge -client = wildedge.WildEdge( - app_version="1.0.0" -) # uses WILDEDGE_DSN if set; otherwise no-op -client.instrument("openai") +client = wildedge.init( + app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op + integrations="openai", +) openai_client = OpenAI() # set OPENAI_API_KEY env var or pass api_key= explicitly diff --git a/examples/tensorflow_example.py b/examples/tensorflow_example.py index cd6d1b0..3cf1962 100644 --- a/examples/tensorflow_example.py +++ b/examples/tensorflow_example.py @@ -17,10 +17,10 @@ import wildedge -client = wildedge.WildEdge( +client = wildedge.init( app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op + integrations="tensorflow", ) -client.instrument("tensorflow") def build_and_save_model(save_path: Path) -> None: @@ -40,7 +40,7 @@ def build_and_save_model(save_path: Path) -> None: model_path = Path(temp_dir) / "demo_model.keras" build_and_save_model(model_path) - # load_model is auto-instrumented by client.instrument("tensorflow") + # load_model is auto-instrumented by init(..., integrations="tensorflow") loaded = tf.keras.models.load_model(model_path) batch = np.random.randn(4, 16).astype(np.float32) diff --git a/examples/timm_example.py b/examples/timm_example.py index ffea6c3..5b4ffa1 100644 --- a/examples/timm_example.py +++ b/examples/timm_example.py @@ -19,10 +19,11 @@ import wildedge -client = wildedge.WildEdge( +client = wildedge.init( app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op + integrations="timm", + hubs=["huggingface", "torchhub"], ) -client.instrument("timm", hubs=["huggingface", "torchhub"]) model = timm.create_model("resnet18", pretrained=True) model.eval() diff --git a/examples/transformers_example.py b/examples/transformers_example.py index cff9277..92e8a1d 100644 --- a/examples/transformers_example.py +++ b/examples/transformers_example.py @@ -92,10 +92,11 @@ def main() -> None: ) args = parser.parse_args() - client = wildedge.WildEdge( - app_version="1.0.0" - ) # uses WILDEDGE_DSN if set; otherwise no-op - client.instrument("transformers", hubs=["huggingface"]) + client = wildedge.init( + app_version="1.0.0", # uses WILDEDGE_DSN if set; otherwise no-op + integrations="transformers", + hubs=["huggingface"], + ) print() {"classify": run_classify, "generate": run_generate, "embed": run_embed}[ diff --git a/tests/test_init.py b/tests/test_init.py new file mode 100644 index 0000000..3450446 --- /dev/null +++ b/tests/test_init.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +import wildedge +import wildedge.convenience as convenience + + +def test_init_calls_instrument_for_integrations(monkeypatch): + + calls: list[tuple[str | None, list[str] | None]] = [] + + class DummyClient: + def __init__(self, **kwargs): + self.kwargs = kwargs + + def instrument(self, integration, *, hubs=None): + calls.append((integration, hubs)) + + monkeypatch.setattr(convenience, "WildEdge", DummyClient) + + client = wildedge.init( + dsn="https://secret@ingest.wildedge.dev/key", + integrations=["onnx", "timm"], + hubs=["huggingface"], + ) + + assert isinstance(client, DummyClient) + assert calls == [("onnx", ["huggingface"]), ("timm", ["huggingface"])] + + +def test_init_hubs_only(monkeypatch): + + calls: list[tuple[str | None, list[str] | None]] = [] + + class DummyClient: + def __init__(self, **kwargs): + self.kwargs = kwargs + + def instrument(self, integration, *, hubs=None): + calls.append((integration, hubs)) + + monkeypatch.setattr(convenience, "WildEdge", DummyClient) + + client = wildedge.init( + dsn="https://secret@ingest.wildedge.dev/key", + hubs=["huggingface"], + ) + + assert isinstance(client, DummyClient) + assert calls == [(None, ["huggingface"])] + + +def test_init_logs_debug_when_no_integrations_or_hubs(monkeypatch): + logs: list[str] = [] + + class DummyClient: + def __init__(self, **kwargs): + self.kwargs = kwargs + self.debug = True + + def instrument(self, integration, *, hubs=None): + raise AssertionError("instrument should not be called") + + monkeypatch.setattr(convenience, "WildEdge", DummyClient) + monkeypatch.setattr(convenience.logger, "debug", lambda msg: logs.append(msg)) + + client = wildedge.init(dsn="https://secret@ingest.wildedge.dev/key") + + assert isinstance(client, DummyClient) + assert logs == ["wildedge: init called without integrations or hubs"] diff --git a/wildedge/__init__.py b/wildedge/__init__.py index 73f74ef..f1f449b 100644 --- a/wildedge/__init__.py +++ b/wildedge/__init__.py @@ -1,6 +1,7 @@ """WildEdge Python SDK.""" from wildedge.client import WildEdge +from wildedge.convenience import init from wildedge.decorators import track from wildedge.events import ( AdapterDownload, @@ -24,6 +25,7 @@ __all__ = [ "WildEdge", + "init", "capture_hardware", "HardwareContext", "ThermalContext", diff --git a/wildedge/convenience.py b/wildedge/convenience.py new file mode 100644 index 0000000..a9e61aa --- /dev/null +++ b/wildedge/convenience.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from collections.abc import Iterable +from typing import Any + +from wildedge.client import WildEdge +from wildedge.logging import logger + + +def _normalize_list(value: str | Iterable[str] | None) -> list[str]: + if value is None: + return [] + if isinstance(value, str): + return [value] + return [item for item in value if item] + + +def init( + *, + integrations: str | Iterable[str] | None = None, + hubs: str | Iterable[str] | None = None, + **kwargs: Any, +) -> WildEdge: + """ + Convenience initializer: construct a WildEdge client and instrument integrations. + + Additional keyword arguments are forwarded to WildEdge(...). + """ + client = WildEdge(**kwargs) + normalized_integrations = _normalize_list(integrations) + normalized_hubs = _normalize_list(hubs) + + if normalized_integrations: + for integration in normalized_integrations: + client.instrument(integration, hubs=normalized_hubs or None) + elif normalized_hubs: + client.instrument(None, hubs=normalized_hubs) + elif getattr(client, "debug", False): + logger.debug("wildedge: init called without integrations or hubs") + + return client From 72b6e555129b7fb3bd2a45bd636ab04a872b64ee Mon Sep 17 00:00:00 2001 From: Piotr Duda Date: Mon, 23 Mar 2026 09:46:21 +0100 Subject: [PATCH 06/11] Release 0.1.2 (#29) * Release 0.1.2 --- .github/workflows/ci.yml | 2 +- .github/workflows/compat.yml | 10 ++--- .github/workflows/release-pr.yml | 30 ++++++++++++++ .github/workflows/release.yml | 12 +++++- CONTRIBUTING.md | 11 ++++- examples/cli/uv.lock | 2 +- examples/django_gemma/uv.lock | 2 +- examples/fastapi_openai/uv.lock | 2 +- pyproject.toml | 2 +- scripts/build_changelog_comment.py | 66 ++++++++++++++++++++++++++++++ uv.lock | 2 +- 11 files changed, 128 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/release-pr.yml create mode 100644 scripts/build_changelog_comment.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9d68b8..4f942d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [ main, devel ] + branches: [ main, devel, 'release/**' ] paths: - 'wildedge/**' - 'tests/**' diff --git a/.github/workflows/compat.yml b/.github/workflows/compat.yml index a411710..3e267e3 100644 --- a/.github/workflows/compat.yml +++ b/.github/workflows/compat.yml @@ -2,7 +2,7 @@ name: Compatibility on: push: - branches: [ main, devel ] + branches: [ main, 'release/**' ] paths: - 'wildedge/**' - 'tests/compat/**' @@ -10,8 +10,8 @@ on: - 'pyproject.toml' - '.github/workflows/compat.yml' pull_request: - branches: [ main, devel ] - types: [opened, synchronize, reopened, labeled] + branches: [ main ] + types: [opened, synchronize, reopened] paths: - 'wildedge/**' - 'tests/compat/**' @@ -24,7 +24,7 @@ on: jobs: compat: - if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'compat') + if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'release/') runs-on: ubuntu-latest strategy: fail-fast: false @@ -84,7 +84,7 @@ jobs: run: python -m pytest tests/compat/test_mlx_compat.py -q compat-canary-314: - if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'compat') + if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'release/') runs-on: ubuntu-latest continue-on-error: true strategy: diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml new file mode 100644 index 0000000..efb015d --- /dev/null +++ b/.github/workflows/release-pr.yml @@ -0,0 +1,30 @@ +name: Release PR + +on: + pull_request: + branches: [ main ] + types: [opened, synchronize, reopened] + +jobs: + changelog-preview: + if: startsWith(github.head_ref, 'release/') + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + + - name: Generate changelog preview + env: + GH_TOKEN: ${{ github.token }} + HEAD_REF: ${{ github.head_ref }} + REPO: ${{ github.repository }} + OUTPUT: /tmp/changelog-preview.md + run: python3 scripts/build_changelog_comment.py + + - name: Post changelog preview comment + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: changelog-preview + path: /tmp/changelog-preview.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9fe7013..732a462 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,12 +38,22 @@ jobs: - name: Check package metadata run: twine check dist/* + - name: Generate release notes + if: github.ref_type == 'tag' + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + HEAD_REF: ${{ github.ref_name }} + TAG_NAME: ${{ github.ref_name }} + OUTPUT: /tmp/release-notes.md + run: python3 scripts/build_changelog_comment.py + - name: Create GitHub release if: github.ref_type == 'tag' uses: softprops/action-gh-release@v2 with: files: dist/* - generate_release_notes: true + body_path: /tmp/release-notes.md - name: Publish package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7679f3e..5de252b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,16 @@ 1. Fork the repository and create a feature branch off `devel`. 2. Make your changes and ensure tests pass. 3. Update documentation if needed. -4. Submit a pull request with a clear description of the changes. +4. Submit a pull request targeting `devel` with a clear description of the changes. + +## Release process + +Releases follow a `release/` branching flow: + +1. Cut a `release/` branch from `devel`. +2. Bump the version in `pyproject.toml`. +3. Open a pull request targeting `main`. CI will automatically run the full compatibility matrix and post a changelog preview comment. +4. Once merged, tag the commit as `v`. The release workflow publishes to PyPI and creates a GitHub release. ## Reporting issues diff --git a/examples/cli/uv.lock b/examples/cli/uv.lock index 6103e3b..4d86671 100644 --- a/examples/cli/uv.lock +++ b/examples/cli/uv.lock @@ -1095,7 +1095,7 @@ requires-dist = [ [[package]] name = "wildedge-sdk" -version = "0.1.1" +version = "0.1.2" source = { editable = "../../" } [package.metadata] diff --git a/examples/django_gemma/uv.lock b/examples/django_gemma/uv.lock index ca6b004..75f37c8 100644 --- a/examples/django_gemma/uv.lock +++ b/examples/django_gemma/uv.lock @@ -728,7 +728,7 @@ requires-dist = [ [[package]] name = "wildedge-sdk" -version = "0.1.1" +version = "0.1.2" source = { editable = "../../" } [package.metadata] diff --git a/examples/fastapi_openai/uv.lock b/examples/fastapi_openai/uv.lock index fff8ef0..b620a91 100644 --- a/examples/fastapi_openai/uv.lock +++ b/examples/fastapi_openai/uv.lock @@ -828,7 +828,7 @@ requires-dist = [ [[package]] name = "wildedge-sdk" -version = "0.1.1" +version = "0.1.2" source = { editable = "../../" } [package.metadata] diff --git a/pyproject.toml b/pyproject.toml index 7f83e84..3a7e63e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "wildedge-sdk" -version = "0.1.1" +version = "0.1.2" description = "On-device ML inference monitoring for Python" readme = "README.md" requires-python = ">=3.10" diff --git a/scripts/build_changelog_comment.py b/scripts/build_changelog_comment.py new file mode 100644 index 0000000..9e54df7 --- /dev/null +++ b/scripts/build_changelog_comment.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Generate a changelog preview comment for release PRs using the GitHub API.""" + +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +import tomllib + + +def get_version() -> str: + data = tomllib.loads(Path("pyproject.toml").read_text()) + return data["project"]["version"] + + +def get_previous_tag(repo: str) -> str | None: + result = subprocess.run( + ["gh", "api", f"repos/{repo}/releases/latest", "--jq", ".tag_name"], + capture_output=True, + text=True, + ) + tag = result.stdout.strip() + return tag if result.returncode == 0 and tag else None + + +def generate_notes(repo: str, tag_name: str, target: str, prev_tag: str | None) -> str: + args = [ + "gh", + "api", + f"repos/{repo}/releases/generate-notes", + "-f", + f"tag_name={tag_name}", + "-f", + f"target_commitish={target}", + ] + if prev_tag: + args += ["-f", f"previous_tag_name={prev_tag}"] + args += ["--jq", ".body"] + + result = subprocess.run(args, capture_output=True, text=True, check=True) + return result.stdout + + +def build_comment(tag_name: str, notes: str) -> str: + return ( + f"## Changelog preview for `{tag_name}`\n\n" + "> Preview of the GitHub release notes that will be generated when this is tagged.\n\n" + + notes + ) + + +def main() -> None: + repo = os.environ["REPO"] + head_ref = os.environ["HEAD_REF"] + output = os.environ.get("OUTPUT", "/tmp/changelog-preview.md") + tag_name = os.environ.get("TAG_NAME") or f"v{get_version()}" + + prev_tag = get_previous_tag(repo) + notes = generate_notes(repo, tag_name, head_ref, prev_tag) + Path(output).write_text(build_comment(tag_name, notes)) + + +if __name__ == "__main__": + main() diff --git a/uv.lock b/uv.lock index dca8b1c..5043436 100644 --- a/uv.lock +++ b/uv.lock @@ -419,7 +419,7 @@ wheels = [ [[package]] name = "wildedge-sdk" -version = "0.1.1" +version = "0.1.2" source = { editable = "." } [package.dev-dependencies] From 4c80592c4eb68560c107e4643fd9d26d03842e79 Mon Sep 17 00:00:00 2001 From: Piotr Duda Date: Mon, 23 Mar 2026 12:19:34 +0100 Subject: [PATCH 07/11] Used in projects table in README --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index cbe9c81..1c24d0b 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,15 @@ For unsupported frameworks, see [Manual tracking](https://github.com/wild-edge/w For advanced options (batching, queue tuning, dead-letter storage), see [Configuration](https://github.com/wild-edge/wildedge-python/blob/main/docs/configuration.md). +## Projects using this SDK + +| Name | Link | +|---|---| +| agntr | [github.com/pmaciolek/agntr](https://github.com/pmaciolek/agntr) | +| *(your project here)* | - | + +Using WildEdge in your project? Open a PR to add it to the list. + ## Privacy Report security & priact issues to: wildedge@googlegroups.com. From 79de451005ea2967050bfd5d4fbdf22be84493fa Mon Sep 17 00:00:00 2001 From: Piotr Duda Date: Wed, 25 Mar 2026 16:24:29 +0100 Subject: [PATCH 08/11] TTFT support for remote llm + GGUF integrations (#32) --- examples/gguf_example.py | 9 +- examples/openai_example.py | 10 +- tests/test_integrations_openai.py | 151 ++++++++++++++++++++++++++++-- wildedge/integrations/common.py | 123 +++++++++++++++++++++++- wildedge/integrations/gguf.py | 105 ++++++++++++++++----- wildedge/integrations/openai.py | 88 ++++++++++++++++- 6 files changed, 439 insertions(+), 47 deletions(-) diff --git a/examples/gguf_example.py b/examples/gguf_example.py index cf58f88..bd50f98 100644 --- a/examples/gguf_example.py +++ b/examples/gguf_example.py @@ -31,6 +31,9 @@ ] for prompt in prompts: - result = llm(prompt, max_tokens=128, temperature=0.7) - text = result["choices"][0]["text"].strip() - print(f"Q: {prompt}\nA: {text}\n") + stream = llm(prompt, max_tokens=128, temperature=0.7, stream=True) + print(f"Q: {prompt}\nA: ", end="", flush=True) + for chunk in stream: + token = chunk["choices"][0].get("text", "") + print(token, end="", flush=True) + print("\n") diff --git a/examples/openai_example.py b/examples/openai_example.py index d9410e4..ad2d3a9 100644 --- a/examples/openai_example.py +++ b/examples/openai_example.py @@ -32,13 +32,19 @@ ] for prompt in prompts: - response = openai_client.chat.completions.create( + stream = openai_client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": prompt}], temperature=0.7, max_tokens=256, + stream=True, + stream_options={"include_usage": True}, ) - print(f"Q: {prompt}\nA: {response.choices[0].message.content}\n") + print(f"Q: {prompt}\nA: ", end="", flush=True) + for chunk in stream: + if chunk.choices and chunk.choices[0].delta.content: + print(chunk.choices[0].delta.content, end="", flush=True) + print("\n") client.flush() print("Done. Events flushed to WildEdge.") diff --git a/tests/test_integrations_openai.py b/tests/test_integrations_openai.py index 9fea3ce..dbba8d0 100644 --- a/tests/test_integrations_openai.py +++ b/tests/test_integrations_openai.py @@ -9,6 +9,7 @@ import pytest import wildedge.integrations.openai as openai_mod +from wildedge.integrations.common import AsyncStreamWrapper, SyncStreamWrapper from wildedge.integrations.openai import ( OpenAIExtractor, build_api_meta, @@ -83,6 +84,56 @@ async def create(self, *args, **kwargs): return self._response +def make_stream_chunk(content=None, finish_reason=None, usage=None): + chunk = SimpleNamespace( + choices=[ + SimpleNamespace( + delta=SimpleNamespace(content=content), + finish_reason=finish_reason, + ) + ], + usage=usage, + model="gpt-4o", + system_fingerprint=None, + service_tier=None, + ) + return chunk + + +class FakeStreamingCompletions: + def __init__(self, chunks): + self._chunks = chunks + + def create(self, *args, **kwargs): + if kwargs.get("stream"): + return iter(self._chunks) + return FakeResponse() + + +class FakeAsyncStreamingCompletions: + def __init__(self, chunks): + self._chunks = chunks + + async def create(self, *args, **kwargs): + if kwargs.get("stream"): + return FakeAsyncIterator(self._chunks) + return FakeResponse() + + +class FakeAsyncIterator: + def __init__(self, items): + self._iter = iter(items) + + def __aiter__(self): + return self + + async def __anext__(self): + try: + return next(self._iter) + except StopIteration: + raise StopAsyncIteration + + # Named "OpenAI" / "AsyncOpenAI" so can_handle sees the right type name. class OpenAI: def __init__(self, base_url="https://api.openai.com/v1", api_key=None): @@ -371,11 +422,65 @@ def create(self, *args, **kwargs): client.handles["gpt-4o"].track_error.assert_called_once() client.handles["gpt-4o"].track_inference.assert_not_called() - def test_streaming_skips_tracking(self): - completions, client = self.setup() - completions.create(model="gpt-4o", messages=[], stream=True) - if "gpt-4o" in client.handles: - client.handles["gpt-4o"].track_inference.assert_not_called() + def test_streaming_returns_sync_stream_wrapper(self): + chunks = [make_stream_chunk("hi", None), make_stream_chunk(None, "stop")] + completions = FakeStreamingCompletions(chunks) + client = make_fake_client() + wrap_sync_completions(completions, "openai", lambda: client) + result = completions.create(model="gpt-4o", messages=[], stream=True) + assert isinstance(result, SyncStreamWrapper) + + def test_streaming_records_inference_on_exhaustion(self): + chunks = [ + make_stream_chunk("Hello", None), + make_stream_chunk(" world", "stop"), + ] + completions = FakeStreamingCompletions(chunks) + client = make_fake_client() + wrap_sync_completions(completions, "openai", lambda: client) + stream = completions.create( + model="gpt-4o", messages=[{"role": "user", "content": "hi"}], stream=True + ) + list(stream) + handle = client.handles["gpt-4o"] + handle.track_inference.assert_called_once() + kwargs = handle.track_inference.call_args.kwargs + assert kwargs["output_meta"].time_to_first_token_ms is not None + assert kwargs["output_meta"].stop_reason == "stop" + assert kwargs["input_modality"] == "text" + assert kwargs["success"] is True + + def test_streaming_captures_usage_from_chunks(self): + usage_chunk = SimpleNamespace(prompt_tokens=8, completion_tokens=15) + chunks = [ + make_stream_chunk("hi", None), + make_stream_chunk(None, "stop", usage=usage_chunk), + ] + completions = FakeStreamingCompletions(chunks) + client = make_fake_client() + wrap_sync_completions(completions, "openai", lambda: client) + list(completions.create(model="gpt-4o", messages=[], stream=True)) + out = client.handles["gpt-4o"].track_inference.call_args.kwargs["output_meta"] + assert out.tokens_in == 8 + assert out.tokens_out == 15 + + def test_streaming_error_during_iteration_tracks_error(self): + def bad_iter(): + yield make_stream_chunk("hi", None) + raise RuntimeError("stream error") + + class ErrorStreamCompletions: + def create(self, *args, **kwargs): + return bad_iter() + + client = make_fake_client() + completions = ErrorStreamCompletions() + wrap_sync_completions(completions, "openai", lambda: client) + stream = completions.create(model="gpt-4o", messages=[], stream=True) + with pytest.raises(RuntimeError, match="stream error"): + list(stream) + client.handles["gpt-4o"].track_error.assert_called_once() + client.handles["gpt-4o"].track_inference.assert_not_called() def test_closed_client_passes_through(self): completions, client = self.setup(closed=True) @@ -438,11 +543,37 @@ async def create(self, *args, **kwargs): client.handles["gpt-4o"].track_error.assert_called_once() - async def test_streaming_skips_tracking(self): - completions, client = self.setup() - await completions.create(model="qwen/qwen3-235b", messages=[], stream=True) - if "qwen/qwen3-235b" in client.handles: - client.handles["qwen/qwen3-235b"].track_inference.assert_not_called() + async def test_streaming_returns_async_stream_wrapper(self): + chunks = [make_stream_chunk("hi", None), make_stream_chunk(None, "stop")] + completions = FakeAsyncStreamingCompletions(chunks) + client = make_fake_client() + wrap_async_completions(completions, "openrouter", lambda: client) + result = await completions.create( + model="qwen/qwen3-235b", messages=[], stream=True + ) + assert isinstance(result, AsyncStreamWrapper) + + async def test_streaming_records_inference_on_exhaustion(self): + chunks = [ + make_stream_chunk("Hello", None), + make_stream_chunk(" world", "stop"), + ] + completions = FakeAsyncStreamingCompletions(chunks) + client = make_fake_client() + wrap_async_completions(completions, "openrouter", lambda: client) + stream = await completions.create( + model="qwen/qwen3-235b", + messages=[{"role": "user", "content": "hi"}], + stream=True, + ) + async for _ in stream: + pass + handle = client.handles["qwen/qwen3-235b"] + handle.track_inference.assert_called_once() + kwargs = handle.track_inference.call_args.kwargs + assert kwargs["output_meta"].time_to_first_token_ms is not None + assert kwargs["output_meta"].stop_reason == "stop" + assert kwargs["success"] is True # --------------------------------------------------------------------------- diff --git a/wildedge/integrations/common.py b/wildedge/integrations/common.py index 22a431a..1506b01 100644 --- a/wildedge/integrations/common.py +++ b/wildedge/integrations/common.py @@ -2,9 +2,15 @@ from __future__ import annotations -from typing import Any +from collections.abc import Callable +from typing import TYPE_CHECKING, Any +from wildedge import constants from wildedge.logging import logger +from wildedge.timing import elapsed_ms + +if TYPE_CHECKING: + from wildedge.model import ModelHandle def debug_failure(framework: str, context: str, exc: BaseException) -> None: @@ -110,3 +116,118 @@ def num_classes_from_output_shape(shape: tuple) -> int: if len(shape) >= 2 and isinstance(shape[-1], int) and shape[-1] > 1: return int(shape[-1]) return 0 + + +# --------------------------------------------------------------------------- +# Generic streaming wrappers +# --------------------------------------------------------------------------- +# Each integration provides: +# on_chunk(chunk) -> None : update mutable state from a single chunk +# on_done(duration_ms, ttft_ms) : record inference once the stream is exhausted +# +# The wrappers handle TTFT capture, error tracking, context-manager delegation, +# and attribute proxying so callers get a drop-in replacement for the raw stream. + + +class SyncStreamWrapper: + """Wraps a sync iterable stream to capture TTFT and record inference on exhaustion.""" + + def __init__( + self, + original: object, + handle: ModelHandle, + t0: float, + on_chunk: Callable[[object], None] | None, + on_done: Callable[[int, int | None], None], + ) -> None: + self._original = original + self._handle = handle + self._t0 = t0 + self._on_chunk = on_chunk + self._on_done = on_done + + def __iter__(self): + return self._track() + + def _track(self): + ttft_ms: int | None = None + try: + for chunk in self._original: # type: ignore[union-attr] + if ttft_ms is None: + ttft_ms = elapsed_ms(self._t0) + if self._on_chunk is not None: + self._on_chunk(chunk) + yield chunk + except Exception as exc: + self._handle.track_error( + error_code="UNKNOWN", + error_message=str(exc)[: constants.ERROR_MSG_MAX_LEN], + ) + raise + else: + self._on_done(elapsed_ms(self._t0), ttft_ms) + + def __enter__(self) -> SyncStreamWrapper: + if hasattr(self._original, "__enter__"): + self._original.__enter__() # type: ignore[union-attr] + return self + + def __exit__(self, *args: object) -> object: + if hasattr(self._original, "__exit__"): + return self._original.__exit__(*args) # type: ignore[union-attr] + return None + + def __getattr__(self, name: str) -> object: + return getattr(self._original, name) + + +class AsyncStreamWrapper: + """Wraps an async iterable stream to capture TTFT and record inference on exhaustion.""" + + def __init__( + self, + original: object, + handle: ModelHandle, + t0: float, + on_chunk: Callable[[object], None] | None, + on_done: Callable[[int, int | None], None], + ) -> None: + self._original = original + self._handle = handle + self._t0 = t0 + self._on_chunk = on_chunk + self._on_done = on_done + + def __aiter__(self): + return self._track() + + async def _track(self): + ttft_ms: int | None = None + try: + async for chunk in self._original: # type: ignore[union-attr] + if ttft_ms is None: + ttft_ms = elapsed_ms(self._t0) + if self._on_chunk is not None: + self._on_chunk(chunk) + yield chunk + except Exception as exc: + self._handle.track_error( + error_code="UNKNOWN", + error_message=str(exc)[: constants.ERROR_MSG_MAX_LEN], + ) + raise + else: + self._on_done(elapsed_ms(self._t0), ttft_ms) + + async def __aenter__(self) -> AsyncStreamWrapper: + if hasattr(self._original, "__aenter__"): + await self._original.__aenter__() # type: ignore[union-attr] + return self + + async def __aexit__(self, *args: object) -> object: + if hasattr(self._original, "__aexit__"): + return await self._original.__aexit__(*args) # type: ignore[union-attr] + return None + + def __getattr__(self, name: str) -> object: + return getattr(self._original, name) diff --git a/wildedge/integrations/gguf.py b/wildedge/integrations/gguf.py index 9937cdd..dc44360 100644 --- a/wildedge/integrations/gguf.py +++ b/wildedge/integrations/gguf.py @@ -12,7 +12,7 @@ from wildedge import constants from wildedge.events.inference import GenerationOutputMeta, TextInputMeta from wildedge.integrations.base import BaseExtractor, patch_instance_call_once -from wildedge.integrations.common import debug_failure +from wildedge.integrations.common import SyncStreamWrapper, debug_failure from wildedge.logging import logger from wildedge.model import ModelInfo from wildedge.platforms import CURRENT_PLATFORM @@ -69,6 +69,76 @@ def parse_quantization(filename: str) -> str | None: return None +def make_gguf_input_meta(prompt: object, tokens_in: int | None) -> TextInputMeta | None: + if not isinstance(prompt, str) or not prompt: + return None + return TextInputMeta( + char_count=len(prompt), + word_count=len(prompt.split()), + token_count=tokens_in, + ) + + +def make_gguf_output_meta( + tokens_in: int | None, + tokens_out: int | None, + stop_reason: str | None, + ttft_ms: int | None, + duration_ms: int, +) -> GenerationOutputMeta | None: + if tokens_out is None and ttft_ms is None: + return None + tps = ( + round(tokens_out / duration_ms * 1000, 1) + if duration_ms > 0 and tokens_out + else None + ) + return GenerationOutputMeta( + task="generation", + tokens_in=tokens_in, + tokens_out=tokens_out, + time_to_first_token_ms=ttft_ms, + tokens_per_second=tps, + stop_reason=stop_reason, + ) + + +def make_gguf_stream_callbacks(handle: object, prompt: object) -> tuple: + """Return (on_chunk, on_done) callbacks for a llama-cpp-python streaming response. + + Chunks are dicts; usage appears in the final chunk when available. + """ + tokens_in: list[int | None] = [None] + tokens_out: list[int | None] = [None] + stop_reason: list[str | None] = [None] + + def on_chunk(chunk: object) -> None: + if not isinstance(chunk, dict): + return + usage = chunk.get("usage") + if usage: + tokens_in[0] = usage.get("prompt_tokens") + tokens_out[0] = usage.get("completion_tokens") + choices = chunk.get("choices") or [] + if choices: + reason = choices[0].get("finish_reason") + if reason: + stop_reason[0] = reason + + def on_done(duration_ms: int, ttft_ms: int | None) -> None: + ti, to, sr = tokens_in[0], tokens_out[0], stop_reason[0] + handle.track_inference( # type: ignore[union-attr] + duration_ms=duration_ms, + input_modality="text", + output_modality="generation", + input_meta=make_gguf_input_meta(prompt, ti), + success=True, + output_meta=make_gguf_output_meta(ti, to, sr, ttft_ms, duration_ms), + ) + + return on_chunk, on_done + + def build_patched_call(original_call): def patched_call(self_inner, *args, **kwargs): handle = getattr(self_inner, GGUF_HANDLE_ATTR, None) @@ -76,9 +146,13 @@ def patched_call(self_inner, *args, **kwargs): return original_call(self_inner, *args, **kwargs) prompt = args[0] if args else kwargs.get("prompt", "") + is_streaming: bool = bool(kwargs.get("stream", False)) t0 = time.perf_counter() try: result = original_call(self_inner, *args, **kwargs) + if is_streaming: + on_chunk, on_done = make_gguf_stream_callbacks(handle, prompt) + return SyncStreamWrapper(result, handle, t0, on_chunk, on_done) duration_ms = elapsed_ms(t0) tokens_in = None tokens_out = None @@ -89,36 +163,15 @@ def patched_call(self_inner, *args, **kwargs): tokens_out = usage.get("completion_tokens") except Exception as exc: debug_gguf_failure("usage extraction", exc) - - input_meta = None - if isinstance(prompt, str) and prompt: - input_meta = TextInputMeta( - char_count=len(prompt), - word_count=len(prompt.split()), - token_count=tokens_in, - ) - - output_meta = None - if tokens_out is not None: - tps = ( - round(tokens_out / duration_ms * 1000, 1) - if duration_ms > 0 - else None - ) - output_meta = GenerationOutputMeta( - task="generation", - tokens_in=tokens_in, - tokens_out=tokens_out, - tokens_per_second=tps, - ) - handle.track_inference( duration_ms=duration_ms, input_modality="text", output_modality="generation", - input_meta=input_meta, + input_meta=make_gguf_input_meta(prompt, tokens_in), success=True, - output_meta=output_meta, + output_meta=make_gguf_output_meta( + tokens_in, tokens_out, None, None, duration_ms + ), ) return result except Exception as exc: diff --git a/wildedge/integrations/openai.py b/wildedge/integrations/openai.py index e5d1525..31281e9 100644 --- a/wildedge/integrations/openai.py +++ b/wildedge/integrations/openai.py @@ -11,7 +11,11 @@ from wildedge import constants from wildedge.events.inference import ApiMeta, GenerationOutputMeta, TextInputMeta from wildedge.integrations.base import BaseExtractor -from wildedge.integrations.common import debug_failure +from wildedge.integrations.common import ( + AsyncStreamWrapper, + SyncStreamWrapper, + debug_failure, +) from wildedge.model import ModelInfo from wildedge.timing import elapsed_ms @@ -58,6 +62,28 @@ def build_input_meta(messages: list, tokens_in: int | None) -> TextInputMeta | N ) +def build_streaming_output_meta( + ttft_ms: int | None, + tokens_in: int | None, + tokens_out: int | None, + stop_reason: str | None, + duration_ms: int, +) -> GenerationOutputMeta: + tps = ( + round(tokens_out / duration_ms * 1000, 1) + if duration_ms > 0 and tokens_out + else None + ) + return GenerationOutputMeta( + task="generation", + tokens_in=tokens_in, + tokens_out=tokens_out, + time_to_first_token_ms=ttft_ms, + tokens_per_second=tps, + stop_reason=stop_reason, + ) + + def build_output_meta( response: object, duration_ms: int ) -> GenerationOutputMeta | None: @@ -145,6 +171,50 @@ def record_inference( ) +def make_openai_stream_callbacks( + handle: ModelHandle, + messages: list, +) -> tuple: + """Return (on_chunk, on_done) callbacks for an OpenAI streaming response. + + on_chunk updates mutable state from each ChatCompletionChunk. + on_done is called with (duration_ms, ttft_ms) when the stream is exhausted. + """ + tokens_in: list[int | None] = [None] + tokens_out: list[int | None] = [None] + stop_reason: list[str | None] = [None] + last_chunk: list[object] = [None] + + def on_chunk(chunk: object) -> None: + last_chunk[0] = chunk + chunk_usage = getattr(chunk, "usage", None) + if chunk_usage is not None: + tokens_in[0] = getattr(chunk_usage, "prompt_tokens", None) + tokens_out[0] = getattr(chunk_usage, "completion_tokens", None) + choices = getattr(chunk, "choices", None) or [] + if choices: + reason = getattr(choices[0], "finish_reason", None) + if reason: + stop_reason[0] = reason + + def on_done(duration_ms: int, ttft_ms: int | None) -> None: + ti, to, sr = tokens_in[0], tokens_out[0], stop_reason[0] + output_meta = build_streaming_output_meta(ttft_ms, ti, to, sr, duration_ms) + handle.track_inference( + duration_ms=duration_ms, + input_modality="text", + output_modality="generation", + success=True, + input_meta=build_input_meta(messages, ti), + output_meta=output_meta, + api_meta=build_api_meta(last_chunk[0]) + if last_chunk[0] is not None + else None, + ) + + return on_chunk, on_done + + def wrap_sync_completions(completions: object, source: str, client_ref: object) -> None: original_create = completions.create # type: ignore[attr-defined] model_handles: dict[str, ModelHandle] = {} @@ -160,8 +230,12 @@ def patched_create(*args, **kwargs): t0 = time.perf_counter() try: result = original_create(*args, **kwargs) - if not is_streaming and handle is not None: - record_inference(handle, result, messages, elapsed_ms(t0)) + if handle is not None: + if is_streaming: + on_chunk, on_done = make_openai_stream_callbacks(handle, messages) + return SyncStreamWrapper(result, handle, t0, on_chunk, on_done) + else: + record_inference(handle, result, messages, elapsed_ms(t0)) return result except Exception as exc: if handle is not None: @@ -191,8 +265,12 @@ async def patched_create(*args, **kwargs): t0 = time.perf_counter() try: result = await original_create(*args, **kwargs) - if not is_streaming and handle is not None: - record_inference(handle, result, messages, elapsed_ms(t0)) + if handle is not None: + if is_streaming: + on_chunk, on_done = make_openai_stream_callbacks(handle, messages) + return AsyncStreamWrapper(result, handle, t0, on_chunk, on_done) + else: + record_inference(handle, result, messages, elapsed_ms(t0)) return result except Exception as exc: if handle is not None: From d26d404dcd30474eb51a9e081a225a664e1a02ad Mon Sep 17 00:00:00 2001 From: Piotr Duda Date: Fri, 27 Mar 2026 08:25:40 +0100 Subject: [PATCH 09/11] Exclude /examples from sdist --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 3a7e63e..7f15e04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ build-backend = "hatchling.build" [tool.hatch.build] exclude = [ "/scripts", + "/examples", ] [tool.hatch.build.targets.wheel] From 7b547c5d0ab8549361211a7fc5eabdb148430970 Mon Sep 17 00:00:00 2001 From: Piotr Duda Date: Tue, 31 Mar 2026 19:38:22 +0200 Subject: [PATCH 10/11] Support for Agentic workflow monitoring (#28) --- .gitignore | 3 +- README.md | 1 - docs/manual-tracking.md | 55 ++++++++ examples/agentic_example.py | 181 ++++++++++++++++++++++++++ tests/test_event_serialization.py | 42 ++++++ tests/test_tracing.py | 93 +++++++++++++ wildedge/__init__.py | 20 ++- wildedge/client.py | 209 ++++++++++++++++++++++++++++++ wildedge/decorators.py | 22 ++++ wildedge/events/__init__.py | 2 + wildedge/events/common.py | 11 ++ wildedge/events/error.py | 26 +++- wildedge/events/feedback.py | 26 +++- wildedge/events/inference.py | 26 +++- wildedge/events/model_download.py | 26 +++- wildedge/events/model_load.py | 26 +++- wildedge/events/model_unload.py | 26 +++- wildedge/events/span.py | 79 +++++++++++ wildedge/integrations/openai.py | 12 +- wildedge/model.py | 115 ++++++++++++++++ wildedge/tracing.py | 167 ++++++++++++++++++++++++ 21 files changed, 1157 insertions(+), 11 deletions(-) create mode 100644 examples/agentic_example.py create mode 100644 tests/test_tracing.py create mode 100644 wildedge/events/common.py create mode 100644 wildedge/events/span.py create mode 100644 wildedge/tracing.py diff --git a/.gitignore b/.gitignore index 64d49ae..468c518 100644 --- a/.gitignore +++ b/.gitignore @@ -213,4 +213,5 @@ marimo/_lsp/ __marimo__/ # Streamlit -.streamlit/secrets.toml \ No newline at end of file +.streamlit/secrets.toml +internal/ diff --git a/README.md b/README.md index 1c24d0b..1777cd2 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,6 @@ client = wildedge.init( If no DSN is configured, the client becomes a no-op and logs a warning. `init(...)` is a convenience wrapper for `WildEdge(...)` + `instrument(...)`. - ## Supported integrations **On-device** diff --git a/docs/manual-tracking.md b/docs/manual-tracking.md index f90f58a..0fa227e 100644 --- a/docs/manual-tracking.md +++ b/docs/manual-tracking.md @@ -215,3 +215,58 @@ handle.feedback(FeedbackType.THUMBS_DOWN) ``` `FeedbackType` values: `THUMBS_UP`, `THUMBS_DOWN`. + +## Track spans for agentic workflows + +Use span events to track non-inference steps like planning, tool calls, retrieval, or memory updates. + +```python +from wildedge.timing import Timer + +with Timer() as t: + tool_result = call_tool() + +client.track_span( + kind="tool", + name="call_tool", + duration_ms=t.elapsed_ms, + status="ok", + attributes={"tool": "search"}, +) +``` + +You can also attach optional correlation fields (`trace_id`, `span_id`, +`parent_span_id`, `run_id`, `agent_id`, `step_index`, `conversation_id`) to any +event by passing them into `track_inference`, `track_error`, `track_feedback`, +or `track_span`. Use `context=` for correlation attributes shared across events. + +### Trace context helpers + +Use `client.trace()` and `client.span()` to auto-populate correlation fields for +all events emitted inside the block. `client.span()` times the block and emits a +span event on exit: + +```python +import wildedge +from wildedge.timing import Timer + +client = wildedge.init() +handle = client.register_model(my_model, model_id="my-org/my-model") + +with client.trace(run_id="run-123", agent_id="agent-1"): + with client.span(kind="agent_step", name="plan", step_index=1): + with Timer() as t: + result = my_model(prompt) + handle.track_inference(duration_ms=t.elapsed_ms, input_modality="text", output_modality="generation") +``` + +If you need to set correlation fields without emitting a span event, use the +lower-level `span_context()` directly: + +```python +with client.trace(run_id="run-123", agent_id="agent-1"): + with wildedge.span_context(step_index=1): + with Timer() as t: + result = my_model(prompt) + handle.track_inference(duration_ms=t.elapsed_ms, input_modality="text", output_modality="generation") +``` diff --git a/examples/agentic_example.py b/examples/agentic_example.py new file mode 100644 index 0000000..446c812 --- /dev/null +++ b/examples/agentic_example.py @@ -0,0 +1,181 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = ["wildedge-sdk", "openai"] +# +# [tool.uv.sources] +# wildedge-sdk = { path = "..", editable = true } +# /// +"""Agentic workflow example with tool use. + +Demonstrates WildEdge tracing for a simple agent that: + - Runs within a trace (one per agent session) + - Wraps each reasoning step in an agent_step span + - Wraps each tool call in a tool span + - Tracks LLM inference automatically via the OpenAI integration + +Run with: uv run agentic_example.py +Requires: OPENROUTER_API_KEY environment variable. Set WILDEDGE_DSN to send events. +""" + +import json +import os +import time +import uuid + +from openai import OpenAI + +import wildedge + +we = wildedge.init( + app_version="1.0.0", + integrations="openai", +) + +openai_client = OpenAI( + base_url="https://openrouter.ai/api/v1", + api_key=os.getenv("OPENROUTER_API_KEY"), +) + +# --- Tools ------------------------------------------------------------------- + +TOOLS = [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Return current weather for a city.", + "parameters": { + "type": "object", + "properties": { + "city": {"type": "string"}, + }, + "required": ["city"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "calculator", + "description": "Evaluate a simple arithmetic expression.", + "parameters": { + "type": "object", + "properties": { + "expression": {"type": "string"}, + }, + "required": ["expression"], + }, + }, + }, +] + + +def get_weather(city: str) -> str: + # ~150ms to simulate a real weather API call. + time.sleep(0.15) + return json.dumps({"city": city, "temperature_c": 18, "condition": "partly cloudy"}) + + +def calculator(expression: str) -> str: + # ~60ms to simulate a remote computation call. + time.sleep(0.06) + try: + result = eval(expression, {"__builtins__": {}}) # noqa: S307 + return json.dumps({"expression": expression, "result": result}) + except Exception as e: + return json.dumps({"error": str(e)}) + + +TOOL_HANDLERS = { + "get_weather": get_weather, + "calculator": calculator, +} + + +# --- Agent loop -------------------------------------------------------------- + + +def call_tool(name: str, arguments: dict) -> str: + with we.span( + kind="tool", + name=name, + input_summary=json.dumps(arguments)[:200], + ) as span: + result = TOOL_HANDLERS[name](**arguments) + span.output_summary = result[:200] + return result + + +def retrieve_context(query: str) -> str: + """Fetch relevant context from the vector store (~120ms).""" + with we.span( + kind="retrieval", + name="vector_search", + input_summary=query[:200], + ) as span: + time.sleep(0.12) + result = f"[context: background knowledge relevant to '{query[:40]}']" + span.output_summary = result + return result + + +def run_agent(task: str, step_index: int, messages: list) -> str: + # Fetch context before the first reasoning step, include it in the user turn. + context = retrieve_context(task) + messages.append({"role": "user", "content": f"{task}\n\nContext: {context}"}) + + while True: + with we.span( + kind="agent_step", + name="reason", + step_index=step_index, + input_summary=task[:200], + ) as span: + response = openai_client.chat.completions.create( + model="qwen/qwen3.5-flash-02-23", + messages=messages, + tools=TOOLS, + tool_choice="auto", + max_tokens=512, + ) + choice = response.choices[0] + span.output_summary = choice.finish_reason + + messages.append(choice.message.model_dump(exclude_none=True)) + + if choice.finish_reason == "tool_calls": + step_index += 1 + for tool_call in choice.message.tool_calls: + arguments = json.loads(tool_call.function.arguments) + result = call_tool(tool_call.function.name, arguments) + messages.append( + { + "role": "tool", + "tool_call_id": tool_call.id, + "content": result, + } + ) + # Not instrumented: context window update between tool calls (~80ms). + # Shows up as a gap stripe in the trace view. + time.sleep(0.08) + else: + return choice.message.content or "" + + +# --- Main -------------------------------------------------------------------- + +TASKS = [ + "What's the weather like in Tokyo, and what is 42 * 18?", + "Is it warmer in Paris or Berlin right now?", +] + +system_prompt = "You are a helpful assistant. Use tools when needed." +messages = [{"role": "system", "content": system_prompt}] + +with we.trace(agent_id="demo-agent", run_id=str(uuid.uuid4())): + for i, task in enumerate(TASKS, start=1): + print(f"\nTask {i}: {task}") + reply = run_agent(task, step_index=i, messages=messages) + print(f"Reply: {reply}") + +we.flush() diff --git a/tests/test_event_serialization.py b/tests/test_event_serialization.py index 12028c7..587940b 100644 --- a/tests/test_event_serialization.py +++ b/tests/test_event_serialization.py @@ -4,6 +4,7 @@ from wildedge.events.inference import InferenceEvent, TextInputMeta from wildedge.events.model_download import AdapterDownload, ModelDownloadEvent from wildedge.events.model_load import AdapterLoad, ModelLoadEvent +from wildedge.events.span import SpanEvent def test_inference_event_to_dict_omits_none_fields(): @@ -72,3 +73,44 @@ def test_feedback_event_enum_and_string_forms(): ) assert enum_event.to_dict()["feedback"]["feedback_type"] == "accept" assert string_event.to_dict()["feedback"]["feedback_type"] == "reject" + + +def test_span_event_to_dict_includes_required_fields(): + event = SpanEvent( + kind="tool", + name="search", + duration_ms=250, + status="ok", + attributes={"provider": "custom"}, + ) + data = event.to_dict() + assert data["event_type"] == "span" + assert data["span"]["kind"] == "tool" + assert data["span"]["attributes"]["provider"] == "custom" + + +def test_span_event_context_serializes_under_context_key(): + event = SpanEvent( + kind="agent_step", + name="plan", + duration_ms=10, + status="ok", + context={"user_id": "u1"}, + ) + data = event.to_dict() + assert data["context"] == {"user_id": "u1"} + assert "attributes" not in data + + +def test_span_event_attributes_and_context_are_independent(): + event = SpanEvent( + kind="tool", + name="search", + duration_ms=50, + status="ok", + attributes={"provider": "custom"}, + context={"user_id": "u1"}, + ) + data = event.to_dict() + assert data["span"]["attributes"] == {"provider": "custom"} + assert data["context"] == {"user_id": "u1"} diff --git a/tests/test_tracing.py b/tests/test_tracing.py new file mode 100644 index 0000000..944f1e0 --- /dev/null +++ b/tests/test_tracing.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from wildedge.client import SpanContextManager +from wildedge.model import ModelHandle, ModelInfo +from wildedge.tracing import get_span_context, span_context, trace_context + + +def test_track_inference_uses_trace_context(): + events: list[dict] = [] + + def publish(event: dict) -> None: + events.append(event) + + handle = ModelHandle( + "model-1", + ModelInfo( + model_name="test", + model_version="1.0", + model_source="local", + model_format="onnx", + ), + publish, + ) + + with trace_context( + trace_id="trace-123", + run_id="run-1", + agent_id="agent-1", + attributes={"trace_key": "trace_val"}, + ): + with span_context(span_id="span-abc", step_index=2, attributes={"span_key": 2}): + handle.track_inference(duration_ms=5) + + assert events[0]["trace_id"] == "trace-123" + assert events[0]["parent_span_id"] == "span-abc" + assert events[0]["run_id"] == "run-1" + assert events[0]["agent_id"] == "agent-1" + assert events[0]["step_index"] == 2 + assert events[0]["attributes"] == {"trace_key": "trace_val", "span_key": 2} + + +class _FakeClient: + def __init__(self, events: list[dict]) -> None: + self._events = events + + def track_span(self, **kwargs) -> str: + self._events.append(kwargs) + return kwargs.get("span_id", "") + + +def test_span_root_has_no_parent(): + """A root span must not reference itself as its own parent.""" + events: list[dict] = [] + client = _FakeClient(events) + + with SpanContextManager(client, kind="agent_step", name="root"): + pass + + assert len(events) == 1 + assert events[0]["parent_span_id"] is None + + +def test_span_context_restored_after_exit(): + """The active span context must revert to the parent after a span exits.""" + events: list[dict] = [] + client = _FakeClient(events) + + with span_context(span_id="parent-span"): + with SpanContextManager(client, kind="agent_step", name="child"): + inner_id = get_span_context().span_id + + assert get_span_context().span_id == "parent-span" + + assert inner_id != "parent-span" + assert events[0]["parent_span_id"] == "parent-span" + assert events[0]["span_id"] != "parent-span" + + +def test_nested_spans_correct_parent_chain(): + """Nested spans must each point to their direct parent, not themselves.""" + events: list[dict] = [] + client = _FakeClient(events) + + with SpanContextManager(client, kind="agent_step", name="outer") as outer: + with SpanContextManager(client, kind="tool", name="inner") as inner: + pass + + assert len(events) == 2 + inner_ev, outer_ev = events[0], events[1] + assert inner_ev["span_id"] == inner.span_id + assert inner_ev["parent_span_id"] == outer.span_id + assert outer_ev["span_id"] == outer.span_id + assert outer_ev["parent_span_id"] is None diff --git a/wildedge/__init__.py b/wildedge/__init__.py index f1f449b..2b7993f 100644 --- a/wildedge/__init__.py +++ b/wildedge/__init__.py @@ -1,6 +1,6 @@ """WildEdge Python SDK.""" -from wildedge.client import WildEdge +from wildedge.client import SpanContextManager, WildEdge from wildedge.convenience import init from wildedge.decorators import track from wildedge.events import ( @@ -15,13 +15,22 @@ GenerationConfig, GenerationOutputMeta, ImageInputMeta, + SpanEvent, TextInputMeta, ) +from wildedge.events.span import SpanKind, SpanStatus from wildedge.platforms import capture_hardware from wildedge.platforms.device_info import DeviceInfo from wildedge.platforms.hardware import HardwareContext, ThermalContext from wildedge.queue import QueuePolicy from wildedge.timing import Timer +from wildedge.tracing import ( + SpanContext, + TraceContext, + get_span_context, + get_trace_context, + span_context, +) __all__ = [ "WildEdge", @@ -42,7 +51,16 @@ "GenerationConfig", "AdapterLoad", "AdapterDownload", + "SpanEvent", "FeedbackType", "ErrorCode", "Timer", + "span_context", + "TraceContext", + "SpanContext", + "get_trace_context", + "get_span_context", + "SpanKind", + "SpanStatus", + "SpanContextManager", ] diff --git a/wildedge/client.py b/wildedge/client.py index da5e052..627cbc1 100644 --- a/wildedge/client.py +++ b/wildedge/client.py @@ -12,6 +12,8 @@ from wildedge import constants from wildedge.consumer import Consumer from wildedge.dead_letters import DeadLetterStore +from wildedge.events import SpanEvent +from wildedge.events.span import SpanKind, SpanStatus from wildedge.hubs.base import BaseHubTracker from wildedge.hubs.huggingface import HuggingFaceHubTracker from wildedge.hubs.registry import supported_hubs @@ -39,6 +41,14 @@ from wildedge.queue import EventQueue, QueuePolicy from wildedge.settings import read_client_env, resolve_app_identity from wildedge.timing import Timer, elapsed_ms +from wildedge.tracing import ( + SpanContext, + _merge_correlation_fields, + _reset_span_context, + _set_span_context, + get_span_context, + trace_context, +) from wildedge.transmitter import Transmitter DSN_FORMAT = "'https://@ingest.wildedge.dev/'" @@ -103,6 +113,101 @@ def parse_dsn(dsn: str) -> tuple[str, str, str]: ] +class SpanContextManager: + def __init__( + self, + client: WildEdge, + *, + kind: SpanKind, + name: str, + status: SpanStatus = "ok", + model_id: str | None = None, + input_summary: str | None = None, + output_summary: str | None = None, + attributes: dict[str, Any] | None = None, + trace_id: str | None = None, + span_id: str | None = None, + parent_span_id: str | None = None, + run_id: str | None = None, + agent_id: str | None = None, + step_index: int | None = None, + conversation_id: str | None = None, + context: dict[str, Any] | None = None, + ): + self._client = client + self.kind = kind + self.name = name + self.status = status + self.model_id = model_id + self.input_summary = input_summary + self.output_summary = output_summary + self.attributes = attributes + self.trace_id = trace_id + self.span_id = span_id + self.parent_span_id = parent_span_id + self.run_id = run_id + self.agent_id = agent_id + self.step_index = step_index + self.conversation_id = conversation_id + self.context = context + self._t0: float | None = None + self._span_token = None + + def __enter__(self): + self._t0 = time.perf_counter() + if self.span_id is None: + self.span_id = str(uuid.uuid4()) + if self.parent_span_id is None: + current = get_span_context() + self.parent_span_id = current.span_id if current else None + self._span_token = _set_span_context( + SpanContext( + span_id=self.span_id, + parent_span_id=self.parent_span_id, + step_index=self.step_index, + attributes=self.context, + ) + ) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if self._t0 is None: + return False + duration_ms = elapsed_ms(self._t0) + status = "error" if exc_type else self.status + # Restore parent span context before emitting, so _merge_correlation_fields + # sees the parent context rather than this span (which would make the span + # appear as its own parent). + if self._span_token is not None: + _reset_span_context(self._span_token) + self._span_token = None + self._client.track_span( + kind=self.kind, + name=self.name, + duration_ms=duration_ms, + status=status, + model_id=self.model_id, + input_summary=self.input_summary, + output_summary=self.output_summary, + attributes=self.attributes, + trace_id=self.trace_id, + span_id=self.span_id, + parent_span_id=self.parent_span_id, + run_id=self.run_id, + agent_id=self.agent_id, + step_index=self.step_index, + conversation_id=self.conversation_id, + context=self.context, + ) + return False + + async def __aenter__(self): + return self.__enter__() + + async def __aexit__(self, exc_type, exc_val, exc_tb): + return self.__exit__(exc_type, exc_val, exc_tb) + + class WildEdge: """ WildEdge on-device ML monitoring client. @@ -381,6 +486,110 @@ def register_model( return handle + def trace( + self, + *, + trace_id: str | None = None, + run_id: str | None = None, + agent_id: str | None = None, + conversation_id: str | None = None, + attributes: dict[str, Any] | None = None, + ): + """Context manager that sets trace correlation fields.""" + return trace_context( + trace_id=trace_id, + run_id=run_id, + agent_id=agent_id, + conversation_id=conversation_id, + attributes=attributes, + ) + + def span( + self, + *, + kind: SpanKind, + name: str, + status: SpanStatus = "ok", + model_id: str | None = None, + input_summary: str | None = None, + output_summary: str | None = None, + attributes: dict[str, Any] | None = None, + trace_id: str | None = None, + span_id: str | None = None, + parent_span_id: str | None = None, + run_id: str | None = None, + agent_id: str | None = None, + step_index: int | None = None, + conversation_id: str | None = None, + context: dict[str, Any] | None = None, + ) -> SpanContextManager: + """Context manager that times and emits a span event.""" + return SpanContextManager( + self, + kind=kind, + name=name, + status=status, + model_id=model_id, + input_summary=input_summary, + output_summary=output_summary, + attributes=attributes, + trace_id=trace_id, + span_id=span_id, + parent_span_id=parent_span_id, + run_id=run_id, + agent_id=agent_id, + step_index=step_index, + conversation_id=conversation_id, + context=context, + ) + + def track_span( + self, + *, + kind: SpanKind, + name: str, + duration_ms: int, + status: SpanStatus = "ok", + model_id: str | None = None, + input_summary: str | None = None, + output_summary: str | None = None, + attributes: dict[str, Any] | None = None, + trace_id: str | None = None, + span_id: str | None = None, + parent_span_id: str | None = None, + run_id: str | None = None, + agent_id: str | None = None, + step_index: int | None = None, + conversation_id: str | None = None, + context: dict[str, Any] | None = None, + ) -> str: + """Emit a span event for agentic workflows and tooling.""" + correlation = _merge_correlation_fields( + trace_id=trace_id, + span_id=span_id, + parent_span_id=parent_span_id, + run_id=run_id, + agent_id=agent_id, + step_index=step_index, + conversation_id=conversation_id, + context=context, + ) + if correlation["span_id"] is None: + correlation["span_id"] = str(uuid.uuid4()) + event = SpanEvent( + kind=kind, + name=name, + duration_ms=duration_ms, + status=status, + model_id=model_id, + input_summary=input_summary, + output_summary=output_summary, + attributes=attributes, + **correlation, + ) + self.publish(event.to_dict()) + return correlation["span_id"] + def _find_extractor(self, model_obj: object) -> BaseExtractor | None: for candidate in DEFAULT_EXTRACTORS: if candidate.can_handle(model_obj): diff --git a/wildedge/decorators.py b/wildedge/decorators.py index fe2e3ff..f953159 100644 --- a/wildedge/decorators.py +++ b/wildedge/decorators.py @@ -38,6 +38,14 @@ def __init__( input_meta: Any = None, output_meta: Any = None, generation_config: Any = None, + trace_id: str | None = None, + span_id: str | None = None, + parent_span_id: str | None = None, + run_id: str | None = None, + agent_id: str | None = None, + step_index: int | None = None, + conversation_id: str | None = None, + context: dict[str, Any] | None = None, ): self.handle = handle self.input_type = input_type @@ -46,6 +54,16 @@ def __init__( self.input_meta = input_meta self.output_meta = output_meta self.generation_config = generation_config + self._correlation = dict( + trace_id=trace_id, + span_id=span_id, + parent_span_id=parent_span_id, + run_id=run_id, + agent_id=agent_id, + step_index=step_index, + conversation_id=conversation_id, + context=context, + ) self.start_time: float | None = None def __call__(self, func): @@ -62,6 +80,7 @@ def wrapper(*args, **kwargs): input_meta=self.input_meta, output_meta=self.output_meta, generation_config=self.generation_config, + **self._correlation, ) return result except Exception as exc: @@ -69,6 +88,7 @@ def wrapper(*args, **kwargs): self.handle.track_error( error_code="UNKNOWN", error_message=str(exc)[: constants.ERROR_MSG_MAX_LEN], + **self._correlation, ) raise @@ -89,6 +109,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): error_message=str(exc_val)[: constants.ERROR_MSG_MAX_LEN] if exc_val else None, + **self._correlation, ) else: self.handle.track_inference( @@ -99,5 +120,6 @@ def __exit__(self, exc_type, exc_val, exc_tb): input_meta=self.input_meta, output_meta=self.output_meta, generation_config=self.generation_config, + **self._correlation, ) return False diff --git a/wildedge/events/__init__.py b/wildedge/events/__init__.py index b08bc78..6648c40 100644 --- a/wildedge/events/__init__.py +++ b/wildedge/events/__init__.py @@ -19,6 +19,7 @@ from wildedge.events.model_download import AdapterDownload, ModelDownloadEvent from wildedge.events.model_load import AdapterLoad, ModelLoadEvent from wildedge.events.model_unload import ModelUnloadEvent +from wildedge.events.span import SpanEvent __all__ = [ "ApiMeta", @@ -40,6 +41,7 @@ "ModelDownloadEvent", "ModelLoadEvent", "ModelUnloadEvent", + "SpanEvent", "TextInputMeta", "TopKPrediction", ] diff --git a/wildedge/events/common.py b/wildedge/events/common.py new file mode 100644 index 0000000..399b55b --- /dev/null +++ b/wildedge/events/common.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from typing import Any + + +def add_optional_fields(event: dict, fields: dict[str, Any]) -> dict: + """Add non-None fields to an event payload.""" + for key, value in fields.items(): + if value is not None: + event[key] = value + return event diff --git a/wildedge/events/error.py b/wildedge/events/error.py index 13d29f1..e814120 100644 --- a/wildedge/events/error.py +++ b/wildedge/events/error.py @@ -23,6 +23,14 @@ class ErrorEvent: error_message: str | None = None stack_trace_hash: str | None = None related_event_id: str | None = None + trace_id: str | None = None + span_id: str | None = None + parent_span_id: str | None = None + run_id: str | None = None + agent_id: str | None = None + step_index: int | None = None + conversation_id: str | None = None + context: dict[str, Any] | None = None event_id: str = field(default_factory=lambda: str(uuid.uuid4())) timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) @@ -40,10 +48,26 @@ def to_dict(self) -> dict: if self.related_event_id is not None: error_data["related_event_id"] = self.related_event_id - return { + event = { "event_id": self.event_id, "event_type": "error", "timestamp": self.timestamp.isoformat(), "model_id": self.model_id, "error": error_data, } + from wildedge.events.common import add_optional_fields + + add_optional_fields( + event, + { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self.parent_span_id, + "run_id": self.run_id, + "agent_id": self.agent_id, + "step_index": self.step_index, + "conversation_id": self.conversation_id, + "attributes": self.context, + }, + ) + return event diff --git a/wildedge/events/feedback.py b/wildedge/events/feedback.py index 650904b..dfb6efa 100644 --- a/wildedge/events/feedback.py +++ b/wildedge/events/feedback.py @@ -24,6 +24,14 @@ class FeedbackEvent: feedback_type: str | FeedbackType delay_ms: int | None = None edit_distance: int | None = None + trace_id: str | None = None + span_id: str | None = None + parent_span_id: str | None = None + run_id: str | None = None + agent_id: str | None = None + step_index: int | None = None + conversation_id: str | None = None + context: dict[str, Any] | None = None event_id: str = field(default_factory=lambda: str(uuid.uuid4())) timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) @@ -42,10 +50,26 @@ def to_dict(self) -> dict: if self.edit_distance is not None: feedback_data["edit_distance"] = self.edit_distance - return { + event = { "event_id": self.event_id, "event_type": "feedback", "timestamp": self.timestamp.isoformat(), "model_id": self.model_id, "feedback": feedback_data, } + from wildedge.events.common import add_optional_fields + + add_optional_fields( + event, + { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self.parent_span_id, + "run_id": self.run_id, + "agent_id": self.agent_id, + "step_index": self.step_index, + "conversation_id": self.conversation_id, + "attributes": self.context, + }, + ) + return event diff --git a/wildedge/events/inference.py b/wildedge/events/inference.py index 0c02996..ef76ae5 100644 --- a/wildedge/events/inference.py +++ b/wildedge/events/inference.py @@ -304,6 +304,14 @@ class InferenceEvent: generation_config: GenerationConfig | None = None hardware: HardwareContext | None = None api_meta: ApiMeta | None = None + trace_id: str | None = None + span_id: str | None = None + parent_span_id: str | None = None + run_id: str | None = None + agent_id: str | None = None + step_index: int | None = None + conversation_id: str | None = None + context: dict[str, Any] | None = None event_id: str = field(default_factory=lambda: str(uuid.uuid4())) timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) inference_id: str = field(default_factory=lambda: str(uuid.uuid4())) @@ -333,10 +341,26 @@ def to_dict(self) -> dict: if self.api_meta is not None: inference_data["api_meta"] = self.api_meta.to_dict() - return { + event = { "event_id": self.event_id, "event_type": "inference", "timestamp": self.timestamp.isoformat(), "model_id": self.model_id, "inference": inference_data, } + from wildedge.events.common import add_optional_fields + + add_optional_fields( + event, + { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self.parent_span_id, + "run_id": self.run_id, + "agent_id": self.agent_id, + "step_index": self.step_index, + "conversation_id": self.conversation_id, + "attributes": self.context, + }, + ) + return event diff --git a/wildedge/events/model_download.py b/wildedge/events/model_download.py index 784c62f..2e68a3a 100644 --- a/wildedge/events/model_download.py +++ b/wildedge/events/model_download.py @@ -49,6 +49,14 @@ class ModelDownloadEvent: cdn_edge: str | None = None error_code: str | None = None adapter: AdapterDownload | None = None + trace_id: str | None = None + span_id: str | None = None + parent_span_id: str | None = None + run_id: str | None = None + agent_id: str | None = None + step_index: int | None = None + conversation_id: str | None = None + context: dict[str, Any] | None = None event_id: str = field(default_factory=lambda: str(uuid.uuid4())) timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) @@ -83,10 +91,26 @@ def to_dict(self) -> dict: if self.adapter is not None: download_data["adapter"] = self.adapter.to_dict() - return { + event = { "event_id": self.event_id, "event_type": "model_download", "timestamp": self.timestamp.isoformat(), "model_id": self.model_id, "download": download_data, } + from wildedge.events.common import add_optional_fields + + add_optional_fields( + event, + { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self.parent_span_id, + "run_id": self.run_id, + "agent_id": self.agent_id, + "step_index": self.step_index, + "conversation_id": self.conversation_id, + "attributes": self.context, + }, + ) + return event diff --git a/wildedge/events/model_load.py b/wildedge/events/model_load.py index 9fae742..e058092 100644 --- a/wildedge/events/model_load.py +++ b/wildedge/events/model_load.py @@ -55,6 +55,14 @@ class ModelLoadEvent: cold_start: bool | None = None compile_time_ms: int | None = None adapter: AdapterLoad | None = None + trace_id: str | None = None + span_id: str | None = None + parent_span_id: str | None = None + run_id: str | None = None + agent_id: str | None = None + step_index: int | None = None + conversation_id: str | None = None + context: dict[str, Any] | None = None event_id: str = field(default_factory=lambda: str(uuid.uuid4())) timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) @@ -84,10 +92,26 @@ def to_dict(self) -> dict: if self.adapter is not None: load_data["adapter"] = self.adapter.to_dict() - return { + event = { "event_id": self.event_id, "event_type": "model_load", "timestamp": self.timestamp.isoformat(), "model_id": self.model_id, "load": load_data, } + from wildedge.events.common import add_optional_fields + + add_optional_fields( + event, + { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self.parent_span_id, + "run_id": self.run_id, + "agent_id": self.agent_id, + "step_index": self.step_index, + "conversation_id": self.conversation_id, + "attributes": self.context, + }, + ) + return event diff --git a/wildedge/events/model_unload.py b/wildedge/events/model_unload.py index 9dab481..16def90 100644 --- a/wildedge/events/model_unload.py +++ b/wildedge/events/model_unload.py @@ -14,6 +14,14 @@ class ModelUnloadEvent: memory_freed_bytes: int | None = None peak_memory_bytes: int | None = None uptime_ms: int | None = None + trace_id: str | None = None + span_id: str | None = None + parent_span_id: str | None = None + run_id: str | None = None + agent_id: str | None = None + step_index: int | None = None + conversation_id: str | None = None + context: dict[str, Any] | None = None event_id: str = field(default_factory=lambda: str(uuid.uuid4())) timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) @@ -30,10 +38,26 @@ def to_dict(self) -> dict: if v is not None: unload_data[k] = v - return { + event = { "event_id": self.event_id, "event_type": "model_unload", "timestamp": self.timestamp.isoformat(), "model_id": self.model_id, "unload": unload_data, } + from wildedge.events.common import add_optional_fields + + add_optional_fields( + event, + { + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self.parent_span_id, + "run_id": self.run_id, + "agent_id": self.agent_id, + "step_index": self.step_index, + "conversation_id": self.conversation_id, + "attributes": self.context, + }, + ) + return event diff --git a/wildedge/events/span.py b/wildedge/events/span.py new file mode 100644 index 0000000..9d5be3c --- /dev/null +++ b/wildedge/events/span.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +import uuid +from dataclasses import dataclass, field +from datetime import datetime, timezone +from typing import Any, Literal + +from wildedge.events.common import add_optional_fields + +SpanKind = Literal[ + "agent_step", + "tool", + "retrieval", + "memory", + "router", + "guardrail", + "cache", + "eval", + "custom", +] +SpanStatus = Literal["ok", "error"] + + +@dataclass +class SpanEvent: + kind: SpanKind + name: str + duration_ms: int + status: SpanStatus + model_id: str | None = None + input_summary: str | None = None + output_summary: str | None = None + attributes: dict[str, Any] | None = None + trace_id: str | None = None + span_id: str | None = None + parent_span_id: str | None = None + run_id: str | None = None + agent_id: str | None = None + step_index: int | None = None + conversation_id: str | None = None + context: dict[str, Any] | None = None + event_id: str = field(default_factory=lambda: str(uuid.uuid4())) + timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + + def to_dict(self) -> dict: + span_data: dict[str, Any] = { + "kind": self.kind, + "name": self.name, + "duration_ms": self.duration_ms, + "status": self.status, + } + if self.input_summary is not None: + span_data["input_summary"] = self.input_summary + if self.output_summary is not None: + span_data["output_summary"] = self.output_summary + if self.attributes is not None: + span_data["attributes"] = self.attributes + + event = { + "event_id": self.event_id, + "event_type": "span", + "timestamp": self.timestamp.isoformat(), + "span": span_data, + } + add_optional_fields( + event, + { + "model_id": self.model_id, + "trace_id": self.trace_id, + "span_id": self.span_id, + "parent_span_id": self.parent_span_id, + "run_id": self.run_id, + "agent_id": self.agent_id, + "step_index": self.step_index, + "conversation_id": self.conversation_id, + "context": self.context, + }, + ) + return event diff --git a/wildedge/integrations/openai.py b/wildedge/integrations/openai.py index 31281e9..700aace 100644 --- a/wildedge/integrations/openai.py +++ b/wildedge/integrations/openai.py @@ -45,13 +45,21 @@ def source_from_base_url(base_url: str | None) -> str: return SOURCE_BY_HOSTNAME.get(hostname or "", hostname or "openai") +def _msg_role(m) -> str | None: + return m.get("role") if isinstance(m, dict) else getattr(m, "role", None) + + +def _msg_content(m) -> str | None: + return m.get("content") if isinstance(m, dict) else getattr(m, "content", None) + + def build_input_meta(messages: list, tokens_in: int | None) -> TextInputMeta | None: if not messages: return None - last_user = next((m for m in reversed(messages) if m.get("role") == "user"), None) + last_user = next((m for m in reversed(messages) if _msg_role(m) == "user"), None) if not last_user: return None - content = last_user.get("content", "") + content = _msg_content(last_user) or "" if not isinstance(content, str) or not content: return None return TextInputMeta( diff --git a/wildedge/model.py b/wildedge/model.py index de7e7b1..2502b14 100644 --- a/wildedge/model.py +++ b/wildedge/model.py @@ -28,6 +28,7 @@ from wildedge.logging import logger from wildedge.platforms import capture_hardware, is_sampling from wildedge.platforms.hardware import HardwareContext +from wildedge.tracing import _merge_correlation_fields @dataclass @@ -68,8 +69,26 @@ def track_load( accelerator: str | None = None, success: bool = True, error_code: str | None = None, + trace_id: str | None = None, + span_id: str | None = None, + parent_span_id: str | None = None, + run_id: str | None = None, + agent_id: str | None = None, + step_index: int | None = None, + conversation_id: str | None = None, + context: dict[str, Any] | None = None, **kwargs: Any, ) -> None: + correlation = _merge_correlation_fields( + trace_id=trace_id, + span_id=span_id, + parent_span_id=parent_span_id, + run_id=run_id, + agent_id=agent_id, + step_index=step_index, + conversation_id=conversation_id, + context=context, + ) event = ModelLoadEvent( model_id=self.model_id, duration_ms=duration_ms, @@ -77,6 +96,7 @@ def track_load( accelerator=accelerator or self.detected_accelerator, success=success, error_code=error_code, + **correlation, **kwargs, ) self.publish(event.to_dict()) @@ -89,7 +109,25 @@ def track_unload( memory_freed_bytes: int | None = None, peak_memory_bytes: int | None = None, uptime_ms: int | None = None, + trace_id: str | None = None, + span_id: str | None = None, + parent_span_id: str | None = None, + run_id: str | None = None, + agent_id: str | None = None, + step_index: int | None = None, + conversation_id: str | None = None, + context: dict[str, Any] | None = None, ) -> None: + correlation = _merge_correlation_fields( + trace_id=trace_id, + span_id=span_id, + parent_span_id=parent_span_id, + run_id=run_id, + agent_id=agent_id, + step_index=step_index, + conversation_id=conversation_id, + context=context, + ) event = ModelUnloadEvent( model_id=self.model_id, duration_ms=duration_ms, @@ -97,6 +135,7 @@ def track_unload( memory_freed_bytes=memory_freed_bytes, peak_memory_bytes=peak_memory_bytes, uptime_ms=uptime_ms, + **correlation, ) self.publish(event.to_dict()) @@ -111,8 +150,26 @@ def track_download( resumed: bool, cache_hit: bool, success: bool, + trace_id: str | None = None, + span_id: str | None = None, + parent_span_id: str | None = None, + run_id: str | None = None, + agent_id: str | None = None, + step_index: int | None = None, + conversation_id: str | None = None, + context: dict[str, Any] | None = None, **kwargs: Any, ) -> None: + correlation = _merge_correlation_fields( + trace_id=trace_id, + span_id=span_id, + parent_span_id=parent_span_id, + run_id=run_id, + agent_id=agent_id, + step_index=step_index, + conversation_id=conversation_id, + context=context, + ) event = ModelDownloadEvent( model_id=self.model_id, source_url=source_url, @@ -124,6 +181,7 @@ def track_download( resumed=resumed, cache_hit=cache_hit, success=success, + **correlation, **kwargs, ) self.publish(event.to_dict()) @@ -146,9 +204,27 @@ def track_inference( generation_config: GenerationConfig | None = None, hardware: HardwareContext | None = None, api_meta: ApiMeta | None = None, + trace_id: str | None = None, + span_id: str | None = None, + parent_span_id: str | None = None, + run_id: str | None = None, + agent_id: str | None = None, + step_index: int | None = None, + conversation_id: str | None = None, + context: dict[str, Any] | None = None, ) -> str: if hardware is None and is_sampling(): hardware = capture_hardware() + correlation = _merge_correlation_fields( + trace_id=trace_id, + span_id=span_id, + parent_span_id=parent_span_id, + run_id=run_id, + agent_id=agent_id, + step_index=step_index, + conversation_id=conversation_id, + context=context, + ) event = InferenceEvent( model_id=self.model_id, duration_ms=duration_ms, @@ -162,6 +238,7 @@ def track_inference( generation_config=generation_config, hardware=hardware, api_meta=api_meta, + **correlation, ) self.last_inference_id = event.inference_id self.publish(event.to_dict()) @@ -174,13 +251,32 @@ def track_feedback( *, delay_ms: int | None = None, edit_distance: int | None = None, + trace_id: str | None = None, + span_id: str | None = None, + parent_span_id: str | None = None, + run_id: str | None = None, + agent_id: str | None = None, + step_index: int | None = None, + conversation_id: str | None = None, + context: dict[str, Any] | None = None, ) -> None: + correlation = _merge_correlation_fields( + trace_id=trace_id, + span_id=span_id, + parent_span_id=parent_span_id, + run_id=run_id, + agent_id=agent_id, + step_index=step_index, + conversation_id=conversation_id, + context=context, + ) event = FeedbackEvent( model_id=self.model_id, related_inference_id=related_inference_id, feedback_type=feedback_type, delay_ms=delay_ms, edit_distance=edit_distance, + **correlation, ) self.publish(event.to_dict()) @@ -205,13 +301,32 @@ def track_error( error_message: str | None = None, stack_trace_hash: str | None = None, related_event_id: str | None = None, + trace_id: str | None = None, + span_id: str | None = None, + parent_span_id: str | None = None, + run_id: str | None = None, + agent_id: str | None = None, + step_index: int | None = None, + conversation_id: str | None = None, + context: dict[str, Any] | None = None, ) -> None: + correlation = _merge_correlation_fields( + trace_id=trace_id, + span_id=span_id, + parent_span_id=parent_span_id, + run_id=run_id, + agent_id=agent_id, + step_index=step_index, + conversation_id=conversation_id, + context=context, + ) event = ErrorEvent( model_id=self.model_id, error_code=error_code, error_message=error_message, stack_trace_hash=stack_trace_hash, related_event_id=related_event_id, + **correlation, ) self.publish(event.to_dict()) diff --git a/wildedge/tracing.py b/wildedge/tracing.py new file mode 100644 index 0000000..a205543 --- /dev/null +++ b/wildedge/tracing.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import contextlib +import contextvars +import uuid +from dataclasses import dataclass +from typing import Any + + +@dataclass(frozen=True) +class TraceContext: + trace_id: str + run_id: str | None = None + agent_id: str | None = None + conversation_id: str | None = None + attributes: dict[str, Any] | None = None + + +@dataclass(frozen=True) +class SpanContext: + span_id: str + parent_span_id: str | None = None + step_index: int | None = None + attributes: dict[str, Any] | None = None + + +_TRACE_CTX: contextvars.ContextVar[TraceContext | None] = contextvars.ContextVar( + "wildedge_trace_ctx", default=None +) +_SPAN_CTX: contextvars.ContextVar[SpanContext | None] = contextvars.ContextVar( + "wildedge_span_ctx", default=None +) + + +def get_trace_context() -> TraceContext | None: + return _TRACE_CTX.get() + + +def get_span_context() -> SpanContext | None: + return _SPAN_CTX.get() + + +def _set_trace_context(ctx: TraceContext) -> contextvars.Token: + return _TRACE_CTX.set(ctx) + + +def _reset_trace_context(token: contextvars.Token) -> None: + _TRACE_CTX.reset(token) + + +def _set_span_context(ctx: SpanContext) -> contextvars.Token: + return _SPAN_CTX.set(ctx) + + +def _reset_span_context(token: contextvars.Token) -> None: + _SPAN_CTX.reset(token) + + +@contextlib.contextmanager +def trace_context( + *, + trace_id: str | None = None, + run_id: str | None = None, + agent_id: str | None = None, + conversation_id: str | None = None, + attributes: dict[str, Any] | None = None, +): + if trace_id is None: + trace_id = str(uuid.uuid4()) + token = _set_trace_context( + TraceContext( + trace_id=trace_id, + run_id=run_id, + agent_id=agent_id, + conversation_id=conversation_id, + attributes=attributes, + ) + ) + try: + yield get_trace_context() + finally: + _reset_trace_context(token) + + +@contextlib.contextmanager +def span_context( + *, + span_id: str | None = None, + parent_span_id: str | None = None, + step_index: int | None = None, + attributes: dict[str, Any] | None = None, +): + """Low-level context manager that sets span correlation fields without emitting a span event. + + Prefer client.span() for most use cases. Use this only when you need correlation + fields attached to auto-instrumented events (e.g. an OpenAI call) without emitting + a redundant span wrapper. + """ + if span_id is None: + span_id = str(uuid.uuid4()) + if parent_span_id is None: + current = get_span_context() + parent_span_id = current.span_id if current else None + token = _set_span_context( + SpanContext( + span_id=span_id, + parent_span_id=parent_span_id, + step_index=step_index, + attributes=attributes, + ) + ) + try: + yield get_span_context() + finally: + _reset_span_context(token) + + +def _merge_context(*candidates: dict[str, Any] | None) -> dict[str, Any] | None: + merged: dict[str, Any] = {} + for attrs in candidates: + if not attrs: + continue + merged.update(attrs) + return merged or None + + +def _merge_correlation_fields( + *, + trace_id: str | None = None, + span_id: str | None = None, + parent_span_id: str | None = None, + run_id: str | None = None, + agent_id: str | None = None, + step_index: int | None = None, + conversation_id: str | None = None, + context: dict[str, Any] | None = None, +) -> dict[str, Any]: + trace = get_trace_context() + span = get_span_context() + + resolved_trace_id = trace_id or (trace.trace_id if trace else None) + resolved_span_id = span_id + resolved_parent_span_id = parent_span_id or (span.span_id if span else None) + resolved_run_id = run_id or (trace.run_id if trace else None) + resolved_agent_id = agent_id or (trace.agent_id if trace else None) + resolved_step_index = ( + step_index if step_index is not None else (span.step_index if span else None) + ) + resolved_conversation_id = conversation_id or ( + trace.conversation_id if trace else None + ) + resolved_context = _merge_context( + trace.attributes if trace else None, + span.attributes if span else None, + context, + ) + + return { + "trace_id": resolved_trace_id, + "span_id": resolved_span_id, + "parent_span_id": resolved_parent_span_id, + "run_id": resolved_run_id, + "agent_id": resolved_agent_id, + "step_index": resolved_step_index, + "conversation_id": resolved_conversation_id, + "context": resolved_context, + } From fced74b3bfba59d169e83a4033b134defb63145f Mon Sep 17 00:00:00 2001 From: Piotr Duda Date: Tue, 31 Mar 2026 20:45:58 +0300 Subject: [PATCH 11/11] 0.1.3 release bump --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7f15e04..fa9edea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "wildedge-sdk" -version = "0.1.2" +version = "0.1.3" description = "On-device ML inference monitoring for Python" readme = "README.md" requires-python = ">=3.10"