From 2dc4384afad596be54c6b582f4f5ee0da8a42672 Mon Sep 17 00:00:00 2001 From: James Ding Date: Fri, 7 Nov 2025 22:06:15 -0600 Subject: [PATCH 1/5] refactor: format and bring python 3.9 support Signed-off-by: James Ding --- src/fish_audio_sdk/__init__.py | 13 ++++++++++++- src/fish_audio_sdk/apis.py | 3 ++- src/fish_audio_sdk/websocket.py | 22 ++++++++++++---------- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/fish_audio_sdk/__init__.py b/src/fish_audio_sdk/__init__.py index b1143bc..d2ca886 100644 --- a/src/fish_audio_sdk/__init__.py +++ b/src/fish_audio_sdk/__init__.py @@ -1,6 +1,17 @@ from .apis import Session from .exceptions import HttpCodeErr, WebSocketErr -from .schemas import ASRRequest, TTSRequest, ReferenceAudio, Prosody, PaginatedResponse, ModelEntity, APICreditEntity, StartEvent, TextEvent, CloseEvent +from .schemas import ( + ASRRequest, + TTSRequest, + ReferenceAudio, + Prosody, + PaginatedResponse, + ModelEntity, + APICreditEntity, + StartEvent, + TextEvent, + CloseEvent, +) from .websocket import WebSocketSession, AsyncWebSocketSession __all__ = [ diff --git a/src/fish_audio_sdk/apis.py b/src/fish_audio_sdk/apis.py index a75aa00..9e1f4b5 100644 --- a/src/fish_audio_sdk/apis.py +++ b/src/fish_audio_sdk/apis.py @@ -168,4 +168,5 @@ def get_package(this) -> G[PackageEntity]: return PackageEntity.model_validate(response.json()) -filter_none = lambda d: {k: v for k, v in d.items() if v is not None} +def filter_none(d): + return {k: v for k, v in d.items() if v is not None} diff --git a/src/fish_audio_sdk/websocket.py b/src/fish_audio_sdk/websocket.py index 41efe33..841b3da 100644 --- a/src/fish_audio_sdk/websocket.py +++ b/src/fish_audio_sdk/websocket.py @@ -72,12 +72,13 @@ def sender(): try: message = ws.receive_bytes() data = ormsgpack.unpackb(message) - match data["event"]: - case "audio": - yield data["audio"] - case "finish" if data["reason"] == "error": + event = data["event"] + if event == "audio": + yield data["audio"] + elif event == "finish": + if data["reason"] == "error": raise WebSocketErr - case "finish" if data["reason"] == "stop": + elif data["reason"] == "stop": break except WebSocketDisconnect: raise WebSocketErr @@ -144,12 +145,13 @@ async def sender(): try: message = await ws.receive_bytes() data = ormsgpack.unpackb(message) - match data["event"]: - case "audio": - yield data["audio"] - case "finish" if data["reason"] == "error": + event = data["event"] + if event == "audio": + yield data["audio"] + elif event == "finish": + if data["reason"] == "error": raise WebSocketErr - case "finish" if data["reason"] == "stop": + elif data["reason"] == "stop": break except WebSocketDisconnect: raise WebSocketErr From ba4ab45db6f463a828bbfe04daa26b8b85a6ae4a Mon Sep 17 00:00:00 2001 From: James Ding Date: Fri, 7 Nov 2025 22:05:24 -0600 Subject: [PATCH 2/5] feat: implement Fish Audio SDK v1.0.0 with dual module support Major changes: - Introduce new 'fishaudio' module with modern Python SDK architecture - Restore legacy 'fish_audio_sdk' module for backwards compatibility - Rename package to 'fish-audio-sdk' (from 'fish-audio') - Add comprehensive test suite (unit and integration tests) - Implement real-time WebSocket streaming for TTS - Add utility functions for audio playback and file operations - Support Python 3.9-3.14 - Include examples and documentation New Features: - Sync/async client support (FishAudio/AsyncFishAudio) - TTS with streaming and real-time WebSocket support - Voice management (list, get, create, delete) - ASR (automatic speech recognition) support - Account/credits management - Event-based streaming (TextEvent, FlushEvent) - Audio utilities (play, save, stream) Breaking Changes: - New SDK uses 'fishaudio' module instead of 'fish_audio_sdk' - Legacy 'fish_audio_sdk' module maintained for compatibility Signed-off-by: James Ding --- .env.example | 1 + .github/workflows/ci.yml | 54 - .github/workflows/python.yml | 121 ++ .mcp.json | 8 + README.md | 22 +- examples/README.md | 9 + examples/getting-started/01_simple_tts.py | 55 + examples/getting-started/02_play_audio.py | 105 ++ examples/getting-started/03_check_credits.py | 105 ++ pdm.lock | 429 ------ pyproject.toml | 55 +- src/fishaudio/__init__.py | 71 + src/fishaudio/_version.py | 3 + src/fishaudio/client.py | 202 +++ src/fishaudio/compatibility/__init__.py | 0 src/fishaudio/core/__init__.py | 12 + src/fishaudio/core/client_wrapper.py | 252 ++++ src/fishaudio/core/omit.py | 30 + src/fishaudio/core/request_options.py | 36 + src/fishaudio/exceptions.py | 73 + src/fishaudio/resources/__init__.py | 19 + src/fishaudio/resources/account.py | 132 ++ src/fishaudio/resources/asr.py | 132 ++ src/fishaudio/resources/realtime.py | 84 ++ src/fishaudio/resources/tts.py | 328 ++++ src/fishaudio/resources/voices.py | 425 ++++++ src/fishaudio/types/__init__.py | 54 + src/fishaudio/types/account.py | 35 + src/fishaudio/types/asr.py | 21 + src/fishaudio/types/shared.py | 34 + src/fishaudio/types/tts.py | 100 ++ src/fishaudio/types/voices.py | 55 + src/fishaudio/utils/__init__.py | 11 + src/fishaudio/utils/play.py | 102 ++ src/fishaudio/utils/save.py | 35 + src/fishaudio/utils/stream.py | 79 + tests/__init__.py | 3 - tests/conftest.py | 22 - tests/hello.mp3 | Bin 18390 -> 0 bytes tests/integration/__init__.py | 0 tests/integration/conftest.py | 35 + tests/integration/test_account_integration.py | 57 + tests/integration/test_asr_integration.py | 73 + tests/integration/test_tts_integration.py | 109 ++ tests/integration/test_voices_integration.py | 96 ++ tests/test_apis.py | 87 -- tests/test_websocket.py | 31 - tests/unit/__init__.py | 0 tests/unit/conftest.py | 108 ++ tests/unit/test_account.py | 211 +++ tests/unit/test_client.py | 94 ++ tests/unit/test_core.py | 106 ++ tests/unit/test_tts.py | 354 +++++ tests/unit/test_tts_realtime.py | 375 +++++ tests/unit/test_types.py | 98 ++ tests/unit/test_utils.py | 142 ++ tests/unit/test_voices.py | 112 ++ uv.lock | 1336 +++++++++++++++++ 58 files changed, 6099 insertions(+), 639 deletions(-) create mode 100644 .env.example delete mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/python.yml create mode 100644 .mcp.json create mode 100644 examples/README.md create mode 100644 examples/getting-started/01_simple_tts.py create mode 100644 examples/getting-started/02_play_audio.py create mode 100644 examples/getting-started/03_check_credits.py delete mode 100644 pdm.lock create mode 100644 src/fishaudio/__init__.py create mode 100644 src/fishaudio/_version.py create mode 100644 src/fishaudio/client.py create mode 100644 src/fishaudio/compatibility/__init__.py create mode 100644 src/fishaudio/core/__init__.py create mode 100644 src/fishaudio/core/client_wrapper.py create mode 100644 src/fishaudio/core/omit.py create mode 100644 src/fishaudio/core/request_options.py create mode 100644 src/fishaudio/exceptions.py create mode 100644 src/fishaudio/resources/__init__.py create mode 100644 src/fishaudio/resources/account.py create mode 100644 src/fishaudio/resources/asr.py create mode 100644 src/fishaudio/resources/realtime.py create mode 100644 src/fishaudio/resources/tts.py create mode 100644 src/fishaudio/resources/voices.py create mode 100644 src/fishaudio/types/__init__.py create mode 100644 src/fishaudio/types/account.py create mode 100644 src/fishaudio/types/asr.py create mode 100644 src/fishaudio/types/shared.py create mode 100644 src/fishaudio/types/tts.py create mode 100644 src/fishaudio/types/voices.py create mode 100644 src/fishaudio/utils/__init__.py create mode 100644 src/fishaudio/utils/play.py create mode 100644 src/fishaudio/utils/save.py create mode 100644 src/fishaudio/utils/stream.py delete mode 100644 tests/__init__.py delete mode 100644 tests/conftest.py delete mode 100644 tests/hello.mp3 create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/conftest.py create mode 100644 tests/integration/test_account_integration.py create mode 100644 tests/integration/test_asr_integration.py create mode 100644 tests/integration/test_tts_integration.py create mode 100644 tests/integration/test_voices_integration.py delete mode 100644 tests/test_apis.py delete mode 100644 tests/test_websocket.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/conftest.py create mode 100644 tests/unit/test_account.py create mode 100644 tests/unit/test_client.py create mode 100644 tests/unit/test_core.py create mode 100644 tests/unit/test_tts.py create mode 100644 tests/unit/test_tts_realtime.py create mode 100644 tests/unit/test_types.py create mode 100644 tests/unit/test_utils.py create mode 100644 tests/unit/test_voices.py create mode 100644 uv.lock diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9ab9793 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +FISH_AUDIO_API_KEY= \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 1716517..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: CI/CD - -on: ["push", "pull_request"] - -jobs: - tests: - name: "Python ${{ matrix.python-version }} ${{ matrix.os }}" - runs-on: "${{ matrix.os }}" - strategy: - matrix: - python-version: ["3.10", "3.11", "3.12"] - os: [ubuntu-latest] - steps: - - uses: actions/checkout@v4 - - - uses: pdm-project/setup-pdm@v4 - name: Setup Python and PDM - with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - pdm sync -v -dG dev - - - name: Tests - run: pdm run pytest tests -o log_cli=true -o log_cli_level=DEBUG - env: - APIKEY: ${{ secrets.APIKEY }} - - publish: - needs: tests - if: startsWith(github.ref, 'refs/tags/') - - name: Publish to PyPI - runs-on: ubuntu-latest - environment: release - permissions: - id-token: write - steps: - - uses: actions/checkout@v4 - - - uses: pdm-project/setup-pdm@v4 - name: Setup Python and PDM - with: - python-version: "3.10" - architecture: x64 - version: 2.10.4 - - - name: Build package distributions - run: | - pdm build - - - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..3f404f4 --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,121 @@ +name: Python + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + lint: + name: Lint & Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Check formatting + run: uv run ruff format --check . + + - name: Lint code + run: uv run ruff check . + + type-check: + name: Type Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install uv + uses: astral-sh/setup-uv@v4 + - name: Install dependencies + run: uv sync + - name: Run mypy + run: uv run mypy src/fishaudio --ignore-missing-imports + + test: + name: Test Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.x"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install uv + uses: astral-sh/setup-uv@v4 + - name: Install dependencies + run: uv sync + - name: Run tests with coverage + run: uv run pytest tests/unit/ -v --cov=src/fishaudio --cov-report=xml --cov-report=term + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + if: matrix.python-version == '3.x' + with: + files: ./coverage.xml + fail_ci_if_error: false + + integration: + name: Integration Tests + runs-on: ubuntu-latest + needs: [test] + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Install dependencies + run: uv sync + + - name: Run integration tests + run: uv run pytest tests/integration/ -v + env: + FISH_AUDIO_API_KEY: ${{ secrets.FISH_AUDIO_API_KEY }} + + build: + name: Build Package + runs-on: ubuntu-latest + needs: [lint, type-check, test, integration] + steps: + - uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v4 + - name: Build package + run: uv build + - name: Check package + run: uv run python -c "import fishaudio; print(f'fishaudio v{fishaudio.__version__}')" + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + name: Publish to PyPI + runs-on: ubuntu-latest + needs: [lint, type-check, test, integration, build] + if: startsWith(github.ref, 'refs/tags/') + environment: pypi + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v4 + - name: Build package + run: uv build + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 \ No newline at end of file diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..06c32dc --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "fish-audio": { + "type": "http", + "url": "https://docs.fish.audio/mcp" + } + } +} \ No newline at end of file diff --git a/README.md b/README.md index ae96ab5..1800e9c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,27 @@ pip install fish-audio-sdk ## Usage -Initialize a `Session` to use APIs. All APIs have synchronous and asynchronous versions. If you want to use the asynchronous version of the API, you only need to rewrite the original `session.api_call(...)` to `session.api_call.awaitable(...)`. +### New SDK (Recommended) + +The new SDK uses the `fishaudio` module: + +```python +from fishaudio import FishAudio + +client = FishAudio(api_key="your_api_key") +``` + +You can customize the base URL: + +```python +from fishaudio import FishAudio + +client = FishAudio(api_key="your_api_key", base_url="https://your-proxy-domain") +``` + +### Legacy SDK + +The legacy SDK uses the `fish_audio_sdk` module. Initialize a `Session` to use APIs. All APIs have synchronous and asynchronous versions. If you want to use the asynchronous version of the API, you only need to rewrite the original `session.api_call(...)` to `session.api_call.awaitable(...)`. ```python from fish_audio_sdk import Session diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..cc7510d --- /dev/null +++ b/examples/README.md @@ -0,0 +1,9 @@ +# Fish Audio SDK Examples + +Example scripts demonstrating how to use the Fish Audio Python SDK. + +```bash +# Install and setup +pip install fishaudio +export FISH_AUDIO_API_KEY="your_api_key" +``` \ No newline at end of file diff --git a/examples/getting-started/01_simple_tts.py b/examples/getting-started/01_simple_tts.py new file mode 100644 index 0000000..f312848 --- /dev/null +++ b/examples/getting-started/01_simple_tts.py @@ -0,0 +1,55 @@ +""" +Simple Text-to-Speech Example + +This example demonstrates the most basic usage of the Fish Audio SDK: +- Initialize the client +- Convert text to speech +- Save the audio to an MP3 file + +Requirements: + pip install fishaudio + +Environment Setup: + export FISH_AUDIO_API_KEY="your_api_key_here" + # Or pass api_key directly to the client + +Expected Output: + - Creates "output.mp3" in the current directory + - Audio file contains the spoken text +""" + +import os +from fishaudio import FishAudio +from fishaudio.utils import save + + +def main(): + # Initialize the client with your API key + # Option 1: Use environment variable FISH_AUDIO_API_KEY + # Option 2: Pass api_key directly: FishAudio(api_key="your_key") + client = FishAudio() + + # The text you want to convert to speech + text = "Hello! This is a simple text-to-speech example using the Fish Audio SDK." + + print(f"Converting text to speech: '{text}'") + + # Convert text to speech + # This returns an iterator of audio bytes + audio = client.tts.convert(text=text) + + # Save the audio to a file + output_file = "output.mp3" + save(audio, output_file) + + print(f"✓ Audio saved to {output_file}") + print(f" File size: {os.path.getsize(output_file) / 1024:.2f} KB") + + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(f"Error: {e}") + print("\nMake sure you have set your API key:") + print(" export FISH_AUDIO_API_KEY='your_api_key'") diff --git a/examples/getting-started/02_play_audio.py b/examples/getting-started/02_play_audio.py new file mode 100644 index 0000000..5b62750 --- /dev/null +++ b/examples/getting-started/02_play_audio.py @@ -0,0 +1,105 @@ +""" +Play Audio Example + +This example demonstrates how to play generated audio immediately: +- Generate text-to-speech audio +- Play audio using the play() utility +- Different playback methods (ffmpeg, sounddevice) + +Requirements: + pip install fishaudio + + # For audio playback (choose one): + # Option 1 (recommended): Install ffmpeg + # macOS: brew install ffmpeg + # Ubuntu: sudo apt install ffmpeg + # Windows: Download from ffmpeg.org + + # Option 2: Use sounddevice (Python-only) + # pip install sounddevice soundfile + +Environment Setup: + export FISH_AUDIO_API_KEY="your_api_key_here" + +Expected Output: + - Plays the generated audio through your speakers + - No file is saved (unless you uncomment the save section) +""" + +from fishaudio import FishAudio +from fishaudio.utils import play + + +def main(): + # Initialize the client + client = FishAudio() + + # Text to convert to speech + text = "Welcome to Fish Audio! This audio will play immediately after generation." + + print(f"Generating speech: '{text}'") + print("Please ensure your speakers are on...\n") + + # Generate the audio + audio = client.tts.convert(text=text) + + # Play the audio immediately + # By default, this uses ffplay (from ffmpeg) if available, + # otherwise falls back to sounddevice + print("▶ Playing audio...") + play(audio) + print("✓ Playback complete!") + + # Optional: Save the audio to a file as well + # Uncomment the following lines if you want to save it: + # print("\nSaving audio to file...") + # audio = client.tts.convert(text=text) # Regenerate since audio was consumed + # save(audio, "playback_example.mp3") + # print("✓ Saved to playback_example.mp3") + + +def demo_playback_methods(): + """ + Demonstrate different playback methods. + + Note: The play() function automatically chooses the best available method, + but you can force specific methods by modifying the code. + """ + client = FishAudio() + text = "This is a demonstration of different playback methods." + + # Method 1: Default (uses ffmpeg if available) + print("Method 1: Default playback") + audio = client.tts.convert(text=text) + play(audio, use_ffmpeg=True) + + # Method 2: Using sounddevice (requires pip install sounddevice soundfile) + print("\nMethod 2: Sounddevice playback") + audio = client.tts.convert(text=text) + play(audio, use_ffmpeg=False) + + # Method 3: Jupyter notebook mode (for .ipynb files) + # This returns an IPython.display.Audio object + # Uncomment if running in a notebook: + # audio = client.tts.convert(text=text) + # play(audio, notebook=True) + + +if __name__ == "__main__": + try: + main() + + # Uncomment to try different playback methods: + # print("\n" + "="*50) + # print("Testing different playback methods...") + # print("="*50 + "\n") + # demo_playback_methods() + + except Exception as e: + print(f"Error: {e}") + print("\nTroubleshooting:") + print("1. Make sure your API key is set: export FISH_AUDIO_API_KEY='your_key'") + print("2. Install ffmpeg for audio playback:") + print(" - macOS: brew install ffmpeg") + print(" - Ubuntu: sudo apt install ffmpeg") + print("3. Or install sounddevice: pip install sounddevice soundfile") diff --git a/examples/getting-started/03_check_credits.py b/examples/getting-started/03_check_credits.py new file mode 100644 index 0000000..68fc721 --- /dev/null +++ b/examples/getting-started/03_check_credits.py @@ -0,0 +1,105 @@ +""" +Check Account Credits Example + +This example demonstrates how to: +- Check your API account balance and credits +- View prepaid package information + +This is useful for: +- Verifying your API setup +- Monitoring usage and remaining credits + +Requirements: + pip install fishaudio + +Environment Setup: + export FISH_AUDIO_API_KEY="your_api_key_here" + +Expected Output: + - Displays account credit balance + - Shows prepaid package details (if any) +""" + +from fishaudio import FishAudio + + +def main(): + # Initialize the client + client = FishAudio() + + print("=" * 60) + print("Fish Audio - Account Information") + print("=" * 60) + + # Check account credits + print("\nAccount Credits:") + print("-" * 60) + try: + credits = client.account.get_credits() + print(f" Balance: {float(credits.credit):,.2f} credits") + + # Note: Credits are consumed based on usage + # Check the Fish Audio pricing page for current rates + + except Exception as e: + print(f" Error fetching credits: {e}") + + # Check prepaid package information + print("\nPrepaid Package:") + print("-" * 60) + try: + package = client.account.get_package() + if package: + print(" Package details available") + # Display package information based on the response structure + print(f" {package}") + else: + print(" No active prepaid package") + + except Exception as e: + print(f" Error fetching package: {e}") + + print("\n" + "=" * 60) + print("Account information retrieved successfully") + print("=" * 60) + + +def check_api_setup(): + """ + Quick check to verify your API is set up correctly. + Returns True if everything is working, False otherwise. + """ + try: + client = FishAudio() + + # Try to fetch credits as a simple API test + credits = client.account.get_credits() + + print("API setup is correct!") + print(f" Your current balance: {float(credits.credit):,.2f} credits") + return True + + except Exception as e: + print("API setup failed!") + print(f" Error: {e}") + print("\nPlease check:") + print(" 1. Your API key is correct") + print(" 2. Environment variable is set: export FISH_AUDIO_API_KEY='your_key'") + print(" 3. You have an active internet connection") + return False + + +if __name__ == "__main__": + try: + main() + + # Uncomment to run the quick setup check: + # print("\n\nRunning quick API setup check...") + # check_api_setup() + + except Exception as e: + print(f"\nError: {e}") + print("\nMake sure you have set your API key:") + print(" export FISH_AUDIO_API_KEY='your_api_key'") + print("\nOr pass it directly when creating the client:") + print(" client = FishAudio(api_key='your_api_key')") diff --git a/pdm.lock b/pdm.lock deleted file mode 100644 index 29905f3..0000000 --- a/pdm.lock +++ /dev/null @@ -1,429 +0,0 @@ -# This file is @generated by PDM. -# It is not intended for manual editing. - -[metadata] -groups = ["default", "dev"] -strategy = ["cross_platform", "inherit_metadata"] -lock_version = "4.5.0" -content_hash = "sha256:5fd761c1f74135159e4442da8af5cca85cfafe9eedff8e88c0145f7d7d41c467" - -[[metadata.targets]] -requires_python = ">=3.10" - -[[package]] -name = "annotated-types" -version = "0.7.0" -requires_python = ">=3.8" -summary = "Reusable constraint types to use with typing.Annotated" -groups = ["default"] -dependencies = [ - "typing-extensions>=4.0.0; python_version < \"3.9\"", -] -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "anyio" -version = "4.6.2.post1" -requires_python = ">=3.9" -summary = "High level compatibility layer for multiple asynchronous event loop implementations" -groups = ["default"] -dependencies = [ - "exceptiongroup>=1.0.2; python_version < \"3.11\"", - "idna>=2.8", - "sniffio>=1.1", - "typing-extensions>=4.1; python_version < \"3.11\"", -] -files = [ - {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, - {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, -] - -[[package]] -name = "certifi" -version = "2024.8.30" -requires_python = ">=3.6" -summary = "Python package for providing Mozilla's CA Bundle." -groups = ["default"] -files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -summary = "Cross-platform colored terminal text." -groups = ["dev"] -marker = "sys_platform == \"win32\"" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.2.2" -requires_python = ">=3.7" -summary = "Backport of PEP 654 (exception groups)" -groups = ["default", "dev"] -marker = "python_version < \"3.11\"" -files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, -] - -[[package]] -name = "h11" -version = "0.14.0" -requires_python = ">=3.7" -summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -groups = ["default"] -dependencies = [ - "typing-extensions; python_version < \"3.8\"", -] -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] - -[[package]] -name = "httpcore" -version = "1.0.7" -requires_python = ">=3.8" -summary = "A minimal low-level HTTP client." -groups = ["default"] -dependencies = [ - "certifi", - "h11<0.15,>=0.13", -] -files = [ - {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, - {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, -] - -[[package]] -name = "httpx" -version = "0.28.0" -requires_python = ">=3.8" -summary = "The next generation HTTP client." -groups = ["default"] -dependencies = [ - "anyio", - "certifi", - "httpcore==1.*", - "idna", -] -files = [ - {file = "httpx-0.28.0-py3-none-any.whl", hash = "sha256:dc0b419a0cfeb6e8b34e85167c0da2671206f5095f1baa9663d23bcfd6b535fc"}, - {file = "httpx-0.28.0.tar.gz", hash = "sha256:0858d3bab51ba7e386637f22a61d8ccddaeec5f3fe4209da3a6168dbb91573e0"}, -] - -[[package]] -name = "httpx-ws" -version = "0.6.2" -requires_python = ">=3.8" -summary = "WebSockets support for HTTPX" -groups = ["default"] -dependencies = [ - "anyio>=4", - "httpcore>=1.0.4", - "httpx>=0.23.1", - "wsproto", -] -files = [ - {file = "httpx_ws-0.6.2-py3-none-any.whl", hash = "sha256:24f87427acb757ada200aeab016cc429fa0bc71b0730429c37634867194e305c"}, - {file = "httpx_ws-0.6.2.tar.gz", hash = "sha256:b07446b9067a30f1012fa9851fdfd14207012cd657c485565884f90553d0854c"}, -] - -[[package]] -name = "idna" -version = "3.10" -requires_python = ">=3.6" -summary = "Internationalized Domain Names in Applications (IDNA)" -groups = ["default"] -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[[package]] -name = "iniconfig" -version = "2.0.0" -requires_python = ">=3.7" -summary = "brain-dead simple config-ini parsing" -groups = ["dev"] -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "ormsgpack" -version = "1.6.0" -requires_python = ">=3.8" -summary = "Fast, correct Python msgpack library supporting dataclasses, datetimes, and numpy" -groups = ["default"] -files = [ - {file = "ormsgpack-1.6.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:613e851edde1dc3b94e84e9d8046b9c603fce1e5bf40f3ebe129a81f9a31d4b9"}, - {file = "ormsgpack-1.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8e2b858cc378d2c46161bebfe6232de1e158eb2e7dfdf07172fe183bf8a9333"}, - {file = "ormsgpack-1.6.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4003867208e3b9b9471c0a4bbf68479dcde69137ca1c5860bd7236410bf65024"}, - {file = "ormsgpack-1.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fd584dd40d479a60cbae0ca59792b82144f35e355c363771566bb7d337ba8a9"}, - {file = "ormsgpack-1.6.0-cp310-none-win_amd64.whl", hash = "sha256:e8b2cab0ddb98b1b26f01da552a76138299ecf29f8a04fe34ac5b686b9db02d0"}, - {file = "ormsgpack-1.6.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6f08a3c448e9ca65c7b5af600b3d795ffbee0dbd5b4defc6e12f89d2d56c57b4"}, - {file = "ormsgpack-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:978330de00c1d8c20b62ae38f20b85312abefb6e46342617ba47d9a01b70d727"}, - {file = "ormsgpack-1.6.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6b747ae7f2fa8e54cf7033a9907cc2dfeee70184148ce57d1c8dff3ed2d3692"}, - {file = "ormsgpack-1.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:243b330577c8158a89fec4abce40254cf907637ea7e604bf866b3e39b3c5a819"}, - {file = "ormsgpack-1.6.0-cp311-none-win_amd64.whl", hash = "sha256:75e1837f569fb6ae2f0c6415e983a0300b633b69a5d3480f34fb0a61f1662dbf"}, - {file = "ormsgpack-1.6.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:af017e8dcf8dad7b36c018e2ec410eb2bedd123aa81beabc808b92f00a76c285"}, - {file = "ormsgpack-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c42c72050499d57d620ae81bf3b47637fb66b1c90a8abcabc0fbf9b193fdabc"}, - {file = "ormsgpack-1.6.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:472a6e9207caad84ad3bddd6c6be54e99fdb7ef667cedc1074f26386aebd6580"}, - {file = "ormsgpack-1.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33075e026d8536c6953d0485c34970f62dcc309d662f5913147bfbebe8b1d59f"}, - {file = "ormsgpack-1.6.0-cp312-none-win_amd64.whl", hash = "sha256:f28b4a0cd7f64b92e4a0de6acbd7d03d20970fdded9e752fa6dedb3b95be86a3"}, - {file = "ormsgpack-1.6.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:113316087520ec70f7c0f36450181ca24d575d4225116545c9038d6e8e576f51"}, - {file = "ormsgpack-1.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a72971eeccfc2e720491abae3fc7f368886d81b7ef3f965eb2adb7922454243"}, - {file = "ormsgpack-1.6.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e17d9b9b0a61ef3f26a66c85d9b3140a2111e4f2c5b4daf04185eac872b39303"}, - {file = "ormsgpack-1.6.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9219d871d05370443129679241a6c4bd579ff351a2c7675ff2bb192485f543e6"}, - {file = "ormsgpack-1.6.0-cp313-none-win_amd64.whl", hash = "sha256:a0ee8ae2981548df90a4ff0a8cde710cd289a19980e74a338d6c958a4e6c1c84"}, - {file = "ormsgpack-1.6.0.tar.gz", hash = "sha256:0c9612147f3c406b56eba6a576948057ada711bda0831f192afd46be6e5dd91e"}, -] - -[[package]] -name = "packaging" -version = "24.2" -requires_python = ">=3.8" -summary = "Core utilities for Python packages" -groups = ["dev"] -files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, -] - -[[package]] -name = "pluggy" -version = "1.5.0" -requires_python = ">=3.8" -summary = "plugin and hook calling mechanisms for python" -groups = ["dev"] -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[[package]] -name = "pydantic" -version = "2.10.3" -requires_python = ">=3.8" -summary = "Data validation using Python type hints" -groups = ["default"] -dependencies = [ - "annotated-types>=0.6.0", - "pydantic-core==2.27.1", - "typing-extensions>=4.12.2", -] -files = [ - {file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"}, - {file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"}, -] - -[[package]] -name = "pydantic-core" -version = "2.27.1" -requires_python = ">=3.8" -summary = "Core functionality for Pydantic validation and serialization" -groups = ["default"] -dependencies = [ - "typing-extensions!=4.7.0,>=4.6.0", -] -files = [ - {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, - {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"}, - {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"}, - {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"}, - {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"}, - {file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"}, - {file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"}, - {file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"}, - {file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"}, - {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"}, - {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"}, - {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"}, - {file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"}, - {file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"}, - {file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"}, - {file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"}, - {file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"}, - {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"}, - {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"}, - {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"}, - {file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"}, - {file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"}, - {file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"}, - {file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"}, - {file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"}, - {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"}, - {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"}, - {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"}, - {file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"}, - {file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"}, - {file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"}, - {file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"}, -] - -[[package]] -name = "pytest" -version = "8.3.4" -requires_python = ">=3.8" -summary = "pytest: simple powerful testing with Python" -groups = ["dev"] -dependencies = [ - "colorama; sys_platform == \"win32\"", - "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", - "iniconfig", - "packaging", - "pluggy<2,>=1.5", - "tomli>=1; python_version < \"3.11\"", -] -files = [ - {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, - {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, -] - -[[package]] -name = "pytest-asyncio" -version = "0.24.0" -requires_python = ">=3.8" -summary = "Pytest support for asyncio" -groups = ["dev"] -dependencies = [ - "pytest<9,>=8.2", -] -files = [ - {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, - {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, -] - -[[package]] -name = "python-dotenv" -version = "1.0.1" -requires_python = ">=3.8" -summary = "Read key-value pairs from a .env file and set them as environment variables" -groups = ["dev"] -files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -requires_python = ">=3.7" -summary = "Sniff out which async library your code is running under" -groups = ["default"] -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "tomli" -version = "2.2.1" -requires_python = ">=3.8" -summary = "A lil' TOML parser" -groups = ["dev"] -marker = "python_version < \"3.11\"" -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -requires_python = ">=3.8" -summary = "Backported and Experimental Type Hints for Python 3.8+" -groups = ["default"] -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - -[[package]] -name = "wsproto" -version = "1.2.0" -requires_python = ">=3.7.0" -summary = "WebSockets state-machine based protocol implementation" -groups = ["default"] -dependencies = [ - "h11<1,>=0.9.0", -] -files = [ - {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, - {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, -] diff --git a/pyproject.toml b/pyproject.toml index 6669198..86e9bca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,10 @@ [project] name = "fish-audio-sdk" -version = "2025.06.03" -description = "fish.audio platform api sdk" +version = "1.0.0" +description = "Python SDK for Fish Audio text-to-speech and voice cloning API" authors = [ {name = "abersheeran", email = "me@abersheeran.com"}, + {name = "James Ding", email = "james@fish.audio"}, ] dependencies = [ "httpx>=0.27.2", @@ -11,23 +12,53 @@ dependencies = [ "pydantic>=2.9.1", "httpx-ws>=0.6.2", ] -requires-python = ">=3.10" +requires-python = ">=3.9" readme = "README.md" license = {text = "MIT"} +keywords = ["fish-audio", "tts", "text-to-speech", "voice-cloning", "ai", "speech-synthesis"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Multimedia :: Sound/Audio :: Speech", + "Topic :: Software Development :: Libraries :: Python Modules", +] + +[project.urls] +Homepage = "https://fish.audio" +Documentation = "https://docs.fish.audio" +Repository = "https://github.com/fishaudio/fish-audio-python" +Issues = "https://github.com/fishaudio/fish-audio-python/issues" + +[project.optional-dependencies] +utils = [ + "sounddevice>=0.4.6", + "soundfile>=0.12.1", +] [build-system] -requires = ["pdm-backend"] -build-backend = "pdm.backend" +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/fishaudio", "src/fish_audio_sdk"] -[tool.pdm] -package-type = "library" +[tool.pytest.ini_options] +asyncio_mode = "auto" -[tool.pdm.dev-dependencies] +[dependency-groups] dev = [ - "pytest>=8.3.3", + "mypy>=1.14.1", + "pytest>=8.3.5", "pytest-asyncio>=0.24.0", + "pytest-cov>=5.0.0", "python-dotenv>=1.0.1", + "ruff>=0.14.3", ] - -[tool.pytest.ini_options] -asyncio_mode = "auto" diff --git a/src/fishaudio/__init__.py b/src/fishaudio/__init__.py new file mode 100644 index 0000000..bf33f15 --- /dev/null +++ b/src/fishaudio/__init__.py @@ -0,0 +1,71 @@ +""" +Fish Audio Python SDK. + +A professional Python SDK for Fish Audio's text-to-speech and voice cloning API. + +Example: + ```python + from fishaudio import FishAudio, play, save + + # Initialize client + client = FishAudio(api_key="your_api_key") + + # Generate speech + audio = client.tts.convert(text="Hello, world!") + + # Play it + play(audio) + + # Or save it + save(audio, "output.mp3") + + # List available voices + voices = client.voices.list() + for voice in voices.items: + print(f"{voice.title}: {voice.id}") + ``` +""" + +from ._version import __version__ +from .client import AsyncFishAudio, FishAudio +from .exceptions import ( + APIError, + AuthenticationError, + DependencyError, + FishAudioError, + NotFoundError, + PermissionError, + RateLimitError, + ServerError, + ValidationError, + WebSocketError, +) +from .types import FlushEvent, TextEvent +from .utils import play, save, stream + +# Main exports +__all__ = [ + # Main clients + "FishAudio", + "AsyncFishAudio", + # Utilities + "play", + "save", + "stream", + # Types + "FlushEvent", + "TextEvent", + # Exceptions + "APIError", + "AuthenticationError", + "DependencyError", + "FishAudioError", + "NotFoundError", + "PermissionError", + "RateLimitError", + "ServerError", + "ValidationError", + "WebSocketError", + # Version + "__version__", +] diff --git a/src/fishaudio/_version.py b/src/fishaudio/_version.py new file mode 100644 index 0000000..01ad014 --- /dev/null +++ b/src/fishaudio/_version.py @@ -0,0 +1,3 @@ +"""Version information.""" + +__version__ = "1.0.0" diff --git a/src/fishaudio/client.py b/src/fishaudio/client.py new file mode 100644 index 0000000..5a914cf --- /dev/null +++ b/src/fishaudio/client.py @@ -0,0 +1,202 @@ +"""Main Fish Audio client classes.""" + +from typing import Optional + +import httpx + +from .core import AsyncClientWrapper, ClientWrapper +from .resources import ( + ASRClient, + AccountClient, + AsyncAccountClient, + AsyncASRClient, + AsyncTTSClient, + AsyncVoicesClient, + TTSClient, + VoicesClient, +) + + +class FishAudio: + """ + Synchronous Fish Audio API client. + + Example: + ```python + from fishaudio import FishAudio + + client = FishAudio(api_key="your_api_key") + + # Generate speech + audio = client.tts.convert(text="Hello world") + with open("output.mp3", "wb") as f: + for chunk in audio: + f.write(chunk) + + # List voices + voices = client.voices.list(page_size=20) + print(f"Found {voices.total} voices") + ``` + """ + + def __init__( + self, + *, + api_key: Optional[str] = None, + base_url: str = "https://api.fish.audio", + timeout: float = 240.0, + httpx_client: Optional[httpx.Client] = None, + ): + """ + Initialize Fish Audio client. + + Args: + api_key: API key (can also use FISH_AUDIO_API_KEY env var) + base_url: API base URL + timeout: Request timeout in seconds + httpx_client: Optional custom HTTP client + """ + self._client_wrapper = ClientWrapper( + api_key=api_key, + base_url=base_url, + timeout=timeout, + httpx_client=httpx_client, + ) + + # Lazy-loaded namespace clients + self._tts: Optional[TTSClient] = None + self._asr: Optional[ASRClient] = None + self._voices: Optional[VoicesClient] = None + self._account: Optional[AccountClient] = None + + @property + def tts(self) -> TTSClient: + """Access TTS (text-to-speech) operations.""" + if self._tts is None: + self._tts = TTSClient(self._client_wrapper) + return self._tts + + @property + def asr(self) -> ASRClient: + """Access ASR (speech-to-text) operations.""" + if self._asr is None: + self._asr = ASRClient(self._client_wrapper) + return self._asr + + @property + def voices(self) -> VoicesClient: + """Access voice management operations.""" + if self._voices is None: + self._voices = VoicesClient(self._client_wrapper) + return self._voices + + @property + def account(self) -> AccountClient: + """Access account/billing operations.""" + if self._account is None: + self._account = AccountClient(self._client_wrapper) + return self._account + + def close(self) -> None: + """Close the HTTP client.""" + self._client_wrapper.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + +class AsyncFishAudio: + """ + Asynchronous Fish Audio API client. + + Example: + ```python + from fishaudio import AsyncFishAudio + + async def main(): + client = AsyncFishAudio(api_key="your_api_key") + + # Generate speech + audio = client.tts.convert(text="Hello world") + async with aiofiles.open("output.mp3", "wb") as f: + async for chunk in audio: + await f.write(chunk) + + # List voices + voices = await client.voices.list(page_size=20) + print(f"Found {voices.total} voices") + + asyncio.run(main()) + ``` + """ + + def __init__( + self, + *, + api_key: Optional[str] = None, + base_url: str = "https://api.fish.audio", + timeout: float = 240.0, + httpx_client: Optional[httpx.AsyncClient] = None, + ): + """ + Initialize async Fish Audio client. + + Args: + api_key: API key (can also use FISH_AUDIO_API_KEY env var) + base_url: API base URL + timeout: Request timeout in seconds + httpx_client: Optional custom async HTTP client + """ + self._client_wrapper = AsyncClientWrapper( + api_key=api_key, + base_url=base_url, + timeout=timeout, + httpx_client=httpx_client, + ) + + # Lazy-loaded namespace clients + self._tts: Optional[AsyncTTSClient] = None + self._asr: Optional[AsyncASRClient] = None + self._voices: Optional[AsyncVoicesClient] = None + self._account: Optional[AsyncAccountClient] = None + + @property + def tts(self) -> AsyncTTSClient: + """Access TTS (text-to-speech) operations.""" + if self._tts is None: + self._tts = AsyncTTSClient(self._client_wrapper) + return self._tts + + @property + def asr(self) -> AsyncASRClient: + """Access ASR (speech-to-text) operations.""" + if self._asr is None: + self._asr = AsyncASRClient(self._client_wrapper) + return self._asr + + @property + def voices(self) -> AsyncVoicesClient: + """Access voice management operations.""" + if self._voices is None: + self._voices = AsyncVoicesClient(self._client_wrapper) + return self._voices + + @property + def account(self) -> AsyncAccountClient: + """Access account/billing operations.""" + if self._account is None: + self._account = AsyncAccountClient(self._client_wrapper) + return self._account + + async def close(self) -> None: + """Close the HTTP client.""" + await self._client_wrapper.close() + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.close() diff --git a/src/fishaudio/compatibility/__init__.py b/src/fishaudio/compatibility/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fishaudio/core/__init__.py b/src/fishaudio/core/__init__.py new file mode 100644 index 0000000..633f95d --- /dev/null +++ b/src/fishaudio/core/__init__.py @@ -0,0 +1,12 @@ +"""Core infrastructure for the Fish Audio SDK.""" + +from .client_wrapper import AsyncClientWrapper, ClientWrapper +from .omit import OMIT +from .request_options import RequestOptions + +__all__ = [ + "AsyncClientWrapper", + "ClientWrapper", + "OMIT", + "RequestOptions", +] diff --git a/src/fishaudio/core/client_wrapper.py b/src/fishaudio/core/client_wrapper.py new file mode 100644 index 0000000..7fa78c9 --- /dev/null +++ b/src/fishaudio/core/client_wrapper.py @@ -0,0 +1,252 @@ +"""HTTP client wrapper for managing requests and authentication.""" + +import os +from json import JSONDecodeError +from typing import Any, Dict, Optional + +import httpx + +from .._version import __version__ +from ..exceptions import ( + APIError, + AuthenticationError, + NotFoundError, + PermissionError, + RateLimitError, + ServerError, +) +from .request_options import RequestOptions + + +def _raise_for_status(response: httpx.Response) -> None: + """Raise appropriate exception based on status code.""" + status = response.status_code + + # Try to extract error message from response + try: + error_data = response.json() + message = error_data.get("message") or error_data.get("detail") or response.text + except JSONDecodeError: + message = response.text or httpx.codes.get_reason_phrase(status) + + # Raise specific exception based on status code + if status == 401: + raise AuthenticationError(status, message, response.text) + elif status == 403: + raise PermissionError(status, message, response.text) + elif status == 404: + raise NotFoundError(status, message, response.text) + elif status == 429: + raise RateLimitError(status, message, response.text) + elif status >= 500: + raise ServerError(status, message, response.text) + else: + raise APIError(status, message, response.text) + + +class BaseClientWrapper: + """Base wrapper with shared logic for sync/async clients.""" + + def __init__( + self, + *, + api_key: Optional[str] = None, + base_url: str = "https://api.fish.audio", + ): + self.api_key = api_key or os.getenv("FISH_AUDIO_API_KEY") + if not self.api_key: + raise ValueError( + "API key must be provided either as argument or via FISH_AUDIO_API_KEY environment variable" + ) + self.base_url = base_url + + def _get_headers( + self, additional_headers: Optional[Dict[str, str]] = None + ) -> Dict[str, str]: + """Build headers including authentication.""" + headers = { + "Authorization": f"Bearer {self.api_key}", + "User-Agent": f"fish-audio/python/{__version__}", + } + if additional_headers: + headers.update(additional_headers) + return headers + + def _prepare_request_kwargs( + self, request_options: Optional[RequestOptions], kwargs: Dict[str, Any] + ) -> None: + """Prepare request kwargs by merging headers, timeout, and query params.""" + # Merge headers + headers = self._get_headers() + if request_options and request_options.additional_headers: + headers.update(request_options.additional_headers) + kwargs["headers"] = {**headers, **kwargs.get("headers", {})} + + # Apply timeout override if provided + if request_options and request_options.timeout is not None: + kwargs["timeout"] = httpx.Timeout(request_options.timeout) + + # Add query params override if provided + if request_options and request_options.additional_query_params: + params = kwargs.get("params", {}) + if isinstance(params, dict): + params.update(request_options.additional_query_params) + kwargs["params"] = params + + +class ClientWrapper(BaseClientWrapper): + """Wrapper for httpx.Client that handles authentication and error handling.""" + + def __init__( + self, + *, + api_key: Optional[str] = None, + base_url: str = "https://api.fish.audio", + timeout: float = 240.0, + httpx_client: Optional[httpx.Client] = None, + ): + super().__init__(api_key=api_key, base_url=base_url) + + if httpx_client is not None: + self._client = httpx_client + else: + self._client = httpx.Client( + base_url=base_url, + timeout=httpx.Timeout(timeout), + headers=self._get_headers(), + ) + + def request( + self, + method: str, + path: str, + *, + request_options: Optional[RequestOptions] = None, + **kwargs: Any, + ) -> httpx.Response: + """ + Make an HTTP request with error handling. + + Args: + method: HTTP method (GET, POST, etc.) + path: API endpoint path + request_options: Optional request-level overrides + **kwargs: Additional arguments to pass to httpx.request + + Returns: + httpx.Response object + + Raises: + APIError: On non-2xx responses + """ + self._prepare_request_kwargs(request_options, kwargs) + + # Make the request + response = self._client.request(method, path, **kwargs) + + # Handle errors + if not response.is_success: + _raise_for_status(response) + + return response + + def create_websocket_client(self) -> httpx.Client: + """ + Create an httpx.Client configured for WebSocket connections. + + Returns: + Configured httpx.Client with authentication + """ + return httpx.Client( + base_url=self.base_url, + headers={"Authorization": f"Bearer {self.api_key}"}, + ) + + def close(self) -> None: + """Close the HTTP client.""" + self._client.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + +class AsyncClientWrapper(BaseClientWrapper): + """Wrapper for httpx.AsyncClient that handles authentication and error handling.""" + + def __init__( + self, + *, + api_key: Optional[str] = None, + base_url: str = "https://api.fish.audio", + timeout: float = 240.0, + httpx_client: Optional[httpx.AsyncClient] = None, + ): + super().__init__(api_key=api_key, base_url=base_url) + + if httpx_client is not None: + self._client = httpx_client + else: + self._client = httpx.AsyncClient( + base_url=base_url, + timeout=httpx.Timeout(timeout), + headers=self._get_headers(), + ) + + async def request( + self, + method: str, + path: str, + *, + request_options: Optional[RequestOptions] = None, + **kwargs: Any, + ) -> httpx.Response: + """ + Make an async HTTP request with error handling. + + Args: + method: HTTP method (GET, POST, etc.) + path: API endpoint path + request_options: Optional request-level overrides + **kwargs: Additional arguments to pass to httpx.request + + Returns: + httpx.Response object + + Raises: + APIError: On non-2xx responses + """ + self._prepare_request_kwargs(request_options, kwargs) + + # Make the request + response = await self._client.request(method, path, **kwargs) + + # Handle errors + if not response.is_success: + _raise_for_status(response) + + return response + + def create_websocket_client(self) -> httpx.AsyncClient: + """ + Create an httpx.AsyncClient configured for WebSocket connections. + + Returns: + Configured httpx.AsyncClient with authentication + """ + return httpx.AsyncClient( + base_url=self.base_url, + headers={"Authorization": f"Bearer {self.api_key}"}, + ) + + async def close(self) -> None: + """Close the HTTP client.""" + await self._client.aclose() + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.close() diff --git a/src/fishaudio/core/omit.py b/src/fishaudio/core/omit.py new file mode 100644 index 0000000..1415325 --- /dev/null +++ b/src/fishaudio/core/omit.py @@ -0,0 +1,30 @@ +"""OMIT sentinel for distinguishing None from not-provided parameters.""" + +from typing import Any + + +class _Omit: + """ + Sentinel value to distinguish between explicitly passing None vs not providing a parameter. + + Example: + def method(param: Optional[str] = OMIT): + if param is OMIT: + # Parameter not provided + pass + elif param is None: + # Explicitly set to None + pass + else: + # Has a value + pass + """ + + def __bool__(self) -> bool: + return False + + def __repr__(self) -> str: + return "OMIT" + + +OMIT: Any = _Omit() diff --git a/src/fishaudio/core/request_options.py b/src/fishaudio/core/request_options.py new file mode 100644 index 0000000..6212cce --- /dev/null +++ b/src/fishaudio/core/request_options.py @@ -0,0 +1,36 @@ +"""Request-level options for API calls.""" + +from typing import Dict, Optional + +import httpx + + +class RequestOptions: + """ + Options that can be provided on a per-request basis to override client defaults. + + Attributes: + timeout: Override the client's default timeout (in seconds) + max_retries: Override the client's default max retries + additional_headers: Additional headers to include in the request + additional_query_params: Additional query parameters to include + """ + + def __init__( + self, + *, + timeout: Optional[float] = None, + max_retries: Optional[int] = None, + additional_headers: Optional[Dict[str, str]] = None, + additional_query_params: Optional[Dict[str, str]] = None, + ): + self.timeout = timeout + self.max_retries = max_retries + self.additional_headers = additional_headers or {} + self.additional_query_params = additional_query_params or {} + + def get_timeout(self) -> Optional[httpx.Timeout]: + """Convert timeout to httpx.Timeout if set.""" + if self.timeout is not None: + return httpx.Timeout(self.timeout) + return None diff --git a/src/fishaudio/exceptions.py b/src/fishaudio/exceptions.py new file mode 100644 index 0000000..236dcee --- /dev/null +++ b/src/fishaudio/exceptions.py @@ -0,0 +1,73 @@ +"""Custom exceptions for the Fish Audio SDK.""" + +from typing import Optional + + +class FishAudioError(Exception): + """Base exception for all Fish Audio SDK errors.""" + + pass + + +class APIError(FishAudioError): + """Raised when the API returns an error response.""" + + def __init__(self, status: int, message: str, body: Optional[str] = None): + self.status = status + self.message = message + self.body = body + super().__init__(f"HTTP {status}: {message}") + + +class AuthenticationError(APIError): + """Raised when authentication fails (401).""" + + pass + + +class PermissionError(APIError): + """Raised when permission is denied (403).""" + + pass + + +class NotFoundError(APIError): + """Raised when a resource is not found (404).""" + + pass + + +class RateLimitError(APIError): + """Raised when rate limit is exceeded (429).""" + + pass + + +class ServerError(APIError): + """Raised when the server encounters an error (5xx).""" + + pass + + +class WebSocketError(FishAudioError): + """Raised when WebSocket connection or streaming fails.""" + + pass + + +class ValidationError(FishAudioError): + """Raised when request validation fails.""" + + pass + + +class DependencyError(FishAudioError): + """Raised when a required dependency is missing.""" + + def __init__(self, dependency: str, install_command: str): + self.dependency = dependency + self.install_command = install_command + super().__init__( + f"Missing required dependency: {dependency}\n" + f"Install it with: {install_command}" + ) diff --git a/src/fishaudio/resources/__init__.py b/src/fishaudio/resources/__init__.py new file mode 100644 index 0000000..f809b3b --- /dev/null +++ b/src/fishaudio/resources/__init__.py @@ -0,0 +1,19 @@ +"""Namespace resource clients.""" + +from .account import AccountClient, AsyncAccountClient +from .asr import ASRClient, AsyncASRClient +from .tts import AsyncTTSClient, TTSClient +from .voices import AsyncVoicesClient, VoicesClient + +__all__ = [ + # Sync clients + "AccountClient", + "ASRClient", + "TTSClient", + "VoicesClient", + # Async clients + "AsyncAccountClient", + "AsyncASRClient", + "AsyncTTSClient", + "AsyncVoicesClient", +] diff --git a/src/fishaudio/resources/account.py b/src/fishaudio/resources/account.py new file mode 100644 index 0000000..02beaf3 --- /dev/null +++ b/src/fishaudio/resources/account.py @@ -0,0 +1,132 @@ +"""Account namespace client for billing and credits.""" + +from typing import Optional + +from ..core import AsyncClientWrapper, ClientWrapper, RequestOptions +from ..types import Credits, Package + + +class AccountClient: + """Synchronous account operations.""" + + def __init__(self, client_wrapper: ClientWrapper): + self._client = client_wrapper + + def get_credits( + self, + *, + request_options: Optional[RequestOptions] = None, + ) -> Credits: + """ + Get API credit balance. + + Args: + request_options: Request-level overrides + + Returns: + Credits information + + Example: + ```python + client = FishAudio(api_key="...") + credits = client.account.get_credits() + print(f"Available credits: {float(credits.credit)}") + ``` + """ + response = self._client.request( + "GET", + "/wallet/self/api-credit", + request_options=request_options, + ) + return Credits.model_validate(response.json()) + + def get_package( + self, + *, + request_options: Optional[RequestOptions] = None, + ) -> Package: + """ + Get package information. + + Args: + request_options: Request-level overrides + + Returns: + Package information + + Example: + ```python + client = FishAudio(api_key="...") + package = client.account.get_package() + print(f"Balance: {package.balance}/{package.total}") + ``` + """ + response = self._client.request( + "GET", + "/wallet/self/package", + request_options=request_options, + ) + return Package.model_validate(response.json()) + + +class AsyncAccountClient: + """Asynchronous account operations.""" + + def __init__(self, client_wrapper: AsyncClientWrapper): + self._client = client_wrapper + + async def get_credits( + self, + *, + request_options: Optional[RequestOptions] = None, + ) -> Credits: + """ + Get API credit balance (async). + + Args: + request_options: Request-level overrides + + Returns: + Credits information + + Example: + ```python + client = AsyncFishAudio(api_key="...") + credits = await client.account.get_credits() + print(f"Available credits: {float(credits.credit)}") + ``` + """ + response = await self._client.request( + "GET", + "/wallet/self/api-credit", + request_options=request_options, + ) + return Credits.model_validate(response.json()) + + async def get_package( + self, + *, + request_options: Optional[RequestOptions] = None, + ) -> Package: + """ + Get package information (async). + + Args: + request_options: Request-level overrides + + Returns: + Package information + + Example: + ```python + client = AsyncFishAudio(api_key="...") + package = await client.account.get_package() + print(f"Balance: {package.balance}/{package.total}") + ``` + """ + response = await self._client.request( + "GET", + "/wallet/self/package", + request_options=request_options, + ) + return Package.model_validate(response.json()) diff --git a/src/fishaudio/resources/asr.py b/src/fishaudio/resources/asr.py new file mode 100644 index 0000000..13b7f82 --- /dev/null +++ b/src/fishaudio/resources/asr.py @@ -0,0 +1,132 @@ +"""ASR (Automatic Speech Recognition) namespace client.""" + +from typing import Optional + +import ormsgpack + +from ..core import OMIT, AsyncClientWrapper, ClientWrapper, RequestOptions +from ..types import ASRResponse + + +class ASRClient: + """Synchronous ASR operations.""" + + def __init__(self, client_wrapper: ClientWrapper): + self._client = client_wrapper + + def transcribe( + self, + *, + audio: bytes, + language: Optional[str] = OMIT, + include_timestamps: bool = True, + request_options: Optional[RequestOptions] = None, + ) -> ASRResponse: + """ + Transcribe audio to text. + + Args: + audio: Audio file bytes + language: Language code (e.g., "en", "zh"). Auto-detected if not provided. + include_timestamps: Whether to include timestamp information for segments + request_options: Request-level overrides + + Returns: + ASRResponse with transcription text, duration, and segments + + Example: + ```python + client = FishAudio(api_key="...") + + with open("audio.mp3", "rb") as f: + audio_bytes = f.read() + + result = client.asr.transcribe(audio=audio_bytes, language="en") + print(result.text) + for segment in result.segments: + print(f"{segment.start}-{segment.end}: {segment.text}") + ``` + """ + # Build request payload + payload = { + "audio": audio, + "ignore_timestamps": not include_timestamps, + } + + # Add optional fields + if language is not OMIT: + payload["language"] = language + + # Make request + response = self._client.request( + "POST", + "/v1/asr", + headers={"Content-Type": "application/msgpack"}, + content=ormsgpack.packb(payload), + request_options=request_options, + ) + + # Parse and return response + return ASRResponse.model_validate(response.json()) + + +class AsyncASRClient: + """Asynchronous ASR operations.""" + + def __init__(self, client_wrapper: AsyncClientWrapper): + self._client = client_wrapper + + async def transcribe( + self, + *, + audio: bytes, + language: Optional[str] = OMIT, + include_timestamps: bool = True, + request_options: Optional[RequestOptions] = None, + ) -> ASRResponse: + """ + Transcribe audio to text (async). + + Args: + audio: Audio file bytes + language: Language code (e.g., "en", "zh"). Auto-detected if not provided. + include_timestamps: Whether to include timestamp information for segments + request_options: Request-level overrides + + Returns: + ASRResponse with transcription text, duration, and segments + + Example: + ```python + client = AsyncFishAudio(api_key="...") + + async with aiofiles.open("audio.mp3", "rb") as f: + audio_bytes = await f.read() + + result = await client.asr.transcribe(audio=audio_bytes, language="en") + print(result.text) + for segment in result.segments: + print(f"{segment.start}-{segment.end}: {segment.text}") + ``` + """ + # Build request payload + payload = { + "audio": audio, + "ignore_timestamps": not include_timestamps, + } + + # Add optional fields + if language is not OMIT: + payload["language"] = language + + # Make request + response = await self._client.request( + "POST", + "/v1/asr", + headers={"Content-Type": "application/msgpack"}, + content=ormsgpack.packb(payload), + request_options=request_options, + ) + + # Parse and return response + return ASRResponse.model_validate(response.json()) diff --git a/src/fishaudio/resources/realtime.py b/src/fishaudio/resources/realtime.py new file mode 100644 index 0000000..2821b48 --- /dev/null +++ b/src/fishaudio/resources/realtime.py @@ -0,0 +1,84 @@ +"""Real-time WebSocket streaming helpers.""" + +from typing import Any, AsyncIterator, Dict, Iterator, Optional + +import ormsgpack +from httpx_ws import WebSocketDisconnect + +from ..exceptions import WebSocketError + + +def _process_audio_event(data: Dict[str, Any]) -> Optional[bytes]: + """ + Process a WebSocket audio event. + + Args: + data: Unpacked WebSocket message data + + Returns: + Audio bytes if audio event, None if should stop + + Raises: + WebSocketError: If finish event has error reason + """ + if data["event"] == "audio": + return data["audio"] + elif data["event"] == "finish" and data["reason"] == "error": + raise WebSocketError("WebSocket stream ended with error") + elif data["event"] == "finish" and data["reason"] == "stop": + return None # Signal to stop + return None # Ignore unknown events + + +def iter_websocket_audio(ws) -> Iterator[bytes]: + """ + Process WebSocket audio messages (sync). + + Receives messages from WebSocket, yields audio chunks, handles errors. + + Args: + ws: WebSocket connection from httpx_ws.connect_ws + + Yields: + Audio bytes + + Raises: + WebSocketError: On disconnect or error finish event + """ + while True: + try: + message = ws.receive_bytes() + data = ormsgpack.unpackb(message) + audio = _process_audio_event(data) + if audio is None: + break + yield audio + except WebSocketDisconnect: + raise WebSocketError("WebSocket disconnected unexpectedly") + + +async def aiter_websocket_audio(ws) -> AsyncIterator[bytes]: + """ + Process WebSocket audio messages (async). + + Receives messages from WebSocket, yields audio chunks, handles errors. + + Args: + ws: WebSocket connection from httpx_ws.aconnect_ws + + Yields: + Audio bytes + + Raises: + WebSocketError: On disconnect or error finish event + """ + while True: + try: + message = await ws.receive_bytes() + data = ormsgpack.unpackb(message) + audio = _process_audio_event(data) + if audio is None: + break + yield audio + except WebSocketDisconnect: + raise WebSocketError("WebSocket disconnected unexpectedly") diff --git a/src/fishaudio/resources/tts.py b/src/fishaudio/resources/tts.py new file mode 100644 index 0000000..73cb7f5 --- /dev/null +++ b/src/fishaudio/resources/tts.py @@ -0,0 +1,328 @@ +"""TTS (Text-to-Speech) namespace client.""" + +import asyncio +from concurrent.futures import ThreadPoolExecutor +from typing import AsyncIterable, Iterable, Iterator, Optional, Union + +import ormsgpack +from httpx_ws import AsyncWebSocketSession, WebSocketSession, aconnect_ws, connect_ws + +from .realtime import aiter_websocket_audio, iter_websocket_audio +from ..core import AsyncClientWrapper, ClientWrapper, RequestOptions +from ..types import ( + CloseEvent, + FlushEvent, + Model, + StartEvent, + TextEvent, + TTSConfig, + TTSRequest, +) + + +def _config_to_tts_request(config: TTSConfig, text: str) -> TTSRequest: + """Convert TTSConfig to TTSRequest with text.""" + return TTSRequest( + text=text, + chunk_length=config.chunk_length, + format=config.format, + sample_rate=config.sample_rate, + mp3_bitrate=config.mp3_bitrate, + opus_bitrate=config.opus_bitrate, + references=config.references, + reference_id=config.reference_id, + normalize=config.normalize, + latency=config.latency, + prosody=config.prosody, + top_p=config.top_p, + temperature=config.temperature, + ) + + +def _normalize_to_event( + item: Union[str, TextEvent, FlushEvent], +) -> Union[TextEvent, FlushEvent]: + """Normalize string input to TextEvent, pass through event types unchanged.""" + if isinstance(item, (TextEvent, FlushEvent)): + return item + return TextEvent(text=item) + + +class TTSClient: + """Synchronous TTS operations.""" + + def __init__(self, client_wrapper: ClientWrapper): + self._client = client_wrapper + + def convert( + self, + *, + text: str, + config: TTSConfig = TTSConfig(), + model: Model = "s1", + request_options: Optional[RequestOptions] = None, + ) -> Iterator[bytes]: + """ + Convert text to speech. + + Args: + text: Text to synthesize + config: TTS configuration (audio settings, voice, model parameters) + model: TTS model to use + request_options: Request-level overrides + + Returns: + Iterator of audio bytes + + Example: + ```python + from fishaudio import FishAudio, TTSConfig + + client = FishAudio(api_key="...") + + # Simple usage with defaults + audio = client.tts.convert(text="Hello world") + + # Custom configuration + config = TTSConfig(format="wav", mp3_bitrate=192) + audio = client.tts.convert(text="Hello world", config=config) + + with open("output.mp3", "wb") as f: + for chunk in audio: + f.write(chunk) + ``` + """ + # Build request payload from config + request = _config_to_tts_request(config, text) + payload = request.model_dump(exclude_none=True) + + # Make request with streaming + response = self._client.request( + "POST", + "/v1/tts", + headers={"Content-Type": "application/msgpack", "model": model}, + content=ormsgpack.packb(payload), + request_options=request_options, + ) + + # Stream response chunks + for chunk in response.iter_bytes(): + if chunk: + yield chunk + + def stream_websocket( + self, + text_stream: Iterable[Union[str, TextEvent, FlushEvent]], + *, + config: TTSConfig = TTSConfig(), + model: Model = "s1", + max_workers: int = 10, + ) -> Iterator[bytes]: + """ + Stream text and receive audio in real-time via WebSocket. + + Perfect for conversational AI, live captioning, and streaming applications. + + Args: + text_stream: Iterator of text chunks to stream + config: TTS configuration (audio settings, voice, model parameters) + model: TTS model to use + max_workers: ThreadPoolExecutor workers for concurrent sender + + Returns: + Iterator of audio bytes + + Example: + ```python + from fishaudio import FishAudio, TTSConfig + + client = FishAudio(api_key="...") + + def text_generator(): + yield "Hello, " + yield "this is " + yield "streaming text!" + + # Simple usage with defaults + with open("output.mp3", "wb") as f: + for audio_chunk in client.tts.stream_websocket(text_generator()): + f.write(audio_chunk) + + # Custom configuration + config = TTSConfig(format="wav", latency="normal") + with open("output.wav", "wb") as f: + for audio_chunk in client.tts.stream_websocket(text_generator(), config=config): + f.write(audio_chunk) + ``` + """ + # Build TTSRequest from config + tts_request = _config_to_tts_request(config, text="") + + # Create WebSocket client + ws_client = self._client.create_websocket_client() + executor = ThreadPoolExecutor(max_workers=max_workers) + + try: + ws: WebSocketSession + with connect_ws( + "/v1/tts/live", client=ws_client, headers={"model": model} + ) as ws: + + def sender(): + ws.send_bytes( + ormsgpack.packb(StartEvent(request=tts_request).model_dump()) + ) + # Normalize strings to TextEvent + for item in text_stream: + event = _normalize_to_event(item) + ws.send_bytes(ormsgpack.packb(event.model_dump())) + ws.send_bytes(ormsgpack.packb(CloseEvent().model_dump())) + + sender_future = executor.submit(sender) + + # Process incoming audio messages + for audio_chunk in iter_websocket_audio(ws): + yield audio_chunk + + sender_future.result() + finally: + ws_client.close() + executor.shutdown(wait=False) + + +class AsyncTTSClient: + """Asynchronous TTS operations.""" + + def __init__(self, client_wrapper: AsyncClientWrapper): + self._client = client_wrapper + + async def convert( + self, + *, + text: str, + config: TTSConfig = TTSConfig(), + model: Model = "s1", + request_options: Optional[RequestOptions] = None, + ): + """ + Convert text to speech (async). + + Args: + text: Text to synthesize + config: TTS configuration (audio settings, voice, model parameters) + model: TTS model to use + request_options: Request-level overrides + + Returns: + Async iterator of audio bytes + + Example: + ```python + from fishaudio import AsyncFishAudio, TTSConfig + + client = AsyncFishAudio(api_key="...") + + # Simple usage with defaults + audio = await client.tts.convert(text="Hello world") + + # Custom configuration + config = TTSConfig(format="wav", mp3_bitrate=192) + audio = await client.tts.convert(text="Hello world", config=config) + + async with aiofiles.open("output.mp3", "wb") as f: + async for chunk in audio: + await f.write(chunk) + ``` + """ + # Build request payload from config + request = _config_to_tts_request(config, text) + payload = request.model_dump(exclude_none=True) + + # Make request with streaming + response = await self._client.request( + "POST", + "/v1/tts", + headers={"Content-Type": "application/msgpack", "model": model}, + content=ormsgpack.packb(payload), + request_options=request_options, + ) + + # Stream response chunks + async for chunk in response.aiter_bytes(): + if chunk: + yield chunk + + async def stream_websocket( + self, + text_stream: AsyncIterable[Union[str, TextEvent, FlushEvent]], + *, + config: TTSConfig = TTSConfig(), + model: Model = "s1", + ): + """ + Stream text and receive audio in real-time via WebSocket (async). + + Perfect for conversational AI, live captioning, and streaming applications. + + Args: + text_stream: Async iterator of text chunks to stream + config: TTS configuration (audio settings, voice, model parameters) + model: TTS model to use + + Returns: + Async iterator of audio bytes + + Example: + ```python + from fishaudio import AsyncFishAudio, TTSConfig + + client = AsyncFishAudio(api_key="...") + + async def text_generator(): + yield "Hello, " + yield "this is " + yield "async streaming!" + + # Simple usage with defaults + async with aiofiles.open("output.mp3", "wb") as f: + async for audio_chunk in client.tts.stream_websocket(text_generator()): + await f.write(audio_chunk) + + # Custom configuration + config = TTSConfig(format="wav", latency="normal") + async with aiofiles.open("output.wav", "wb") as f: + async for audio_chunk in client.tts.stream_websocket(text_generator(), config=config): + await f.write(audio_chunk) + ``` + """ + # Build TTSRequest from config + tts_request = _config_to_tts_request(config, text="") + + # Create WebSocket client + ws_client = self._client.create_websocket_client() + + try: + ws: AsyncWebSocketSession + async with aconnect_ws( + "/v1/tts/live", client=ws_client, headers={"model": model} + ) as ws: + + async def sender(): + await ws.send_bytes( + ormsgpack.packb(StartEvent(request=tts_request).model_dump()) + ) + # Normalize strings to TextEvent + async for item in text_stream: + event = _normalize_to_event(item) + await ws.send_bytes(ormsgpack.packb(event.model_dump())) + await ws.send_bytes(ormsgpack.packb(CloseEvent().model_dump())) + + sender_task = asyncio.create_task(sender()) + + # Process incoming audio messages + async for audio_chunk in aiter_websocket_audio(ws): + yield audio_chunk + + await sender_task + finally: + await ws_client.aclose() diff --git a/src/fishaudio/resources/voices.py b/src/fishaudio/resources/voices.py new file mode 100644 index 0000000..629b8fa --- /dev/null +++ b/src/fishaudio/resources/voices.py @@ -0,0 +1,425 @@ +"""Voice management namespace client.""" + +from typing import List, Optional, Union + +from ..core import OMIT, AsyncClientWrapper, ClientWrapper, RequestOptions +from ..types import PaginatedResponse, Visibility, Voice + + +def _filter_none(d: dict) -> dict: + """Remove None and OMIT values from dictionary.""" + return {k: v for k, v in d.items() if v is not None and v is not OMIT} + + +class VoicesClient: + """Synchronous voice management operations.""" + + def __init__(self, client_wrapper: ClientWrapper): + self._client = client_wrapper + + def list( + self, + *, + page_size: int = 10, + page_number: int = 1, + title: Optional[str] = OMIT, + tags: Optional[Union[List[str], str]] = OMIT, + self_only: bool = False, + author_id: Optional[str] = OMIT, + language: Optional[Union[List[str], str]] = OMIT, + title_language: Optional[Union[List[str], str]] = OMIT, + sort_by: str = "task_count", + request_options: Optional[RequestOptions] = None, + ) -> PaginatedResponse[Voice]: + """ + List available voices/models. + + Args: + page_size: Number of results per page + page_number: Page number (1-indexed) + title: Filter by title + tags: Filter by tags (single tag or list) + self_only: Only return user's own voices + author_id: Filter by author ID + language: Filter by language(s) + title_language: Filter by title language(s) + sort_by: Sort field ("task_count" or "created_at") + request_options: Request-level overrides + + Returns: + Paginated response with total count and voice items + + Example: + ```python + client = FishAudio(api_key="...") + + # List all voices + voices = client.voices.list(page_size=20) + print(f"Total: {voices.total}") + for voice in voices.items: + print(f"{voice.title}: {voice.id}") + + # Filter by tags + tagged = client.voices.list(tags=["male", "english"]) + ``` + """ + # Build query parameters + params = _filter_none( + { + "page_size": page_size, + "page_number": page_number, + "title": title, + "tag": tags, + "self": self_only, + "author_id": author_id, + "language": language, + "title_language": title_language, + "sort_by": sort_by, + } + ) + + # Make request + response = self._client.request( + "GET", + "/model", + params=params, + request_options=request_options, + ) + + # Parse and return + return PaginatedResponse[Voice].model_validate(response.json()) + + def get( + self, + voice_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Voice: + """ + Get voice by ID. + + Args: + voice_id: Voice model ID + request_options: Request-level overrides + + Returns: + Voice model details + + Example: + ```python + client = FishAudio(api_key="...") + voice = client.voices.get("voice_id_here") + print(voice.title, voice.description) + ``` + """ + response = self._client.request( + "GET", + f"/model/{voice_id}", + request_options=request_options, + ) + return Voice.model_validate(response.json()) + + def create( + self, + *, + title: str, + voices: List[bytes], + description: Optional[str] = OMIT, + texts: Optional[List[str]] = OMIT, + tags: Optional[List[str]] = OMIT, + cover_image: Optional[bytes] = OMIT, + visibility: Visibility = "private", + train_mode: str = "fast", + enhance_audio_quality: bool = True, + request_options: Optional[RequestOptions] = None, + ) -> Voice: + """ + Create/clone a new voice. + + Args: + title: Voice model name + voices: List of audio file bytes for training + description: Voice description + texts: Transcripts for voice samples + tags: Tags for categorization + cover_image: Cover image bytes + visibility: Visibility setting (public, unlist, private) + train_mode: Training mode (currently only "fast" supported) + enhance_audio_quality: Whether to enhance audio quality + request_options: Request-level overrides + + Returns: + Created voice model + + Example: + ```python + client = FishAudio(api_key="...") + + with open("voice1.wav", "rb") as f1, open("voice2.wav", "rb") as f2: + voice = client.voices.create( + title="My Voice", + voices=[f1.read(), f2.read()], + description="Custom voice clone", + tags=["custom", "english"] + ) + print(f"Created: {voice.id}") + ``` + """ + # Build form data + data = _filter_none( + { + "title": title, + "description": description, + "visibility": visibility, + "type": "tts", + "train_mode": train_mode, + "texts": texts or [], + "tags": tags or [], + "enhance_audio_quality": enhance_audio_quality, + } + ) + + # Build files + files = [("voices", voice) for voice in voices] + if cover_image is not OMIT and cover_image is not None: + files.append(("cover_image", cover_image)) + + # Make request + response = self._client.request( + "POST", + "/model", + data=data, + files=files, + request_options=request_options, + ) + + return Voice.model_validate(response.json()) + + def update( + self, + voice_id: str, + *, + title: Optional[str] = OMIT, + description: Optional[str] = OMIT, + cover_image: Optional[bytes] = OMIT, + visibility: Optional[Visibility] = OMIT, + tags: Optional[List[str]] = OMIT, + request_options: Optional[RequestOptions] = None, + ) -> None: + """ + Update voice metadata. + + Args: + voice_id: Voice model ID + title: New title + description: New description + cover_image: New cover image bytes + visibility: New visibility setting + tags: New tags + request_options: Request-level overrides + + Example: + ```python + client = FishAudio(api_key="...") + client.voices.update( + "voice_id_here", + title="Updated Title", + visibility="public" + ) + ``` + """ + # Build form data + data = _filter_none( + { + "title": title, + "description": description, + "visibility": visibility, + "tags": tags, + } + ) + + # Build files if needed + files = [] + if cover_image is not OMIT and cover_image is not None: + files.append(("cover_image", cover_image)) + + # Make request + self._client.request( + "PATCH", + f"/model/{voice_id}", + data=data, + files=files if files else None, + request_options=request_options, + ) + + def delete( + self, + voice_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """ + Delete a voice. + + Args: + voice_id: Voice model ID + request_options: Request-level overrides + + Example: + ```python + client = FishAudio(api_key="...") + client.voices.delete("voice_id_here") + ``` + """ + self._client.request( + "DELETE", + f"/model/{voice_id}", + request_options=request_options, + ) + + +class AsyncVoicesClient: + """Asynchronous voice management operations.""" + + def __init__(self, client_wrapper: AsyncClientWrapper): + self._client = client_wrapper + + async def list( + self, + *, + page_size: int = 10, + page_number: int = 1, + title: Optional[str] = OMIT, + tags: Optional[Union[List[str], str]] = OMIT, + self_only: bool = False, + author_id: Optional[str] = OMIT, + language: Optional[Union[List[str], str]] = OMIT, + title_language: Optional[Union[List[str], str]] = OMIT, + sort_by: str = "task_count", + request_options: Optional[RequestOptions] = None, + ) -> PaginatedResponse[Voice]: + """List available voices/models (async). See sync version for details.""" + params = _filter_none( + { + "page_size": page_size, + "page_number": page_number, + "title": title, + "tag": tags, + "self": self_only, + "author_id": author_id, + "language": language, + "title_language": title_language, + "sort_by": sort_by, + } + ) + + response = await self._client.request( + "GET", + "/model", + params=params, + request_options=request_options, + ) + + return PaginatedResponse[Voice].model_validate(response.json()) + + async def get( + self, + voice_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Voice: + """Get voice by ID (async). See sync version for details.""" + response = await self._client.request( + "GET", + f"/model/{voice_id}", + request_options=request_options, + ) + return Voice.model_validate(response.json()) + + async def create( + self, + *, + title: str, + voices: List[bytes], + description: Optional[str] = OMIT, + texts: Optional[List[str]] = OMIT, + tags: Optional[List[str]] = OMIT, + cover_image: Optional[bytes] = OMIT, + visibility: Visibility = "private", + train_mode: str = "fast", + enhance_audio_quality: bool = True, + request_options: Optional[RequestOptions] = None, + ) -> Voice: + """Create/clone a new voice (async). See sync version for details.""" + data = _filter_none( + { + "title": title, + "description": description, + "visibility": visibility, + "type": "tts", + "train_mode": train_mode, + "texts": texts or [], + "tags": tags or [], + "enhance_audio_quality": enhance_audio_quality, + } + ) + + files = [("voices", voice) for voice in voices] + if cover_image is not OMIT and cover_image is not None: + files.append(("cover_image", cover_image)) + + response = await self._client.request( + "POST", + "/model", + data=data, + files=files, + request_options=request_options, + ) + + return Voice.model_validate(response.json()) + + async def update( + self, + voice_id: str, + *, + title: Optional[str] = OMIT, + description: Optional[str] = OMIT, + cover_image: Optional[bytes] = OMIT, + visibility: Optional[Visibility] = OMIT, + tags: Optional[List[str]] = OMIT, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Update voice metadata (async). See sync version for details.""" + data = _filter_none( + { + "title": title, + "description": description, + "visibility": visibility, + "tags": tags, + } + ) + + files = [] + if cover_image is not OMIT and cover_image is not None: + files.append(("cover_image", cover_image)) + + await self._client.request( + "PATCH", + f"/model/{voice_id}", + data=data, + files=files if files else None, + request_options=request_options, + ) + + async def delete( + self, + voice_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a voice (async). See sync version for details.""" + await self._client.request( + "DELETE", + f"/model/{voice_id}", + request_options=request_options, + ) diff --git a/src/fishaudio/types/__init__.py b/src/fishaudio/types/__init__.py new file mode 100644 index 0000000..9c4994e --- /dev/null +++ b/src/fishaudio/types/__init__.py @@ -0,0 +1,54 @@ +"""Type definitions for the Fish Audio SDK.""" + +from .account import Credits, Package +from .asr import ASRResponse, ASRSegment +from .shared import ( + AudioFormat, + LatencyMode, + Model, + ModelState, + PaginatedResponse, + TrainMode, + Visibility, +) +from .tts import ( + CloseEvent, + FlushEvent, + Prosody, + ReferenceAudio, + StartEvent, + TextEvent, + TTSConfig, + TTSRequest, +) +from .voices import Author, Sample, Voice + +__all__ = [ + # Account types + "Credits", + "Package", + # ASR types + "ASRResponse", + "ASRSegment", + # Shared types + "AudioFormat", + "LatencyMode", + "Model", + "ModelState", + "PaginatedResponse", + "TrainMode", + "Visibility", + # TTS types + "CloseEvent", + "FlushEvent", + "Prosody", + "ReferenceAudio", + "StartEvent", + "TextEvent", + "TTSConfig", + "TTSRequest", + # Voice types + "Author", + "Sample", + "Voice", +] diff --git a/src/fishaudio/types/account.py b/src/fishaudio/types/account.py new file mode 100644 index 0000000..2803383 --- /dev/null +++ b/src/fishaudio/types/account.py @@ -0,0 +1,35 @@ +"""Account-related types (credits, packages, etc.).""" + +import decimal +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field + + +class Credits(BaseModel): + """User's API credit balance.""" + + model_config = ConfigDict(populate_by_name=True) + + id: str = Field(alias="_id") + user_id: str + credit: decimal.Decimal + created_at: str + updated_at: str + has_phone_sha256: Optional[bool] = None + has_free_credit: Optional[bool] = None + + +class Package(BaseModel): + """User's prepaid package information.""" + + model_config = ConfigDict(populate_by_name=True) + + id: str = Field(alias="_id") + user_id: str + type: str + total: int + balance: int + created_at: str + updated_at: str + finished_at: Optional[str] = None diff --git a/src/fishaudio/types/asr.py b/src/fishaudio/types/asr.py new file mode 100644 index 0000000..84d2dbb --- /dev/null +++ b/src/fishaudio/types/asr.py @@ -0,0 +1,21 @@ +"""ASR (Automatic Speech Recognition) related types.""" + +from typing import List + +from pydantic import BaseModel + + +class ASRSegment(BaseModel): + """A timestamped segment of transcribed text.""" + + text: str + start: float + end: float + + +class ASRResponse(BaseModel): + """Response from speech-to-text transcription.""" + + text: str + duration: float # Duration in milliseconds + segments: List[ASRSegment] diff --git a/src/fishaudio/types/shared.py b/src/fishaudio/types/shared.py new file mode 100644 index 0000000..28ef594 --- /dev/null +++ b/src/fishaudio/types/shared.py @@ -0,0 +1,34 @@ +"""Shared types used across the SDK.""" + +from typing import Generic, List, Literal, TypeVar + +from pydantic import BaseModel + +# Type variable for generic pagination +T = TypeVar("T") + + +class PaginatedResponse(BaseModel, Generic[T]): + """Generic paginated response.""" + + total: int + items: List[T] + + +# Model types +Model = Literal["speech-1.5", "speech-1.6", "s1"] + +# Audio format types +AudioFormat = Literal["wav", "pcm", "mp3"] + +# Visibility types +Visibility = Literal["public", "unlist", "private"] + +# Training mode types +TrainMode = Literal["fast", "full"] + +# Model state types +ModelState = Literal["created", "training", "trained", "failed"] + +# Latency modes +LatencyMode = Literal["normal", "balanced"] diff --git a/src/fishaudio/types/tts.py b/src/fishaudio/types/tts.py new file mode 100644 index 0000000..eb36398 --- /dev/null +++ b/src/fishaudio/types/tts.py @@ -0,0 +1,100 @@ +"""TTS-related types.""" + +from typing import Annotated, List, Literal, Optional + +from pydantic import BaseModel, Field + +from .shared import LatencyMode + + +class ReferenceAudio(BaseModel): + """Reference audio for voice cloning/style.""" + + audio: bytes + text: str + + +class Prosody(BaseModel): + """Speech prosody settings (speed and volume).""" + + speed: float = 1.0 + volume: float = 0.0 + + +class TTSConfig(BaseModel): + """ + TTS generation configuration. + + Reusable configuration for text-to-speech requests. Create once, use multiple times. + All parameters have sensible defaults. + """ + + # Audio output settings + format: Literal["wav", "pcm", "mp3"] = "mp3" + sample_rate: Optional[int] = None + mp3_bitrate: Literal[64, 128, 192] = 128 + opus_bitrate: Literal[-1000, 24, 32, 48, 64] = 32 + normalize: bool = True + + # Generation settings + chunk_length: Annotated[int, Field(ge=100, le=300, strict=True)] = 200 + latency: LatencyMode = "balanced" + + # Voice/style settings + reference_id: Optional[str] = None + references: List[ReferenceAudio] = [] + prosody: Optional[Prosody] = None + + # Model parameters + top_p: float = 0.7 + temperature: float = 0.7 + + +class TTSRequest(BaseModel): + """ + Request parameters for text-to-speech generation. + + This model is used internally for WebSocket streaming. + For the HTTP API, parameters are passed directly to methods. + """ + + text: str + chunk_length: Annotated[int, Field(ge=100, le=300, strict=True)] = 200 + format: Literal["wav", "pcm", "mp3"] = "mp3" + sample_rate: Optional[int] = None + mp3_bitrate: Literal[64, 128, 192] = 128 + opus_bitrate: Literal[-1000, 24, 32, 48, 64] = 32 + references: List[ReferenceAudio] = [] + reference_id: Optional[str] = None + normalize: bool = True + latency: LatencyMode = "balanced" + prosody: Optional[Prosody] = None + top_p: float = 0.7 + temperature: float = 0.7 + + +# WebSocket event types for streaming TTS +class StartEvent(BaseModel): + """WebSocket start event.""" + + event: Literal["start"] = "start" + request: TTSRequest + + +class TextEvent(BaseModel): + """WebSocket text chunk event.""" + + event: Literal["text"] = "text" + text: str + + +class FlushEvent(BaseModel): + """WebSocket flush event - forces buffer to generate audio immediately.""" + + event: Literal["flush"] = "flush" + + +class CloseEvent(BaseModel): + """WebSocket close event.""" + + event: Literal["stop"] = "stop" diff --git a/src/fishaudio/types/voices.py b/src/fishaudio/types/voices.py new file mode 100644 index 0000000..90e41b2 --- /dev/null +++ b/src/fishaudio/types/voices.py @@ -0,0 +1,55 @@ +"""Voice and model management types.""" + +import datetime +from typing import List, Literal + +from pydantic import BaseModel, Field + +from .shared import ModelState, TrainMode, Visibility + + +class Sample(BaseModel): + """A sample audio for a voice model.""" + + title: str + text: str + task_id: str + audio: str + + +class Author(BaseModel): + """Voice model author information.""" + + id: str = Field(alias="_id") + nickname: str + avatar: str + + +class Voice(BaseModel): + """ + A voice model + + Represents a TTS voice that can be used for synthesis. + """ + + id: str = Field(alias="_id") + type: Literal["svc", "tts"] + title: str + description: str + cover_image: str + train_mode: TrainMode + state: ModelState + tags: List[str] + samples: List[Sample] + created_at: datetime.datetime + updated_at: datetime.datetime + languages: List[str] + visibility: Visibility + lock_visibility: bool + like_count: int + mark_count: int + shared_count: int + task_count: int + liked: bool = False + marked: bool = False + author: Author diff --git a/src/fishaudio/utils/__init__.py b/src/fishaudio/utils/__init__.py new file mode 100644 index 0000000..2136fc6 --- /dev/null +++ b/src/fishaudio/utils/__init__.py @@ -0,0 +1,11 @@ +"""Helper utilities for audio playback and saving.""" + +from .play import play +from .save import save +from .stream import stream + +__all__ = [ + "play", + "save", + "stream", +] diff --git a/src/fishaudio/utils/play.py b/src/fishaudio/utils/play.py new file mode 100644 index 0000000..685918b --- /dev/null +++ b/src/fishaudio/utils/play.py @@ -0,0 +1,102 @@ +"""Audio playback utility.""" + +import io +import subprocess +from typing import Iterator, Union + +from ..exceptions import DependencyError + + +def _is_installed(command: str) -> bool: + """Check if a command is available in PATH.""" + try: + subprocess.run(["which", command], capture_output=True, check=True) + return True + except subprocess.CalledProcessError: + return False + + +def play( + audio: Union[bytes, Iterator[bytes]], + *, + notebook: bool = False, + use_ffmpeg: bool = True, +) -> None: + """ + Play audio using various playback methods. + + Args: + audio: Audio bytes or iterator of bytes + notebook: Use Jupyter notebook playback (IPython.display.Audio) + use_ffmpeg: Use ffplay for playback (default, falls back to sounddevice) + + Raises: + DependencyError: If required playback tool is not installed + + Examples: + ```python + from fishaudio import FishAudio, play + + client = FishAudio(api_key="...") + audio = client.tts.convert(text="Hello world") + + # Play directly + play(audio) + + # In Jupyter notebook + play(audio, notebook=True) + + # Force sounddevice fallback + play(audio, use_ffmpeg=False) + ``` + """ + # Consolidate iterator to bytes + if isinstance(audio, Iterator): + audio = b"".join(audio) + + # Notebook mode + if notebook: + try: + from IPython.display import Audio, display + + display(Audio(audio, rate=44100, autoplay=True)) + return + except ImportError: + raise DependencyError("IPython", "pip install ipython") + + # FFmpeg mode (default) + if use_ffmpeg: + if not _is_installed("ffplay"): + raise DependencyError( + "ffplay", + "brew install ffmpeg # macOS\n" + "sudo apt install ffmpeg # Linux\n" + "https://ffmpeg.org/download.html # Windows", + ) + + try: + subprocess.run( + ["ffplay", "-autoexit", "-", "-nodisp"], + input=audio, + capture_output=True, + check=True, + ) + return + except subprocess.CalledProcessError: + # Fall through to sounddevice if ffplay fails + pass + + # Sounddevice fallback + try: + import sounddevice as sd + import soundfile as sf + except ImportError: + raise DependencyError( + "sounddevice and soundfile", + "pip install 'fishaudio[utils]' # or\npip install sounddevice soundfile", + ) + + # Load and play audio + data, samplerate = sf.read(io.BytesIO(audio)) + sd.play(data, samplerate) + sd.wait() # Wait until playback finishes diff --git a/src/fishaudio/utils/save.py b/src/fishaudio/utils/save.py new file mode 100644 index 0000000..29d309d --- /dev/null +++ b/src/fishaudio/utils/save.py @@ -0,0 +1,35 @@ +"""Audio saving utility.""" + +from typing import Iterator, Union + + +def save(audio: Union[bytes, Iterator[bytes]], filename: str) -> None: + """ + Save audio to a file. + + Args: + audio: Audio bytes or iterator of bytes + filename: Path to save the audio file + + Examples: + ```python + from fishaudio import FishAudio, save + + client = FishAudio(api_key="...") + audio = client.tts.convert(text="Hello world") + + # Save to file + save(audio, "output.mp3") + + # Works with iterators too + audio_stream = client.tts.convert(text="Another example") + save(audio_stream, "another.mp3") + ``` + """ + # Consolidate iterator to bytes if needed + if isinstance(audio, Iterator): + audio = b"".join(audio) + + # Write to file + with open(filename, "wb") as f: + f.write(audio) diff --git a/src/fishaudio/utils/stream.py b/src/fishaudio/utils/stream.py new file mode 100644 index 0000000..887c42f --- /dev/null +++ b/src/fishaudio/utils/stream.py @@ -0,0 +1,79 @@ +"""Audio streaming utility.""" + +import subprocess +from typing import Iterator + +from ..exceptions import DependencyError + + +def _is_installed(command: str) -> bool: + """Check if a command is available in PATH.""" + try: + subprocess.run(["which", command], capture_output=True, check=True) + return True + except subprocess.CalledProcessError: + return False + + +def stream(audio_stream: Iterator[bytes]) -> bytes: + """ + Stream audio in real-time while playing it with mpv. + + This function plays the audio as it's being generated and + simultaneously captures it to return the complete audio buffer. + + Args: + audio_stream: Iterator of audio byte chunks + + Returns: + Complete audio bytes after streaming finishes + + Raises: + DependencyError: If mpv is not installed + + Examples: + ```python + from fishaudio import FishAudio, stream + + client = FishAudio(api_key="...") + audio_stream = client.tts.convert(text="Hello world") + + # Stream and play in real-time, get complete audio + complete_audio = stream(audio_stream) + + # Save the captured audio + with open("output.mp3", "wb") as f: + f.write(complete_audio) + ``` + """ + if not _is_installed("mpv"): + raise DependencyError( + "mpv", + "brew install mpv # macOS\n" + "sudo apt install mpv # Linux\n" + "https://mpv.io/installation/ # Windows", + ) + + # Launch mpv process + mpv_process = subprocess.Popen( + ["mpv", "--no-cache", "--no-terminal", "--", "fd://0"], + stdin=subprocess.PIPE, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + # Stream chunks to mpv while accumulating + audio_buffer = b"" + try: + for chunk in audio_stream: + if chunk and mpv_process.stdin: + mpv_process.stdin.write(chunk) + mpv_process.stdin.flush() + audio_buffer += chunk + finally: + # Cleanup + if mpv_process.stdin: + mpv_process.stdin.close() + mpv_process.wait() + + return audio_buffer diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index 76acad3..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -import dotenv - -dotenv.load_dotenv() diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 17c2108..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,22 +0,0 @@ -import os - -import pytest - -from fish_audio_sdk import Session, WebSocketSession, AsyncWebSocketSession - -APIKEY = os.environ["APIKEY"] - - -@pytest.fixture -def session(): - return Session(APIKEY) - - -@pytest.fixture -def sync_websocket(): - return WebSocketSession(APIKEY) - - -@pytest.fixture -def async_websocket(): - return AsyncWebSocketSession(APIKEY) diff --git a/tests/hello.mp3 b/tests/hello.mp3 deleted file mode 100644 index afee1bea511f5c3073e3ba010d81f069d948728d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18390 zcmX`yRahKd(=h4*26xw>Gq?=y34>c;a0~7l+zIaP?(XjHPH=}1+=GN53E_Y8?(5rq z)_u6{uBx@FtKWVni~tapb+*repzWxdfxLl~EH5}G3K$unfE6jsD)Lhgjmb&c3s|^& zM})fb5hW-n>_ZU_la|krLibJTtN5~5AfNyi%z&cK-3>hNxxOsn#W>olzSL=yb*=Q7_XXxPZ{#ry5~yH#zm8v zC*tK6Dr#-08;V(oGnvU3>ngTseuA>|op0eB%CE2CTt4{dM4waS_dVvL;oRhM2k+TNW`spy+}rxq{>9ANhMv9O;mGsBay(8!q;;AbUKO~ z!*!cW;6&TUv-kEpX@m-(9)|P`%os$Mff|zh0(Z_S{+tiz#~o(o9#}#lRPL4S9np}3 zYYc@W?H5Ed`stkDLNaDr2#>-#F~Sgr!%H5=Zt@eVtTw4sb8=Txc9a$dNu%$JSjZ1wYslX7PuI zJiR(U@DH6IRb5iDv!a4w-_rNc;*#^4}Ft_FXeqtX%_E6o5H>;#Ak z*MO6X%7sTD3p9(RLC%ci#*!i~v4%T(e;LOnlR1-dCLZM$Hx$p>xtiEAk})V0LW)Tc znOnug9C|0ckd#lC#U`2iU{P$(A0ayOfJL378GKk>w9CBf+d1pZln(tki+aSjUG>gd zgf^>U9nB$wr9LNKVNS{(Ps9XQ-a*B+wwDdOMFnP-M=quonTpQmkNMylh>sFQAM#nLp(DONO7ILXAB#@in-j1iKX zVi_z>=<{s$4!))n_@DS12$J z-|?eciE>9@Cl1CI%hpK~EofQIgXCyb1xQK5nzpkuy>p}|Ve*zM(hOLcHC5D-V#Q_& z&xi3wbQs)Ly#h`1dPMq(qc(^XNL36>!Nj2qp#)aq%+@SeE!UVXGe@Mj$k4GxcaI_@ z60@~!uc}`W_z?<k~t!AT)S3p$gRf}e|${mwAfI8G8vh-?Bfp$;PtvG}O6!6AXCE~pW)iRVTejJ9b; zMP&Vyl}%Z$gI$vAqLXK~9s5}4KVP6wv?NzSBhaj&aovC?C6x=F zh z5BYG%GI$YL6VX2Tq&evW>2Wj*_?{1f_wpoRGImmah>4HU~CNXZrmOqj4TcIM(6x}e4*;Y&g?o%@bo=7oy( zOeF&|08P!Nz}&|}V5cT(!q4m)HYtq(88w<&7dYM)O~g?$PIVP? zf+<{RIduh9hax-8)w7R3V&+9hqG8%7)J36?I9qf5+6i6rFqZnHG=thy7Sk9jkc6&& z_H837q$ds{v1Eg73R@EQUbeD~DiMFms0U^$8fVnS!h;fnZ|Mw5NKr)wDoo4EgW6S9 z7mM;6^um}DQuGr`ndbo$^n{e+fguVtvXSJEHBO(zr<6P-n~1FvZfS%qB#tH`&>}Y> z5?Pu>-XJTy@j@J# zg1XZh6K&oq!70ALsclrZfbm=}4Y(6qKG_nAsuMe1!hr#zG(E3~5=)3q@w;oJdA6cI z-JKYiJ(AiY>ARvYW?%fG<7~7t$bZcn} zZ4Ni5#<6Mt6EY9hbb5scl`KuoLyaVd8EAAAEN_2;xLeKT6EX1gz1*z1XGx^2a9S22 zM>scWSSU!5=zy)rqERK#?uXSa59~1>Xi_{e+^^cwq zh9uH&lJJ#~eLN+kJvSErB*vCFY+oBq@oVt_#z~LZkJvX}>+Q<`OGB z+>3&~;n7dOH;Y7fz3yDaeitwG=q)$X&@npKcUM)(U+6;g%yvzJR(=V6!3U@U0Q{Ik z^Nz&GL+ewKdP+oyO(^y7B-Vfm#z@#6i-He$hz%lMlm-lF8M>l18r#FsF`f)4cqd{bFl5VMf+iYE>AI00IYcul$7NlT zc%0cxKPil*VmX{I$Ab_jfk{_kB4_OE%bF_A#Nm@n42@n)CuQ2fr6@U3lK~xKZ4kuK zwPa_8USELr&bP-sC=>I(xi>1FwzO(oQ68s~squJX`xv<@t7bZ5y&r;yOE7?cqsb5$ z{jEmG4{nTMYm}>Yg2w&Le=S$#Z>q}-VwoqM&oz|q_ zBqs;?`Tb$)KcUDV?Z8{4@sh=8?hnRr;aP&sVE3!u*5;lb{l{*bFY7fyCmDA=cD<&o zBbh0^IIv;>wpDOEbzo?|LxBWN$ObLRXDc8l%F-hC+;#Kmp}JS=9S$^Ki3$GsMvYXi zYe|eei~ZPEWh8b^MDJz%2VXH6LoLm(&=pe6)i3_@Lj(^>vx29uOZhZ+eHZCu zuBA}Mvp~cmBN${T7=s!(E304tz(b_KizA)KO%KUH4Gaq)F*~c|$Q~#a?x(T~77G~AO=_ib#$5T?X+nIK+Q@A~mzpFd zH6Sd#VhDJveanR-QL#Dz9k!Y|DycBT=^+mKma3B=(BSLz;%+zzGLd}lzO}Yy&991) z-`5LGo=)ni`h?BRyT`7_9dgYO`^_!=;1T276RVA_Ot{r<_ZfLz9chK!Q%KRK;=S!}LAa z48Q?QMn-sSS(?ac6NP9p9w;}`Z2ZA`$5{|`oj#^Qi$y~mcC!~{=d8yr^$vS%I)-Sh zLsG>&#ppiieXQ}|5y|v^n2rto`j>pT@7s1d!{OCmdT;*9p_y3RNSPuh)_R5h)IP{) z%V<4aqhC!s)HvC0@8xjY_HYMqg&>d7n{oar1&>t)0N@As_n2mIVc1-#7|a1euxQ01 zC4{uiLvwoBa@;V-VZw~^Pz6XjcL*EfKs6=MYF=VelPpPjz|EpG@#4n}GO2@dv#{yq zL%0&}R7{;YWCLNS8)zD(-TM(f#+eR~vQ$Kl0aw6^uLO?>8QD4FDDYu#eN;b_NC6GF zr-8D1&5l0&6A};J!|W!i>3tuq*?2G*M!txXQ7$-x3iqNIhp!JRc&~cB zVsXo=T!<97#}`#mfB?d9?UtMwDM2F6SrFv!^t-M$s`7-};YIc3jf2*2gBweIR z+=xV1gAB9D2oK22T4Wm*vI!Z?H}RZIvZ&5eKW|Oo(jB%rk}BquEv4w=ml963&XY*8 zNl{;D`?9tdJ%9fh9>zDh9|}(cAjybnsX&ZcMZrjm<}_`{gfkl{Yy}C2DdH)3o@oLGaE~=F^(q_f^)Q_YR9qLaEBu@&@~DhtFDtK zn>~Ys|Fg*kvN;e);k;~PF$hznHsRHko4rfs0zRM>*32Gbq$BT0vYfa_(sP065SEek*zq9SWQA3X066 z2nae+plDQ2f>aqC3+;8wB7?-ypwAY8RA>8p>5h%of$%(Ve0;N zX2L`Z)z~{DJyhAwQaVwMzKD7k$#;{Zo5YBrm8saadU*(s&QSNi#I`*j`RBfZ?LB^v zD92nkJQ`BU;?kKU{Sf~8<*jFN`zLVr0L>*50G|W^sJ9WIOCr>vVwI45z+p?#`x-GA zKWoX^P$-WzO8Nwt6q&%V*I9P>gUjA>|I2wL({G~hO^{$0e<8CJ9$-lnH~S&}`FxNy z$5l9~2Ar^n80>`7)>27&Kd)Qk`)AR@wv#`spJ+49Eeu;JUJwYm zyDcg|Uro1)>gu&CkLRDfh5w21A2mX8pVL#|s_yY<@HsS7s)RVz@AVN7CByI5(h)^IBni zPMKV$al_1wWsX>?n^V97LLO;i=hg2_ zgdoHbh-Cav2p4XB)`b&GR8Rp_e*`z)OkD^fIx2U~nE^ zwbi`S&vFtC2S$~8iZI>AltB(@`3(I}^Qkwxxiwv^qa1DBTok_&OfOiw*249N>hwzM zzJ1K;tD0V2z9!V!TD};blH_#_mPo16cxPIr0RjHx$gpj>Sq6(S=k|&mJ;8-z%}%1+1$0s zP{Bo%*fpQo{FhoMVDq%ZF$O8=sEzpAdHNx}QHImS|!$O+)E_LOAoh09LsM0{Hw6%%!hvCm@|QHhk@NTRD0y zL~3|Oo72#dSCgQ8VR;<#KsQxQLU4~$yx1Qrsxe7gb){n?<}y3quY`B|8&j{({XLKV zHZ3z6#aoU%pkjlxo)(wR^(Pkp7;Ul>c01#nE-M!MXU zJjHBm>M1ang9xh(21L6-5aoel=3!q{7HgtjLRb2>pSPOq&BiPR;XXX>n*!3kVKbGs zj*F=|j|dSAioYP#P#c7bz&6%A zD=lY(Xf^Dp7A81(FB3!9@5#te$6xS)8AX~4pIVgdX5|jhu*hV6zx=Tf#8m(E?7zV` zOROi3Xr6zXT@_Q&qn|P&mfXGSXe3_m=tHGC$HQTwCbNFisx8$ z#?`al|J8`F-yvA}y%O;mD6(<;H3*>!gFv818Gtw|ZWP08WqlS6r4hq{-B~^Csl$)N z5JaG`3wcO|D+@~zBI4o|-F1w_9j&SP1l8ZWf6y`&ttRQVvER)ruUTnods~Zulwx_| zJ-|wT|J;!ii;+mcU%v#!xJevjW`9ufhkR(E`mX1tdfaBU%MYibd*u{WPDKqBRE+Sh$3V(0})FIz}ve` zU3VHB`Cpa;=*9Y4=q-dAiBA)K?p0!5q?;M(J6K zySxHUxye&hAflIkFz1yjiE~Wy!j9x7^^zzQD5@X`r&5FqZYVbiK;SEjHKOF`43G|g zC)$KKW!$(BDPjLbz!Y}@62=RM2xzyaMrr%~uHB)=`5TMsBeULSH9$AR^^>BDRReFH zw8ehKln5}iH+B|Y`4U*NdnH%I?W($Sul(BEEV_68p22@lQahaPNQ1T7UR8ihH`tO2 zSQ~bWCjnMko!64lTTx3)8LKd5(Ic0;$EZ3nKa!tn`Xw1~4H9djP0ON>8a}<}Q%n*V zQthb=bLcsf@MwQ`*WvlcOqi#|W^a7dc7p}Ycy@+L8KJmv>bgG5J)6h*0r@7MN9+?z&0s`LNqCPP zgo#j=8-PiKpK4U1R5C#>x|Q*RRPG&bU8}X?v>{m;z3#Qm^zy`!TPhI|Bde4JmDNp4 zW^f{72Q4)o)(S$2Y>7)6)|@`PS#S`Py36ifF0M~_5&Uk&8jpR&)$43}wdUUTOt$_l zx>jM$*yPRq+s_n^FT|zKKh6dKh-vBTMzqFTUXcjw0|Iz1dld;#Quw20vLFJ{RY(9% zzcVViVPjXld$+`Aj_!gWXBtm+vAt{1nZxf>8pv; zu+#Q#o2gS~X?x)!;6Xv9R`f_PMx#3faevP z1=b+hey?cxL`_SBPeBOJgBOiBRc3+8l4eAV$9Vrc=udm>GAzydtax{C>qC{{%hJws zv>}vu^`8(GV3jkNJr1%xwZ{(DSc}hu=*L$B^oq)Wh&q3uzI7)C=HNVqX7(b1j?{bY z^33P7d&ZeVaAs1AhoZfrAWCw>J3m(OV)#aJjlqSi-{3D;2r<&1DCs2Rdxux6{PD)j zlUrLuLvt^7wsMl}wAgJq4&OYDd%Ar$vlNV*e<>a9Vb)m@>_mv+G{_p)$Qn_z&W}pC zP{bXM`^SK=W_AGwu_OJ%1E9D}<@;lD1DiU(p+}FK`nUexSzFCgg8p}(eH6DDKf}<1 zCiUO{O+ms%@l1e^SoCq;wF)_T>KtWw$X1&;Y16)JJgC0UR@KYCipT#I8HUhyby);I zuxv;Ft=kW^Q@a_>99p$Ln||Xyv^ux?3#rKRyS`*076-IE(7(|!|f<|Lg0*Gdf5qCe+ zmY|r~O+-hv6Y>42yFQoxe7$ack>T#6k0NnSaHh_-BxBkh0)LuUy zV}3FC{7(oHpw8*R;UJ1XkdQ5Ba>{W|Qt=sx?m1(VJc6Dr@6gy(Bo}!)C1^{qs2CPK{GPXIH33Aoq zDl%gTWKd*K%ECb+&2%KNrd45<#fGU}H!=Ce$E{(r(IXzV4Hw!kFORAE;N?iGWrRv5 z7LAgp0gjK7ofoaFx+N~{uk=^_$IsBq8NW_l48s8!(#ZCt?V*WD^~xHp8;tZ)U-T(L=aCT{W0W> zGMz-kYV;rrI7)gXIEXJn+^JN|-IBfsv72*mX}(|fllx!8pC|VG=c~#x-Zz^0ccD5I zX2}hS4)H`aldD`#H%h;bD&>#(*_f3293-1YDMb09^_KT0sAQx(Mz#3w5jgalM{1)m zH-p_~sY{DNTrs$V8}<-s$?8q>s&4VDWF^+tj}=kcE46C(Qs5#>6DxFr@Ty7x)MGUzP=p%}I_PB?gG zUMLk*jVM^JIt3H}5JeQr&UPTj_0i6eR__kg--n&e*X8~&yqu1itHn;BnCXALD2x|g z-JNT_R-M`P=OTVH0SEhmXvyQ~;*!T!H65D_TI@2Zy`gKUqutXPU5peTm>U=>x4#6_ zbTZ1$P^?{0-5avnl?CflbtBXO$7E5TdQ;L>|UagdFj30$ycP^SIMHL~sWQHQ)>SI#lIgzTNVtc`--d?Juw6Ar$#uu zP{0sGiV@@B7FFuqw4^4Z`sk#n?1>DiIGXA%QJ0t3%cvFBG`MHy?J)sBAgj2t%lSF@ z8dW)|tZ*r-0teLNxxyMWJZ8syBLnXne8^FD;D+DbnjJN+2Qxj>*I>rv9XnDPM@mp&e{^DEG=YH+eDsR0hIv@->QU| z5hHk>p{CB)6G}_Vf*+g4&&OL@S}VKW$;572r^#ze+L|NLN6_Jl(rw6y!c5qK5kAF_nA97)qb4$TVKk>eI8SSf&fBW|J?cmko z=5u9r_a8M?!Ry9b;+KcjqP~C^@>ODa5k8d#G7c-Sue+jH7AR^=>*E4Ak0(nNWgtRv zz;2jy`~Kvcch%>(QQ^00WT~_ILXQjq5{p;PzwlwWL?pX6TuAH-!7I|_@azZp>CynP zz2}sVjtY9TaFT{t6K7U8VVCB(C$#?EZ=ASuj7!_@dyfixEI+Y=-$XLlW`hF*WPj>I z3!2;N)c+vev|5^A)YaPShONo}(GqwbvySiO9I;RoRnzHAZ4?W^=bjCz);0G>WHgX` z9_forN|X<`TjWn-9rrDF{Eo=HEJr77MGTr;B$pnJ5>y;>;AeX0;5{25J64EN=43jQ z!)T+cIV7D8o7MRUPl3sFS-El@nPKu;Bj}E#R)K~{skZjBav)0U%&2s`- zu{Z-uT8v;Di9>*b43LGH3LPaHc}s0CsEo2CGtD7sx00*U=ZxV2-|av)lP+4-RF%U! zbu96p&_RGY>1S>Sk(FUP$iJ>nW_$D|B6@9IySaQjb;)Q@F5>nql~{}z3*XWOo44|t z@7MbTJ6OIhd6mJ`evB=4Vcesz2upr07IXOiC%Q^dH*KRpPndQE8Bj8yA!Y9tB}I!p z@GdHu0UHY^%9W}9lW$mEriMV5Q+t`Ub(?uT_QkKVU7`&6!MQS@w!u9BqE5JYGdwkf z(LgWY5YZI}?mOD8H?*+3ra{b@ngD@)33Y%Z$Y+Gpl$XIc+qI!i(NCvUYFeb?RBT4= zr|6il+DqS5KxeNB!o+GL!Kj)BZ94?TzwzjMvlKA&KHPlZuDhl)pMF zy?4#V**nY8!5k*8MZu0M;(l+AN`Kc0 z?nTcvFLxzoNKEF}G#)Y*VdKw`C%p%!3W-U`&|F%s`x=oD(DcF?Bp@ zR?k17OL&q87xp^i*r}I>W74&(&J(my9NoYt_b44Ji)fJ4!UK<1XKizT?(5RUqIS3H z!ueULoG4Dkr40%CoNm{Cg)Y@QgN~3I4n}^bwMms>+|vcxLzWa)RPn@MCta#^lPbYGHPL9Yb z&mTfgyy~$qjHH25s*-Q=Be;UFss=0kpSFo*rJ5gn2W|Cq#^2ulzCL1s864+~7;ILL zgr7M3Qm152R2N^;w7A#bB&#V!4V6kxm=}O-BnpJb@OX2@zKbJsyXTzT0dkWT(G6y< zVTi@`D5ObR2KdJmIySSjym$@g(%&`aOkySd0Alc@Bp4K#^+*{vSV7QX;!vJ63cBG? z#TW$C>Ag4%K!KN z?P=vGuv$1b@}gA{=}D!M*>#-b7!I?D=+KcVBe#^;X7y=%ytm+*{Fkj5n`w~l?oz`ip89JwIwhe*Y!^)VI z>Mxh7LJQwI;GmgXSgP*HsbNx?gju91ZQD@4N*xXx#tC^ z$dQx}K>4JZQMBi$`({nq;Xq37_})|YM*ZE4x|`GcGL{`x<6&V)_pA}WHT_va!zOVv zOO;mXUkF{nuMdW@*7Xuj!$Ztdhnpjj@ZEA-6*{!q&feahYu4Cm6=rkyebw&`pOHjA zrprcBqPscU8#(o{BB1|TV`r|airhSOX>Qo%#j%Heo>#^pSld9D%dUc`V z{iXWRDL0UaC*-GnKCjI6_J!(pJ)HLp78WO@7i|WO2Qo#7tVltJ?4SrFLPt-0 zXOTgmXf*F-1{ zaDQI{x=B|$u>dR6Sz3{d)?N`rTd+ZX20`K$6se~Y4@;=mBI+z{;<@OHI@#pV<2q4N zYf4wr$Koln?sV|%$$}T8RLMMoJd-8|df{MBC8tdNPvsB_7M@K+u4M)+IK^-hxPmNH zj4}Z1CIbzcRz!l9E-8(Ki~RM##Nt<$x`_ry`p1);s#--md5^AJ5-cqDj8WNAkfw#Q z0~jFwPv{0fESJm~_g`z)-qcLx_jDVXWz1}m^vAes)7#43)+Woc zt^WQE#P{SM)$!1oVsNs#g(*jhv~?wb2kGO*`_{kFnh^#lLAaB#0nERA_YOi5eA6~f zOT%dS#m94Pq^jnu?wkqf&$wgN6_cB!B{+|s6-2L*wGWXmDH1P`0BjXNb}o2T&JcRz zG6_VZ0SXd)k_NGL6>>hPQ z=6JrfYBCb~p>p-OC!A$1RSBX1sw@e?mq-yU$XH|0`4j1=)yDmI?(vyng=WpwBHCws zMno*q`@aD1>y#$SNt19EjG@e6LI$BauTG!o_+^k-uBu0J0ynLPiy``Uga`7(S;~QFT=RV9M_DaK`I$AE_;WM$Q1$TNUo{pB z%>0dg`V5u_ZZK)2Avo_(u}v+)r)cD&Ge>^oUTwA|eG_;i{MLH;+V!ETlRhT6w@~Tu zGo$#jz;K3^T4<;Qd<}S1ycEo=8p+Bs7$Pz{7L}QG61Kgl{jSOfT}7fqr66tfan8Gi z+n#GnN`q4xn4}0x2xgLu`Xp(7?kMbNNhHg{rU3*1c(5Ud#N<5~&x}JOxKYKpM+4@; z@+0Wkj23l*4r!z{!!@NJZG}gSm)8z7Fl2aEWF2b7<5S63I$g33;kKK{6W&6~91D&1XDrqzOFCU3EI!7r1Pe)+Ne0u1 zIbsD&!WR`ZkgYDWAUG8NwrwUba17Z!Bfc!VKb@~AC@C1WptVMeZt~zIZDvLfq9H8^ z(A6n1QtYY&Yj<MLNXeRn%plsfmjk& zn5O$bA;3CsK3U!OPz7S{eZnh{Q-)s(!mSEQogB!IVud;ICzA+wNXe)cyH!dGv;v5X zk1M+-rf6S;Z&Id$nM5XCnyNpQyUEB^!si$YozN7o-d(0_X>8fE)&pnn=UCIVnYVG$ z5}C9tv(_?HPYO!4l%^A&O^Gv$-D!RQFrL6n8tjK93;o`8ZneB+G)N+GZ-b9A}f zXx*WdO{i11ndf%h2z4{zzx+*!?Q$6O;(hVKL=XU%0N`NMj=6Y>DXH|8Gvn}$qTG7r zWS?EOgha?Bm|);0f1>FM+f>_solOwpb>b##jt`|Rj#!bX)a_wZZ9F$vD6-5y>AUQh z{$Cvm-Y6}{z_zASpcDKK9m(=i6h*V@-`lNKUShFMJ|V1i!&hvybRkkBqGIiKAT6Y~ znN^+pjZ|T^sc_x4|LX#?+MUti50AoUnRofcpV<`5=vk0GG{*JpN8cvw3x6wUimEfT z>&T%Uq;ywP2chrgQ{@Xa|8mjq8Fy!$q57nWo6f5>yXJ59S`ZMyNgwgwCoPn;XeKS7 z6z2MUYv#m`?0~eGhj(grQ5s@U_Aj1gCFbnB!!bcu(+`GoEC_s3F;i+>^lDG?J3h`` z*NF*?NZQ2kx5IcR?-lYm(Gz93qhoFW zVPj(dt3v=1^FegNHUOsPa4`dK0l+B@U<4%##%Nh9%(&Bs#R)62Tm!9ifl`|0I6;5{ zq|$o9sIkQeU>5Usvt5C}9%Qgd-M^ktElV~5s&=cwqJA+6cKGSE78W?*O^vFKI=(Ry zb?||xBt~54yu=sL_o|gWGM@hq_+(udtSNQHn0z(zzqE?F0aL!usZ-3dfM?ogebkx(n*M|SMYHx62 z52kq46#xJI+uz%aVIG|#^v(dp1Scva+&`_HZfxQI8>}w_Fjx=@d&L0YZ`#V#@@@cL zi%u;Zg*Bum_G;M9SgH+mJ0h{38l^7TmU{6D$`PKyJhpnAr49~S6jSa%Ay0X*>yOO( zv@+r@6D^tv7w+RVvfdS2D`lhizGW&DV=6Pbn&q%nlGu9E3>~B16^Gk!G}BD=zlux3 zKE$G3e7k)nvGz}#23yzte{{cTv{)(UOO@D3`(@N{w5GSTI@vsF{;2hAFsjyQH0brw zpLou8to81Twe?i4MPpCJ_^&QQje>&mu#$2)EZkT3WFp7>^Up9GIIx8;JNoDEbW5gO zWQ`__{}zW&Ga_=|O{Q0C_aV4CfH@;q4yqELZ5pI;m%AkVgM+FP zz!Gh>j%SYQY|KYh!*s9_845f0kExCWd-Gz6XSg3x{)-{UfHkjrvKq+BP)~c&-Y>F_ zsamF9g?I`X(u-cRHl|Su6>yPSAyW~_;HUx}C{4b~u;sycbl&tL8QS^I=AgBQIPjcl z;-ZmlWqoR;fv(vzOtZ0TiAYB!W3hsmE_S8mVP>~s5j9JYkB*UZ?gzUJX!wkoW4cQ! zs5NIqQSh!M@zFUaxmAt%+%L@(`S;ar@a3a)x$EylQ!dgp7c;TeByOs%5oTo;yf5{nybDE;0wD`nU4%4mU2n|)Z-k2JlxifRO z4%6c3#ZCuHp;o^*erFb6{cq$lV>^jM@}E#(@XF{d76xQNys_UH@QaMG zMguA&nJcKC>7N*x(WaAu&Ken;Z%=dSo+>?7R6JWAJ1TKsiTX$=rLp^5z4w`ZwK(Nz ze40Yu;DlN6TXsus!EX&E^K|Yvc9yoSmV=c8ues}QB}~vRKmuUUlV?f=gB+-nz0TL! zC8iBOfq)Sp4j(9i%qa-O6FY!Mh0(`CQ6!X>!7NyTUXqJse)zm=S6uPq&a+0pa%?~x z`*ZEu1lD1jdYPuC1sJ(q(F)2aGfek96)mip6L)B9-pB^@BEw5fSKl!NukZDKF>|E^ zxm=81OD=iV(qQA+bDPT8p1umiEed;o`Er8$v)t>}xqr`-6q`6&vmPoE6@ZL8m))pG zrk9+wCDg1CwXTGn92l4;qo6_}#V63aUUQtwf3{ zJrG!+Xp9goM@X%DX zUoEL49=u^=g%Xr2@~dSh_jnU*iD<>nP=+#l#?i~w_b05C@)4!w)+<=PWmnP#k5q2G zMkZEs&*GWMmc1?J$|*jk<1YNKW?Mys4ohdYI<|*TT;D62P@M&vRf|q+^v` z^BBs}96=yH^vfX+r7u&8?LckOLds)F`W<+iG~tcYIwvwUHF@LxqcUT0(nD7C-ZWaCg#?$x<8Iw#a_!JhkGz4y#Rs$1e99wh@;l&E2cSX z(hKaeSmnFXd1Kj{Gp*UnGi%bb;Ax8YkGTla87sl@|NA+mCusR>4XFk+JQdcpHyHBY z36W40l#py!lw-o5#zPd#lSroK{%Q0Vt?}Fu7Lt|kiw=Ih9p_i{4~sk_nRN0(b}5aI zb6;Vh#%6`gw@$e$X(2-O$0xSLFuF7 zbzGhI+yAJTk-LlL2y$E$>b6EP&#Rrmjmk#Q!s#sD+BF!8P#AqU-ZXB~h>tF>w$T}_ z*buVz9xJPH`a3y+1cl#k8S_}bSD$w7+db%QSS4w{CcKfuyP13(OuW0(M`M=?zG^@+ryQqk5WvC(2jM@}E9WLVx zAN)qx4j5^5*TRevk*O|=8Zu0g1ha~;Kpfy_(nvzN@>7j;iK<3CV^V)5hu7ftA6c)VoeD8ICb`m=iwcQQ5^xFT#=(o<;s$*$NN|p63&~JX ztH&i`7m|-TGubmn$?(m|@RXuf8N9Kv8f9ooLkb*{3aQ}bnJ-h6HwwtnS2IoqD&q6S zj_Zmg6v)dCAx3l17yS`|nPR{n#&MnDrbwZR4*eif!qG=}WK=1GceF|!jZ>XJ{Yv7C zg2%b#A+co4xhfPUZLo-yIn`QmsUoBa2$UvA&XYF!E$Mx9E1Es6DnUVhA$mw|yVeAqzLx~sqXR-Xz zG_coMJpLq?Qz3IW`s_u<$fO)5MZB*+PLS;Rv%aZOF( zB*3kjH!dubJRWSq!+8O8aWFns|K_ z25D>mU;EbjmGl9IF|Qz|qGe=WxGAC*-tdwZ&<_Fx^M*>MC(-aS^R{$pZYR7jMJ+%u z!SXO*DrhUs%dt8;shNV#!aQc>&dkb{X|wCeEo-iSVf(oE`}2HwK0WXIJa6BTo-PDg z=lI@tQ!59mZu?~uX+llmK(JT$wO4O({msZNlu36UwcOHK*$1RjPX&J8ad(>hm zd$`%KFO1PRWfzHp6`W?e!yW{N6GOuZ{@*#YBOOe^XJp|Ev{I2Cydmm}uqp79}1&1~Dbc3+krbj0MH--3Q)m18Abr=N)mgaS$!lT7^<{GKr zlguCL6Yzb0pWK+_S7^hGcm7UHwoj$>u3GDk!>t=D z#vC})2e4hWX3ahj;C@g~*ElX3cm1ZQDg_=P$jO`uJKUJfCvyE5hSb#ugW4dHao1lmG5#Oz>i4cq;p0iLJIdK$L zu>=-7Hr`rwd5z7Fi_watAmC_*66s|I3~oT=_RgR-5Ps)25RZ*vTkrEi=u_$OU#dPY zD>$Q{K~O)0;T_Rkrjf62j6!yd^2&5uSdw8qcqeTQ%7FpewN)-{5;m)|0Ck?KXt3DI zxPep0$X6I0lK4NV;Jk826F*eU3PbNRb8LIjZYoWTgs9cL%)yS8DW){?6gtb?4P(}K z!wP%-qpQN1D3koKBr#4m+jI%&WGU_L@h!-cKJpW=)l|O{)l_PGo;~tPcX2wo zh9;m2NJc)Va>d}!3CS0ZMxhl3jUuu`vNk_wcSd@U>et5j@-_Jiz;SbXtuc05AXyb?{iCtZZsr~n~|1= 0 + assert package.balance >= 0 + assert package.user_id + except Exception: + # User might not have a package + pytest.skip("No package available for this account") + + +class TestAsyncAccountIntegration: + """Test async Account with real API.""" + + @pytest.mark.asyncio + async def test_async_get_credits(self, async_client): + """Test getting credits async.""" + credits = await async_client.account.get_credits() + + assert isinstance(credits, Credits) + assert isinstance(credits.credit, Decimal) + assert credits.user_id + + @pytest.mark.asyncio + async def test_async_get_package(self, async_client): + """Test getting package async.""" + try: + package = await async_client.account.get_package() + + assert isinstance(package, Package) + assert package.total >= 0 + assert package.balance >= 0 + except Exception: + pytest.skip("No package available for this account") diff --git a/tests/integration/test_asr_integration.py b/tests/integration/test_asr_integration.py new file mode 100644 index 0000000..953f7c8 --- /dev/null +++ b/tests/integration/test_asr_integration.py @@ -0,0 +1,73 @@ +"""Integration tests for ASR functionality.""" + +import pytest + +from fishaudio.types import ASRResponse, TTSConfig + + +class TestASRIntegration: + """Test ASR with real API.""" + + @pytest.fixture + def sample_audio(self, client): + """Generate sample audio for ASR testing.""" + # Generate audio from known text + config = TTSConfig(format="wav") + audio_chunks = list( + client.tts.convert(text="Hello world, this is a test.", config=config) + ) + return b"".join(audio_chunks) + + def test_basic_asr(self, client, sample_audio): + """Test basic speech-to-text transcription.""" + result = client.asr.transcribe(audio=sample_audio) + + assert isinstance(result, ASRResponse) + assert result.text # Should have transcribed text + assert result.duration > 0 + assert len(result.segments) > 0 + # Verify segments have timestamps + for segment in result.segments: + assert segment.text + assert segment.start >= 0 + assert segment.end > segment.start + + def test_asr_with_language(self, client, sample_audio): + """Test ASR with language specification.""" + result = client.asr.transcribe(audio=sample_audio, language="en") + + assert isinstance(result, ASRResponse) + assert result.text + + def test_asr_without_timestamps(self, client, sample_audio): + """Test ASR without timestamp information.""" + result = client.asr.transcribe(audio=sample_audio, include_timestamps=False) + + assert isinstance(result, ASRResponse) + assert result.text + # Segments might still be present but potentially empty or without timing + + +class TestAsyncASRIntegration: + """Test async ASR with real API.""" + + @pytest.fixture + async def async_sample_audio(self, async_client): + """Generate sample audio for async ASR testing.""" + audio_chunks = [] + config = TTSConfig(format="wav") + async for chunk in async_client.tts.convert( + text="Async test audio", config=config + ): + audio_chunks.append(chunk) + return b"".join(audio_chunks) + + @pytest.mark.asyncio + async def test_async_basic_asr(self, async_client, async_sample_audio): + """Test basic async transcription.""" + result = await async_client.asr.transcribe(audio=async_sample_audio) + + assert isinstance(result, ASRResponse) + assert result.text + assert result.duration > 0 + assert len(result.segments) > 0 diff --git a/tests/integration/test_tts_integration.py b/tests/integration/test_tts_integration.py new file mode 100644 index 0000000..1d4890f --- /dev/null +++ b/tests/integration/test_tts_integration.py @@ -0,0 +1,109 @@ +"""Integration tests for TTS functionality.""" + +from typing import get_args + +import pytest + +from fishaudio.types import Prosody, TTSConfig +from fishaudio.types.shared import Model + + +class TestTTSIntegration: + """Test TTS with real API.""" + + def test_basic_tts(self, client): + """Test basic text-to-speech generation.""" + audio_chunks = list(client.tts.convert(text="Hello, this is a test.")) + + assert len(audio_chunks) > 0 + # Verify we got audio data (check for common audio headers) + complete_audio = b"".join(audio_chunks) + assert len(complete_audio) > 1000 # Should have substantial audio data + + def test_tts_with_different_formats(self, client): + """Test TTS with different audio formats.""" + formats = ["mp3", "wav", "pcm"] + + for fmt in formats: + config = TTSConfig(format=fmt, chunk_length=100) + audio_chunks = list( + client.tts.convert(text="Testing format", config=config) + ) + assert len(audio_chunks) > 0, f"Failed for format: {fmt}" + + def test_tts_with_prosody(self, client): + """Test TTS with prosody settings.""" + prosody = Prosody(speed=1.2, volume=0.5) + config = TTSConfig(prosody=prosody) + + audio_chunks = list( + client.tts.convert(text="Testing prosody settings", config=config) + ) + + assert len(audio_chunks) > 0 + + def test_tts_with_different_backends(self, client): + """Test TTS with different backend models.""" + models = get_args(Model) + + for model in models: + try: + audio_chunks = list( + client.tts.convert(text="Testing model", model=model) + ) + assert len(audio_chunks) > 0, f"Failed for model: {model}" + except Exception as e: + # Some models might not be available + pytest.skip(f"Model {model} not available: {e}") + + def test_tts_longer_text(self, client): + """Test TTS with longer text.""" + long_text = "This is a longer piece of text for testing. " * 10 + config = TTSConfig(chunk_length=200) + + audio_chunks = list(client.tts.convert(text=long_text, config=config)) + + assert len(audio_chunks) > 0 + complete_audio = b"".join(audio_chunks) + # Longer text should produce more audio + assert len(complete_audio) > 5000 + + def test_tts_empty_text_should_fail(self, client): + """Test that empty text is handled.""" + # This might succeed with silence or fail - test behavior + try: + audio_chunks = list(client.tts.convert(text="")) + # If it succeeds, verify we get something + assert len(audio_chunks) >= 0 + except Exception: + # If it fails, that's also acceptable + pass + + +class TestAsyncTTSIntegration: + """Test async TTS with real API.""" + + @pytest.mark.asyncio + async def test_basic_async_tts(self, async_client): + """Test basic async text-to-speech generation.""" + audio_chunks = [] + async for chunk in async_client.tts.convert(text="Hello from async"): + audio_chunks.append(chunk) + + assert len(audio_chunks) > 0 + complete_audio = b"".join(audio_chunks) + assert len(complete_audio) > 1000 + + @pytest.mark.asyncio + async def test_async_tts_with_prosody(self, async_client): + """Test async TTS with prosody.""" + prosody = Prosody(speed=0.8, volume=-0.2) + config = TTSConfig(prosody=prosody) + + audio_chunks = [] + async for chunk in async_client.tts.convert( + text="Async prosody test", config=config + ): + audio_chunks.append(chunk) + + assert len(audio_chunks) > 0 diff --git a/tests/integration/test_voices_integration.py b/tests/integration/test_voices_integration.py new file mode 100644 index 0000000..2f624ed --- /dev/null +++ b/tests/integration/test_voices_integration.py @@ -0,0 +1,96 @@ +"""Integration tests for Voices functionality.""" + +import pytest + +from fishaudio.types import PaginatedResponse, Voice + + +class TestVoicesIntegration: + """Test Voices with real API.""" + + def test_list_voices(self, client): + """Test listing available voices.""" + voices = client.voices.list(page_size=10) + + assert isinstance(voices, PaginatedResponse) + assert voices.total >= 0 + assert len(voices.items) <= 10 + for voice in voices.items: + assert isinstance(voice, Voice) + assert voice.id + assert voice.title + + def test_list_voices_with_filters(self, client): + """Test listing voices with filters.""" + voices = client.voices.list(page_size=5, sort_by="created_at") + + assert len(voices.items) <= 5 + + def test_list_self_voices(self, client): + """Test listing only user's own voices.""" + voices = client.voices.list(page_size=10, self_only=True) + + assert isinstance(voices, PaginatedResponse) + # All voices should belong to this user + for voice in voices.items: + assert isinstance(voice, Voice) + + def test_get_voice_by_id(self, client): + """Test getting a specific voice by ID.""" + # First get a list to find a voice ID + voices = client.voices.list(page_size=1) + + if voices.total > 0 and len(voices.items) > 0: + voice_id = voices.items[0].id + + # Now get that specific voice + voice = client.voices.get(voice_id) + + assert isinstance(voice, Voice) + assert voice.id == voice_id + assert voice.title + else: + pytest.skip("No voices available to test get()") + + def test_pagination(self, client): + """Test voice pagination works.""" + page1 = client.voices.list(page_size=5, page_number=1) + page2 = client.voices.list(page_size=5, page_number=2) + + # Both should be valid responses + assert isinstance(page1, PaginatedResponse) + assert isinstance(page2, PaginatedResponse) + + # If there are enough voices, items should be different + if page1.total > 5: + page1_ids = {v.id for v in page1.items} + page2_ids = {v.id for v in page2.items} + # Pages should have different voices + assert page1_ids != page2_ids + + +class TestAsyncVoicesIntegration: + """Test async Voices with real API.""" + + @pytest.mark.asyncio + async def test_async_list_voices(self, async_client): + """Test listing voices async.""" + voices = await async_client.voices.list(page_size=10) + + assert isinstance(voices, PaginatedResponse) + assert voices.total >= 0 + assert len(voices.items) <= 10 + + @pytest.mark.asyncio + async def test_async_get_voice(self, async_client): + """Test getting voice by ID async.""" + voices = await async_client.voices.list(page_size=1) + + if voices.total > 0 and len(voices.items) > 0: + voice_id = voices.items[0].id + voice = await async_client.voices.get(voice_id) + + assert isinstance(voice, Voice) + assert voice.id == voice_id + else: + pytest.skip("No voices available") diff --git a/tests/test_apis.py b/tests/test_apis.py deleted file mode 100644 index 0e6d137..0000000 --- a/tests/test_apis.py +++ /dev/null @@ -1,87 +0,0 @@ -import pytest - -from fish_audio_sdk import ASRRequest, HttpCodeErr, Session, TTSRequest -from fish_audio_sdk.schemas import APICreditEntity, PackageEntity - - -def test_tts(session: Session): - buffer = bytearray() - for chunk in session.tts(TTSRequest(text="Hello, world!")): - buffer.extend(chunk) - assert len(buffer) > 0 - -def test_tts_model_1_6(session: Session): - buffer = bytearray() - for chunk in session.tts(TTSRequest(text="Hello, world!"), backend="speech-1.6"): - buffer.extend(chunk) - assert len(buffer) > 0 - - -async def test_tts_async(session: Session): - buffer = bytearray() - async for chunk in session.tts.awaitable(TTSRequest(text="Hello, world!")): - buffer.extend(chunk) - assert len(buffer) > 0 - - -def test_asr(session: Session): - buffer = bytearray() - for chunk in session.tts(TTSRequest(text="Hello, world!")): - buffer.extend(chunk) - res = session.asr(ASRRequest(audio=buffer, language="zh")) - assert res.text - - -async def test_asr_async(session: Session): - buffer = bytearray() - async for chunk in session.tts.awaitable(TTSRequest(text="Hello, world!")): - buffer.extend(chunk) - res = await session.asr.awaitable(ASRRequest(audio=buffer, language="zh")) - assert res.text - - -def test_list_models(session: Session): - res = session.list_models() - assert res.total > 0 - - -async def test_list_models_async(session: Session): - res = await session.list_models.awaitable() - assert res.total > 0 - - -def test_list_self_models(session: Session): - res = session.list_models(self_only=True) - assert res.total > 0 - - -def test_get_model(session: Session): - res = session.get_model(model_id="7f92f8afb8ec43bf81429cc1c9199cb1") - assert res.id == "7f92f8afb8ec43bf81429cc1c9199cb1" - - -def test_get_model_not_found(session: Session): - with pytest.raises(HttpCodeErr) as exc_info: - session.get_model(model_id="123") - assert exc_info.value.status == 404 - - -def test_invalid_token(session: Session): - session._apikey = "invalid" - session.init_async_client() - session.init_sync_client() - - with pytest.raises(HttpCodeErr) as exc_info: - test_tts(session) - - assert exc_info.value.status in [401, 402] - - -def test_get_api_credit(session: Session): - res = session.get_api_credit() - assert isinstance(res, APICreditEntity) - - -def test_get_package(session: Session): - res = session.get_package() - assert isinstance(res, PackageEntity) diff --git a/tests/test_websocket.py b/tests/test_websocket.py deleted file mode 100644 index bd41ba0..0000000 --- a/tests/test_websocket.py +++ /dev/null @@ -1,31 +0,0 @@ -from fish_audio_sdk import TTSRequest, WebSocketSession, AsyncWebSocketSession - -story = """ -修炼了六千三百七十九年又三月零六天后,天门因她终于洞开。 - -她凭虚站立在黄山峰顶,因天门洞开而鼓起的飓风不停拍打着她身上的黑袍,在催促她快快登仙而去;黄山间壮阔的云海也随之翻涌,为这一场天地幸事欢呼雀跃。她没有抬头看向那似隐似现、若有若无、形态万千变化的天门,只是呆立在原处自顾自地看向远方。 -""" - - -def test_tts(sync_websocket: WebSocketSession): - buffer = bytearray() - - def stream(): - for line in story.split("\n"): - yield line - - for chunk in sync_websocket.tts(TTSRequest(text=""), stream()): - buffer.extend(chunk) - assert len(buffer) > 0 - - -async def test_async_tts(async_websocket: AsyncWebSocketSession): - buffer = bytearray() - - async def stream(): - for line in story.split("\n"): - yield line - - async for chunk in async_websocket.tts(TTSRequest(text=""), stream()): - buffer.extend(chunk) - assert len(buffer) > 0 diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 0000000..3a857d9 --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1,108 @@ +"""Shared pytest fixtures for unit tests.""" + +import pytest +from unittest.mock import Mock, AsyncMock +import httpx + + +@pytest.fixture +def mock_api_key(): + """Mock API key for testing.""" + return "test_api_key_12345" + + +@pytest.fixture +def mock_base_url(): + """Mock base URL for testing.""" + return "https://api.test.fish.audio" + + +@pytest.fixture +def sample_voice_response(): + """Sample voice API response.""" + return { + "_id": "voice123", + "type": "tts", + "title": "Test Voice", + "description": "A test voice", + "cover_image": "https://example.com/image.jpg", + "train_mode": "fast", + "state": "trained", + "tags": ["test", "english"], + "samples": [], + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + "languages": ["en"], + "visibility": "private", + "lock_visibility": False, + "like_count": 0, + "mark_count": 0, + "shared_count": 0, + "task_count": 0, + "liked": False, + "marked": False, + "author": { + "_id": "author123", + "nickname": "Test Author", + "avatar": "https://example.com/avatar.jpg", + }, + } + + +@pytest.fixture +def sample_paginated_voices_response(sample_voice_response): + """Sample paginated voices API response.""" + return {"total": 1, "items": [sample_voice_response]} + + +@pytest.fixture +def sample_asr_response(): + """Sample ASR API response.""" + return { + "text": "Hello world", + "duration": 1500.0, + "segments": [ + {"text": "Hello", "start": 0.0, "end": 500.0}, + {"text": "world", "start": 500.0, "end": 1500.0}, + ], + } + + +@pytest.fixture +def sample_credits_response(): + """Sample credits API response.""" + return { + "id": "credit123", + "user_id": "user123", + "credit": "100.50", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + } + + +@pytest.fixture +def sample_package_response(): + """Sample package API response.""" + return { + "id": "package123", + "user_id": "user123", + "type": "standard", + "total": 1000, + "balance": 750, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + "finished_at": "2024-12-31T23:59:59Z", + } + + +def create_mock_response(status_code=200, json_data=None, content=b""): + """Create a mock httpx.Response.""" + response = Mock(spec=httpx.Response) + response.status_code = status_code + response.is_success = 200 <= status_code < 300 + response.json = Mock(return_value=json_data or {}) + response.text = str(json_data) if json_data else "" + response.content = content + response.iter_bytes = Mock(return_value=iter([content])) + response.aiter_bytes = AsyncMock(return_value=iter([content])) + return response diff --git a/tests/unit/test_account.py b/tests/unit/test_account.py new file mode 100644 index 0000000..97a0faf --- /dev/null +++ b/tests/unit/test_account.py @@ -0,0 +1,211 @@ +"""Tests for Account namespace client.""" + +import pytest +from unittest.mock import Mock, AsyncMock +from decimal import Decimal + +from fishaudio.core import ClientWrapper, AsyncClientWrapper, RequestOptions +from fishaudio.resources.account import AccountClient, AsyncAccountClient +from fishaudio.types import Credits, Package + + +@pytest.fixture +def mock_client_wrapper(mock_api_key): + """Mock client wrapper.""" + wrapper = Mock(spec=ClientWrapper) + wrapper.api_key = mock_api_key + return wrapper + + +@pytest.fixture +def async_mock_client_wrapper(mock_api_key): + """Mock async client wrapper.""" + wrapper = Mock(spec=AsyncClientWrapper) + wrapper.api_key = mock_api_key + return wrapper + + +@pytest.fixture +def account_client(mock_client_wrapper): + """AccountClient instance with mocked wrapper.""" + return AccountClient(mock_client_wrapper) + + +@pytest.fixture +def async_account_client(async_mock_client_wrapper): + """AsyncAccountClient instance with mocked wrapper.""" + return AsyncAccountClient(async_mock_client_wrapper) + + +class TestAccountClient: + """Test synchronous AccountClient.""" + + def test_get_credits( + self, account_client, mock_client_wrapper, sample_credits_response + ): + """Test getting API credit balance.""" + # Setup mock response + mock_response = Mock() + mock_response.json.return_value = sample_credits_response + mock_client_wrapper.request.return_value = mock_response + + # Call method + result = account_client.get_credits() + + # Verify result + assert isinstance(result, Credits) + assert result.id == "credit123" + assert result.user_id == "user123" + assert isinstance(result.credit, Decimal) + assert result.credit == Decimal("100.50") + + # Verify request + mock_client_wrapper.request.assert_called_once() + call_args = mock_client_wrapper.request.call_args + assert call_args[0][0] == "GET" + assert call_args[0][1] == "/wallet/self/api-credit" + assert call_args[1]["request_options"] is None + + def test_get_credits_with_request_options( + self, account_client, mock_client_wrapper, sample_credits_response + ): + """Test getting credits with custom request options.""" + mock_response = Mock() + mock_response.json.return_value = sample_credits_response + mock_client_wrapper.request.return_value = mock_response + + request_options = RequestOptions(timeout=60.0) + account_client.get_credits(request_options=request_options) + + # Verify request options passed through + call_args = mock_client_wrapper.request.call_args + assert call_args[1]["request_options"] == request_options + + def test_get_package( + self, account_client, mock_client_wrapper, sample_package_response + ): + """Test getting package information.""" + # Setup mock response + mock_response = Mock() + mock_response.json.return_value = sample_package_response + mock_client_wrapper.request.return_value = mock_response + + # Call method + result = account_client.get_package() + + # Verify result + assert isinstance(result, Package) + assert result.id == "package123" + assert result.user_id == "user123" + assert result.type == "standard" + assert result.total == 1000 + assert result.balance == 750 + + # Verify request + mock_client_wrapper.request.assert_called_once() + call_args = mock_client_wrapper.request.call_args + assert call_args[0][0] == "GET" + assert call_args[0][1] == "/wallet/self/package" + + def test_get_package_with_request_options( + self, account_client, mock_client_wrapper, sample_package_response + ): + """Test getting package with custom request options.""" + mock_response = Mock() + mock_response.json.return_value = sample_package_response + mock_client_wrapper.request.return_value = mock_response + + request_options = RequestOptions( + timeout=30.0, additional_headers={"X-Custom": "header"} + ) + account_client.get_package(request_options=request_options) + + # Verify request options passed through + call_args = mock_client_wrapper.request.call_args + assert call_args[1]["request_options"] == request_options + + +class TestAsyncAccountClient: + """Test asynchronous AsyncAccountClient.""" + + @pytest.mark.asyncio + async def test_get_credits( + self, async_account_client, async_mock_client_wrapper, sample_credits_response + ): + """Test getting API credit balance (async).""" + # Setup mock response + mock_response = Mock() + mock_response.json.return_value = sample_credits_response + async_mock_client_wrapper.request = AsyncMock(return_value=mock_response) + + # Call method + result = await async_account_client.get_credits() + + # Verify result + assert isinstance(result, Credits) + assert result.id == "credit123" + assert result.user_id == "user123" + assert result.credit == Decimal("100.50") + + # Verify request + async_mock_client_wrapper.request.assert_called_once() + call_args = async_mock_client_wrapper.request.call_args + assert call_args[0][0] == "GET" + assert call_args[0][1] == "/wallet/self/api-credit" + + @pytest.mark.asyncio + async def test_get_credits_with_request_options( + self, async_account_client, async_mock_client_wrapper, sample_credits_response + ): + """Test getting credits with custom request options (async).""" + mock_response = Mock() + mock_response.json.return_value = sample_credits_response + async_mock_client_wrapper.request = AsyncMock(return_value=mock_response) + + request_options = RequestOptions(timeout=60.0) + await async_account_client.get_credits(request_options=request_options) + + # Verify request options passed through + call_args = async_mock_client_wrapper.request.call_args + assert call_args[1]["request_options"] == request_options + + @pytest.mark.asyncio + async def test_get_package( + self, async_account_client, async_mock_client_wrapper, sample_package_response + ): + """Test getting package information (async).""" + # Setup mock response + mock_response = Mock() + mock_response.json.return_value = sample_package_response + async_mock_client_wrapper.request = AsyncMock(return_value=mock_response) + + # Call method + result = await async_account_client.get_package() + + # Verify result + assert isinstance(result, Package) + assert result.id == "package123" + assert result.total == 1000 + assert result.balance == 750 + + # Verify request + async_mock_client_wrapper.request.assert_called_once() + call_args = async_mock_client_wrapper.request.call_args + assert call_args[0][0] == "GET" + assert call_args[0][1] == "/wallet/self/package" + + @pytest.mark.asyncio + async def test_get_package_with_request_options( + self, async_account_client, async_mock_client_wrapper, sample_package_response + ): + """Test getting package with custom request options (async).""" + mock_response = Mock() + mock_response.json.return_value = sample_package_response + async_mock_client_wrapper.request = AsyncMock(return_value=mock_response) + + request_options = RequestOptions(timeout=30.0) + await async_account_client.get_package(request_options=request_options) + + # Verify request options passed through + call_args = async_mock_client_wrapper.request.call_args + assert call_args[1]["request_options"] == request_options diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py new file mode 100644 index 0000000..f1aa2c0 --- /dev/null +++ b/tests/unit/test_client.py @@ -0,0 +1,94 @@ +"""Tests for main client classes.""" + +import pytest +from unittest.mock import patch + +from fishaudio import FishAudio, AsyncFishAudio +from fishaudio.resources import ( + TTSClient, + AsyncTTSClient, + VoicesClient, + AsyncVoicesClient, +) + + +class TestFishAudio: + """Test sync FishAudio client.""" + + def test_init_with_api_key(self, mock_api_key): + client = FishAudio(api_key=mock_api_key) + assert client._client_wrapper.api_key == mock_api_key + + def test_init_with_env_var(self, mock_api_key): + with patch.dict("os.environ", {"FISH_AUDIO_API_KEY": mock_api_key}): + client = FishAudio() + assert client._client_wrapper.api_key == mock_api_key + + def test_init_with_custom_base_url(self, mock_api_key, mock_base_url): + client = FishAudio(api_key=mock_api_key, base_url=mock_base_url) + assert client._client_wrapper.base_url == mock_base_url + + def test_tts_lazy_loading(self, mock_api_key): + client = FishAudio(api_key=mock_api_key) + assert client._tts is None # Not loaded yet + tts = client.tts + assert client._tts is not None # Now loaded + assert isinstance(tts, TTSClient) + # Second access returns same instance + assert client.tts is tts + + def test_asr_lazy_loading(self, mock_api_key): + client = FishAudio(api_key=mock_api_key) + assert client._asr is None + asr = client.asr + assert client._asr is not None + assert client.asr is asr + + def test_voices_lazy_loading(self, mock_api_key): + client = FishAudio(api_key=mock_api_key) + assert client._voices is None + voices = client.voices + assert client._voices is not None + assert isinstance(voices, VoicesClient) + + def test_account_lazy_loading(self, mock_api_key): + client = FishAudio(api_key=mock_api_key) + assert client._account is None + _ = client.account + assert client._account is not None + + def test_context_manager(self, mock_api_key): + with FishAudio(api_key=mock_api_key) as client: + assert client._client_wrapper is not None + + +class TestAsyncFishAudio: + """Test async AsyncFishAudio client.""" + + def test_init_with_api_key(self, mock_api_key): + client = AsyncFishAudio(api_key=mock_api_key) + assert client._client_wrapper.api_key == mock_api_key + + def test_init_with_custom_base_url(self, mock_api_key, mock_base_url): + client = AsyncFishAudio(api_key=mock_api_key, base_url=mock_base_url) + assert client._client_wrapper.base_url == mock_base_url + + def test_tts_lazy_loading(self, mock_api_key): + client = AsyncFishAudio(api_key=mock_api_key) + assert client._tts is None + tts = client.tts + assert client._tts is not None + assert isinstance(tts, AsyncTTSClient) + assert client.tts is tts + + def test_voices_lazy_loading(self, mock_api_key): + client = AsyncFishAudio(api_key=mock_api_key) + assert client._voices is None + voices = client.voices + assert client._voices is not None + assert isinstance(voices, AsyncVoicesClient) + + @pytest.mark.asyncio + async def test_async_context_manager(self, mock_api_key): + async with AsyncFishAudio(api_key=mock_api_key) as client: + assert client._client_wrapper is not None diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py new file mode 100644 index 0000000..76a3611 --- /dev/null +++ b/tests/unit/test_core.py @@ -0,0 +1,106 @@ +"""Tests for core components.""" + +import pytest +from unittest.mock import patch +import httpx + +from fishaudio.core import OMIT, ClientWrapper, AsyncClientWrapper, RequestOptions + + +class TestOMIT: + """Test OMIT sentinel.""" + + def test_omit_is_falsy(self): + assert not OMIT + + def test_omit_repr(self): + assert repr(OMIT) == "OMIT" + + def test_omit_identity(self): + # OMIT should be a singleton-like value + value = OMIT + assert value is OMIT + + +class TestRequestOptions: + """Test RequestOptions class.""" + + def test_defaults(self): + options = RequestOptions() + assert options.timeout is None + assert options.max_retries is None + assert options.additional_headers == {} + assert options.additional_query_params == {} + + def test_with_values(self): + options = RequestOptions( + timeout=30.0, + max_retries=3, + additional_headers={"X-Custom": "value"}, + additional_query_params={"param": "value"}, + ) + assert options.timeout == 30.0 + assert options.max_retries == 3 + assert options.additional_headers == {"X-Custom": "value"} + assert options.additional_query_params == {"param": "value"} + + def test_get_timeout(self): + options = RequestOptions(timeout=30.0) + timeout = options.get_timeout() + assert isinstance(timeout, httpx.Timeout) + assert timeout.connect == 30.0 + + +class TestClientWrapper: + """Test sync ClientWrapper.""" + + def test_init_with_api_key(self, mock_api_key, mock_base_url): + wrapper = ClientWrapper( + api_key=mock_api_key, base_url=mock_base_url, timeout=60.0 + ) + assert wrapper.api_key == mock_api_key + assert wrapper.base_url == mock_base_url + + def test_init_without_api_key_raises(self): + with patch.dict("os.environ", {}, clear=True): + with pytest.raises(ValueError, match="API key must be provided"): + ClientWrapper() + + def test_init_with_env_var(self, mock_api_key): + with patch.dict("os.environ", {"FISH_AUDIO_API_KEY": mock_api_key}): + wrapper = ClientWrapper() + assert wrapper.api_key == mock_api_key + + def test_get_headers(self, mock_api_key): + wrapper = ClientWrapper(api_key=mock_api_key) + headers = wrapper._get_headers() + assert headers["Authorization"] == f"Bearer {mock_api_key}" + assert "User-Agent" in headers + + def test_get_headers_with_additional(self, mock_api_key): + wrapper = ClientWrapper(api_key=mock_api_key) + headers = wrapper._get_headers({"X-Custom": "value"}) + assert headers["X-Custom"] == "value" + assert headers["Authorization"] == f"Bearer {mock_api_key}" + + +class TestAsyncClientWrapper: + """Test async AsyncClientWrapper.""" + + def test_init_with_api_key(self, mock_api_key, mock_base_url): + wrapper = AsyncClientWrapper( + api_key=mock_api_key, base_url=mock_base_url, timeout=60.0 + ) + assert wrapper.api_key == mock_api_key + assert wrapper.base_url == mock_base_url + + def test_init_without_api_key_raises(self): + with patch.dict("os.environ", {}, clear=True): + with pytest.raises(ValueError, match="API key must be provided"): + AsyncClientWrapper() + + def test_get_headers(self, mock_api_key): + wrapper = AsyncClientWrapper(api_key=mock_api_key) + headers = wrapper._get_headers() + assert headers["Authorization"] == f"Bearer {mock_api_key}" + assert "User-Agent" in headers diff --git a/tests/unit/test_tts.py b/tests/unit/test_tts.py new file mode 100644 index 0000000..d41d8ca --- /dev/null +++ b/tests/unit/test_tts.py @@ -0,0 +1,354 @@ +"""Tests for TTS namespace client.""" + +import pytest +from unittest.mock import Mock, AsyncMock +import ormsgpack + +from fishaudio.core import ClientWrapper, AsyncClientWrapper, RequestOptions +from fishaudio.resources.tts import TTSClient, AsyncTTSClient +from fishaudio.types import ReferenceAudio, Prosody, TTSConfig + + +@pytest.fixture +def mock_client_wrapper(mock_api_key): + """Mock client wrapper.""" + wrapper = Mock(spec=ClientWrapper) + wrapper.api_key = mock_api_key + return wrapper + + +@pytest.fixture +def async_mock_client_wrapper(mock_api_key): + """Mock async client wrapper.""" + wrapper = Mock(spec=AsyncClientWrapper) + wrapper.api_key = mock_api_key + return wrapper + + +@pytest.fixture +def tts_client(mock_client_wrapper): + """TTSClient instance with mocked wrapper.""" + return TTSClient(mock_client_wrapper) + + +@pytest.fixture +def async_tts_client(async_mock_client_wrapper): + """AsyncTTSClient instance with mocked wrapper.""" + return AsyncTTSClient(async_mock_client_wrapper) + + +class TestTTSClient: + """Test synchronous TTSClient.""" + + def test_convert_basic(self, tts_client, mock_client_wrapper): + """Test basic TTS conversion.""" + # Setup mock response with audio chunks + mock_response = Mock() + mock_response.iter_bytes.return_value = iter([b"chunk1", b"chunk2", b"chunk3"]) + mock_client_wrapper.request.return_value = mock_response + + # Call convert + audio_chunks = list(tts_client.convert(text="Hello world")) + + # Verify we got chunks back + assert audio_chunks == [b"chunk1", b"chunk2", b"chunk3"] + + # Verify request was made correctly + mock_client_wrapper.request.assert_called_once() + call_args = mock_client_wrapper.request.call_args + + assert call_args[0][0] == "POST" + assert call_args[0][1] == "/v1/tts" + + # Check headers + assert call_args[1]["headers"]["Content-Type"] == "application/msgpack" + assert call_args[1]["headers"]["model"] == "s1" # default model + + # Check payload was msgpack encoded + assert "content" in call_args[1] + + def test_convert_with_reference_id(self, tts_client, mock_client_wrapper): + """Test TTS with reference voice ID.""" + mock_response = Mock() + mock_response.iter_bytes.return_value = iter([b"audio"]) + mock_client_wrapper.request.return_value = mock_response + + config = TTSConfig(reference_id="voice_123") + list(tts_client.convert(text="Hello", config=config)) + + # Verify reference_id in payload + call_args = mock_client_wrapper.request.call_args + payload = ormsgpack.unpackb(call_args[1]["content"]) + assert payload["reference_id"] == "voice_123" + + def test_convert_with_references(self, tts_client, mock_client_wrapper): + """Test TTS with reference audio samples.""" + mock_response = Mock() + mock_response.iter_bytes.return_value = iter([b"audio"]) + mock_client_wrapper.request.return_value = mock_response + + references = [ + ReferenceAudio(audio=b"ref_audio_1", text="Sample 1"), + ReferenceAudio(audio=b"ref_audio_2", text="Sample 2"), + ] + + config = TTSConfig(references=references) + list(tts_client.convert(text="Hello", config=config)) + + # Verify references in payload + call_args = mock_client_wrapper.request.call_args + payload = ormsgpack.unpackb(call_args[1]["content"]) + assert len(payload["references"]) == 2 + assert payload["references"][0]["text"] == "Sample 1" + assert payload["references"][1]["text"] == "Sample 2" + + def test_convert_with_different_backend(self, tts_client, mock_client_wrapper): + """Test TTS with different backend/model.""" + mock_response = Mock() + mock_response.iter_bytes.return_value = iter([b"audio"]) + mock_client_wrapper.request.return_value = mock_response + + list(tts_client.convert(text="Hello", model="s1")) + + # Verify model in headers + call_args = mock_client_wrapper.request.call_args + assert call_args[1]["headers"]["model"] == "s1" + + def test_convert_with_prosody(self, tts_client, mock_client_wrapper): + """Test TTS with prosody settings.""" + mock_response = Mock() + mock_response.iter_bytes.return_value = iter([b"audio"]) + mock_client_wrapper.request.return_value = mock_response + + prosody = Prosody(speed=1.5, volume=0.5) + config = TTSConfig(prosody=prosody) + + list(tts_client.convert(text="Hello", config=config)) + + # Verify prosody in payload + call_args = mock_client_wrapper.request.call_args + payload = ormsgpack.unpackb(call_args[1]["content"]) + assert payload["prosody"]["speed"] == 1.5 + assert payload["prosody"]["volume"] == 0.5 + + def test_convert_with_custom_parameters(self, tts_client, mock_client_wrapper): + """Test TTS with custom audio parameters.""" + mock_response = Mock() + mock_response.iter_bytes.return_value = iter([b"audio"]) + mock_client_wrapper.request.return_value = mock_response + + config = TTSConfig( + format="wav", + sample_rate=48000, + mp3_bitrate=192, + chunk_length=150, + normalize=False, + latency="normal", + top_p=0.9, + temperature=0.8, + ) + + list(tts_client.convert(text="Hello", config=config)) + + # Verify parameters in payload + call_args = mock_client_wrapper.request.call_args + payload = ormsgpack.unpackb(call_args[1]["content"]) + assert payload["format"] == "wav" + assert payload["sample_rate"] == 48000 + assert payload["mp3_bitrate"] == 192 + assert payload["chunk_length"] == 150 + assert payload["normalize"] is False + assert payload["latency"] == "normal" + assert payload["top_p"] == 0.9 + assert payload["temperature"] == 0.8 + + def test_convert_omit_parameters_not_sent(self, tts_client, mock_client_wrapper): + """Test that None/optional parameters are not included in request.""" + mock_response = Mock() + mock_response.iter_bytes.return_value = iter([b"audio"]) + mock_client_wrapper.request.return_value = mock_response + + # Call with defaults (None values should be excluded) + list(tts_client.convert(text="Hello")) + + # Verify None params not in payload + call_args = mock_client_wrapper.request.call_args + payload = ormsgpack.unpackb(call_args[1]["content"]) + + # These should NOT be in payload (are None by default) + assert "reference_id" not in payload + assert "sample_rate" not in payload + assert "prosody" not in payload + + # references is an empty list by default, so it IS included + assert payload["references"] == [] + + def test_convert_with_request_options(self, tts_client, mock_client_wrapper): + """Test TTS with custom request options.""" + mock_response = Mock() + mock_response.iter_bytes.return_value = iter([b"audio"]) + mock_client_wrapper.request.return_value = mock_response + + request_options = RequestOptions( + timeout=120.0, additional_headers={"X-Custom": "value"} + ) + + list(tts_client.convert(text="Hello", request_options=request_options)) + + # Verify request_options passed through + call_args = mock_client_wrapper.request.call_args + assert call_args[1]["request_options"] == request_options + + def test_convert_streaming_behavior(self, tts_client, mock_client_wrapper): + """Test that convert returns an iterator that can be consumed.""" + # Setup mock with multiple chunks + mock_response = Mock() + chunks = [b"chunk1", b"chunk2", b"chunk3", b""] # Empty chunk should be skipped + mock_response.iter_bytes.return_value = iter(chunks) + mock_client_wrapper.request.return_value = mock_response + + # Get iterator + audio_iterator = tts_client.convert(text="Hello") + + # Consume one chunk at a time + result = [] + for chunk in audio_iterator: + result.append(chunk) + + # Empty chunk should be filtered out + assert result == [b"chunk1", b"chunk2", b"chunk3"] + + def test_convert_empty_response(self, tts_client, mock_client_wrapper): + """Test handling of empty audio response.""" + mock_response = Mock() + mock_response.iter_bytes.return_value = iter([]) + mock_client_wrapper.request.return_value = mock_response + + audio_chunks = list(tts_client.convert(text="Hello")) + + assert audio_chunks == [] + + +class TestAsyncTTSClient: + """Test asynchronous AsyncTTSClient.""" + + @pytest.mark.asyncio + async def test_convert_basic(self, async_tts_client, async_mock_client_wrapper): + """Test basic async TTS conversion.""" + # Setup mock response + mock_response = Mock() + + async def async_iter_bytes(): + for chunk in [b"chunk1", b"chunk2", b"chunk3"]: + yield chunk + + mock_response.aiter_bytes = async_iter_bytes + async_mock_client_wrapper.request = AsyncMock(return_value=mock_response) + + # Call convert and collect chunks + audio_chunks = [] + async for chunk in async_tts_client.convert(text="Hello world"): + audio_chunks.append(chunk) + + assert audio_chunks == [b"chunk1", b"chunk2", b"chunk3"] + + # Verify request was made + async_mock_client_wrapper.request.assert_called_once() + call_args = async_mock_client_wrapper.request.call_args + + assert call_args[0][0] == "POST" + assert call_args[0][1] == "/v1/tts" + + @pytest.mark.asyncio + async def test_convert_with_reference_id( + self, async_tts_client, async_mock_client_wrapper + ): + """Test async TTS with reference voice ID.""" + mock_response = Mock() + + async def async_iter_bytes(): + yield b"audio" + + mock_response.aiter_bytes = async_iter_bytes + async_mock_client_wrapper.request = AsyncMock(return_value=mock_response) + + config = TTSConfig(reference_id="voice_123") + audio_chunks = [] + async for chunk in async_tts_client.convert(text="Hello", config=config): + audio_chunks.append(chunk) + + # Verify reference_id in payload + call_args = async_mock_client_wrapper.request.call_args + payload = ormsgpack.unpackb(call_args[1]["content"]) + assert payload["reference_id"] == "voice_123" + + @pytest.mark.asyncio + async def test_convert_with_prosody( + self, async_tts_client, async_mock_client_wrapper + ): + """Test async TTS with prosody settings.""" + mock_response = Mock() + + async def async_iter_bytes(): + yield b"audio" + + mock_response.aiter_bytes = async_iter_bytes + async_mock_client_wrapper.request = AsyncMock(return_value=mock_response) + + prosody = Prosody(speed=2.0, volume=1.0) + config = TTSConfig(prosody=prosody) + + audio_chunks = [] + async for chunk in async_tts_client.convert(text="Hello", config=config): + audio_chunks.append(chunk) + + # Verify prosody in payload + call_args = async_mock_client_wrapper.request.call_args + payload = ormsgpack.unpackb(call_args[1]["content"]) + assert payload["prosody"]["speed"] == 2.0 + assert payload["prosody"]["volume"] == 1.0 + + @pytest.mark.asyncio + async def test_convert_omit_parameters( + self, async_tts_client, async_mock_client_wrapper + ): + """Test that OMIT parameters work in async client.""" + mock_response = Mock() + + async def async_iter_bytes(): + yield b"audio" + + mock_response.aiter_bytes = async_iter_bytes + async_mock_client_wrapper.request = AsyncMock(return_value=mock_response) + + audio_chunks = [] + async for chunk in async_tts_client.convert(text="Hello"): + audio_chunks.append(chunk) + + # Verify OMIT params not in payload + call_args = async_mock_client_wrapper.request.call_args + payload = ormsgpack.unpackb(call_args[1]["content"]) + + assert "reference_id" not in payload + assert "sample_rate" not in payload + assert "prosody" not in payload + + @pytest.mark.asyncio + async def test_convert_empty_response( + self, async_tts_client, async_mock_client_wrapper + ): + """Test handling of empty async response.""" + mock_response = Mock() + + async def async_iter_bytes(): + return + yield # Make it a generator + + mock_response.aiter_bytes = async_iter_bytes + async_mock_client_wrapper.request = AsyncMock(return_value=mock_response) + + audio_chunks = [] + async for chunk in async_tts_client.convert(text="Hello"): + audio_chunks.append(chunk) + + assert audio_chunks == [] diff --git a/tests/unit/test_tts_realtime.py b/tests/unit/test_tts_realtime.py new file mode 100644 index 0000000..87548f8 --- /dev/null +++ b/tests/unit/test_tts_realtime.py @@ -0,0 +1,375 @@ +"""Tests for TTS realtime streaming.""" + +import pytest +from unittest.mock import Mock, AsyncMock, MagicMock, patch + +from fishaudio.core import ClientWrapper, AsyncClientWrapper +from fishaudio.resources.tts import TTSClient, AsyncTTSClient +from fishaudio.types import Prosody, TTSConfig, TextEvent, FlushEvent + + +@pytest.fixture +def mock_client_wrapper(mock_api_key): + """Mock client wrapper.""" + wrapper = Mock(spec=ClientWrapper) + wrapper.api_key = mock_api_key + return wrapper + + +@pytest.fixture +def async_mock_client_wrapper(mock_api_key): + """Mock async client wrapper.""" + wrapper = Mock(spec=AsyncClientWrapper) + wrapper.api_key = mock_api_key + return wrapper + + +@pytest.fixture +def tts_client(mock_client_wrapper): + """TTSClient instance with mocked wrapper.""" + return TTSClient(mock_client_wrapper) + + +@pytest.fixture +def async_tts_client(async_mock_client_wrapper): + """AsyncTTSClient instance with mocked wrapper.""" + return AsyncTTSClient(async_mock_client_wrapper) + + +class TestTTSRealtimeClient: + """Test synchronous TTSClient realtime streaming.""" + + @patch("fishaudio.resources.tts.connect_ws") + @patch("fishaudio.resources.tts.ThreadPoolExecutor") + def test_stream_websocket_basic( + self, mock_executor, mock_connect_ws, tts_client, mock_client_wrapper + ): + """Test basic WebSocket streaming.""" + # Setup mock WebSocket + mock_ws = MagicMock() + mock_ws.__enter__ = Mock(return_value=mock_ws) + mock_ws.__exit__ = Mock(return_value=None) + mock_connect_ws.return_value = mock_ws + + # Setup mock executor + mock_future = Mock() + mock_future.result.return_value = None + mock_executor_instance = Mock() + mock_executor_instance.submit.return_value = mock_future + mock_executor.return_value = mock_executor_instance + + # Mock the WebSocket client creation + mock_ws_client = Mock() + mock_client_wrapper.create_websocket_client.return_value = mock_ws_client + + # Mock the audio receiver (iter_websocket_audio) + with patch("fishaudio.resources.tts.iter_websocket_audio") as mock_receiver: + mock_receiver.return_value = iter([b"audio1", b"audio2", b"audio3"]) + + # Create text stream + text_stream = iter(["Hello ", "world", "!"]) + + # Call stream_websocket + audio_chunks = list(tts_client.stream_websocket(text_stream)) + + # Verify audio chunks + assert audio_chunks == [b"audio1", b"audio2", b"audio3"] + + # Verify WebSocket connection was created + mock_connect_ws.assert_called_once() + assert mock_connect_ws.call_args[0][0] == "/v1/tts/live" + + # Verify WebSocket client was closed + mock_ws_client.close.assert_called_once() + + @patch("fishaudio.resources.tts.connect_ws") + @patch("fishaudio.resources.tts.ThreadPoolExecutor") + def test_stream_websocket_with_config( + self, mock_executor, mock_connect_ws, tts_client, mock_client_wrapper + ): + """Test WebSocket streaming with custom config.""" + # Setup mocks + mock_ws = MagicMock() + mock_ws.__enter__ = Mock(return_value=mock_ws) + mock_ws.__exit__ = Mock(return_value=None) + mock_connect_ws.return_value = mock_ws + + mock_future = Mock() + mock_future.result.return_value = None + mock_executor_instance = Mock() + mock_executor_instance.submit.return_value = mock_future + mock_executor.return_value = mock_executor_instance + + mock_ws_client = Mock() + mock_client_wrapper.create_websocket_client.return_value = mock_ws_client + + with patch("fishaudio.resources.tts.iter_websocket_audio") as mock_receiver: + mock_receiver.return_value = iter([b"audio"]) + + # Custom config + config = TTSConfig( + reference_id="voice_123", + format="wav", + prosody=Prosody(speed=1.2, volume=0.5), + ) + + text_stream = iter(["Test"]) + list( + tts_client.stream_websocket( + text_stream, config=config, model="speech-1.5" + ) + ) + + # Verify model in headers + call_args = mock_connect_ws.call_args + assert call_args[1]["headers"]["model"] == "speech-1.5" + + # Verify config was sent in StartEvent (would be first send_bytes call) + # The sender runs in a thread, so we can't easily inspect the exact calls + mock_ws_client.close.assert_called_once() + + @patch("fishaudio.resources.tts.connect_ws") + @patch("fishaudio.resources.tts.ThreadPoolExecutor") + def test_stream_websocket_with_text_events( + self, mock_executor, mock_connect_ws, tts_client, mock_client_wrapper + ): + """Test WebSocket streaming with TextEvent and FlushEvent.""" + # Setup mocks + mock_ws = MagicMock() + mock_ws.__enter__ = Mock(return_value=mock_ws) + mock_ws.__exit__ = Mock(return_value=None) + mock_connect_ws.return_value = mock_ws + + mock_future = Mock() + mock_future.result.return_value = None + mock_executor_instance = Mock() + mock_executor_instance.submit.return_value = mock_future + mock_executor.return_value = mock_executor_instance + + mock_ws_client = Mock() + mock_client_wrapper.create_websocket_client.return_value = mock_ws_client + + with patch("fishaudio.resources.tts.iter_websocket_audio") as mock_receiver: + mock_receiver.return_value = iter([b"audio1", b"audio2"]) + + # Mix of strings and events + text_stream = iter( + [ + "Hello ", + TextEvent(text="world"), + FlushEvent(), + ] + ) + + audio_chunks = list(tts_client.stream_websocket(text_stream)) + + # Should receive audio + assert len(audio_chunks) == 2 + assert audio_chunks == [b"audio1", b"audio2"] + + @patch("fishaudio.resources.tts.connect_ws") + @patch("fishaudio.resources.tts.ThreadPoolExecutor") + def test_stream_websocket_max_workers( + self, mock_executor, mock_connect_ws, tts_client, mock_client_wrapper + ): + """Test WebSocket streaming with custom max_workers.""" + # Setup mocks + mock_ws = MagicMock() + mock_ws.__enter__ = Mock(return_value=mock_ws) + mock_ws.__exit__ = Mock(return_value=None) + mock_connect_ws.return_value = mock_ws + + mock_future = Mock() + mock_future.result.return_value = None + mock_executor_instance = Mock() + mock_executor_instance.submit.return_value = mock_future + mock_executor.return_value = mock_executor_instance + + mock_ws_client = Mock() + mock_client_wrapper.create_websocket_client.return_value = mock_ws_client + + with patch("fishaudio.resources.tts.iter_websocket_audio") as mock_receiver: + mock_receiver.return_value = iter([b"audio"]) + + text_stream = iter(["Test"]) + list(tts_client.stream_websocket(text_stream, max_workers=5)) + + # Verify ThreadPoolExecutor was created with max_workers=5 + mock_executor.assert_called_once_with(max_workers=5) + + +class TestAsyncTTSRealtimeClient: + """Test asynchronous AsyncTTSClient realtime streaming.""" + + @pytest.mark.asyncio + @patch("fishaudio.resources.tts.aconnect_ws") + async def test_stream_websocket_basic( + self, mock_aconnect_ws, async_tts_client, async_mock_client_wrapper + ): + """Test basic async WebSocket streaming.""" + # Setup mock WebSocket + mock_ws = MagicMock() + mock_ws.__aenter__ = AsyncMock(return_value=mock_ws) + mock_ws.__aexit__ = AsyncMock(return_value=None) + mock_ws.send_bytes = AsyncMock() + mock_aconnect_ws.return_value = mock_ws + + # Mock the WebSocket client creation + mock_ws_client = Mock() + mock_ws_client.aclose = AsyncMock() + async_mock_client_wrapper.create_websocket_client.return_value = mock_ws_client + + # Mock the audio receiver + async def mock_audio_receiver(ws): + for chunk in [b"audio1", b"audio2", b"audio3"]: + yield chunk + + with patch( + "fishaudio.resources.tts.aiter_websocket_audio", + return_value=mock_audio_receiver(mock_ws), + ): + # Create async text stream + async def text_stream(): + for text in ["Hello ", "world", "!"]: + yield text + + # Call stream_websocket + audio_chunks = [] + async for chunk in async_tts_client.stream_websocket(text_stream()): + audio_chunks.append(chunk) + + # Verify audio chunks + assert audio_chunks == [b"audio1", b"audio2", b"audio3"] + + # Verify WebSocket connection was created + mock_aconnect_ws.assert_called_once() + assert mock_aconnect_ws.call_args[0][0] == "/v1/tts/live" + + # Verify WebSocket client was closed + mock_ws_client.aclose.assert_called_once() + + @pytest.mark.asyncio + @patch("fishaudio.resources.tts.aconnect_ws") + async def test_stream_websocket_with_config( + self, mock_aconnect_ws, async_tts_client, async_mock_client_wrapper + ): + """Test async WebSocket streaming with custom config.""" + # Setup mocks + mock_ws = MagicMock() + mock_ws.__aenter__ = AsyncMock(return_value=mock_ws) + mock_ws.__aexit__ = AsyncMock(return_value=None) + mock_ws.send_bytes = AsyncMock() + mock_aconnect_ws.return_value = mock_ws + + mock_ws_client = Mock() + mock_ws_client.aclose = AsyncMock() + async_mock_client_wrapper.create_websocket_client.return_value = mock_ws_client + + async def mock_audio_receiver(ws): + yield b"audio" + + with patch( + "fishaudio.resources.tts.aiter_websocket_audio", + return_value=mock_audio_receiver(mock_ws), + ): + # Custom config + config = TTSConfig( + reference_id="voice_123", + format="wav", + prosody=Prosody(speed=1.2, volume=0.5), + ) + + async def text_stream(): + yield "Test" + + audio_chunks = [] + async for chunk in async_tts_client.stream_websocket( + text_stream(), config=config, model="speech-1.6" + ): + audio_chunks.append(chunk) + + # Verify model in headers + call_args = mock_aconnect_ws.call_args + assert call_args[1]["headers"]["model"] == "speech-1.6" + + # Verify client was closed + mock_ws_client.aclose.assert_called_once() + + @pytest.mark.asyncio + @patch("fishaudio.resources.tts.aconnect_ws") + async def test_stream_websocket_with_text_events( + self, mock_aconnect_ws, async_tts_client, async_mock_client_wrapper + ): + """Test async WebSocket streaming with TextEvent and FlushEvent.""" + # Setup mocks + mock_ws = MagicMock() + mock_ws.__aenter__ = AsyncMock(return_value=mock_ws) + mock_ws.__aexit__ = AsyncMock(return_value=None) + mock_ws.send_bytes = AsyncMock() + mock_aconnect_ws.return_value = mock_ws + + mock_ws_client = Mock() + mock_ws_client.aclose = AsyncMock() + async_mock_client_wrapper.create_websocket_client.return_value = mock_ws_client + + async def mock_audio_receiver(ws): + yield b"audio1" + yield b"audio2" + + with patch( + "fishaudio.resources.tts.aiter_websocket_audio", + return_value=mock_audio_receiver(mock_ws), + ): + # Mix of strings and events + async def text_stream(): + yield "Hello " + yield TextEvent(text="world") + yield FlushEvent() + + audio_chunks = [] + async for chunk in async_tts_client.stream_websocket(text_stream()): + audio_chunks.append(chunk) + + # Should receive audio + assert len(audio_chunks) == 2 + assert audio_chunks == [b"audio1", b"audio2"] + + @pytest.mark.asyncio + @patch("fishaudio.resources.tts.aconnect_ws") + async def test_stream_websocket_empty_stream( + self, mock_aconnect_ws, async_tts_client, async_mock_client_wrapper + ): + """Test async WebSocket streaming with empty text stream.""" + # Setup mocks + mock_ws = MagicMock() + mock_ws.__aenter__ = AsyncMock(return_value=mock_ws) + mock_ws.__aexit__ = AsyncMock(return_value=None) + mock_ws.send_bytes = AsyncMock() + mock_aconnect_ws.return_value = mock_ws + + mock_ws_client = Mock() + mock_ws_client.aclose = AsyncMock() + async_mock_client_wrapper.create_websocket_client.return_value = mock_ws_client + + async def mock_audio_receiver(ws): + return + yield # Make it a generator + + with patch( + "fishaudio.resources.tts.aiter_websocket_audio", + return_value=mock_audio_receiver(mock_ws), + ): + + async def text_stream(): + return + yield # Empty stream + + audio_chunks = [] + async for chunk in async_tts_client.stream_websocket(text_stream()): + audio_chunks.append(chunk) + + # Should have no audio + assert audio_chunks == [] + + # Verify client was still closed + mock_ws_client.aclose.assert_called_once() diff --git a/tests/unit/test_types.py b/tests/unit/test_types.py new file mode 100644 index 0000000..e9dee3d --- /dev/null +++ b/tests/unit/test_types.py @@ -0,0 +1,98 @@ +"""Tests for type definitions.""" + +from decimal import Decimal + +from fishaudio.types import ( + Voice, + PaginatedResponse, + ASRResponse, + ASRSegment, + Credits, + Package, + ReferenceAudio, + Prosody, +) + + +class TestVoice: + """Test Voice model.""" + + def test_voice_from_dict(self, sample_voice_response): + voice = Voice.model_validate(sample_voice_response) + assert voice.id == "voice123" + assert voice.title == "Test Voice" + assert voice.type == "tts" + assert voice.state == "trained" + assert len(voice.tags) == 2 + assert voice.author.id == "author123" + + def test_voice_alias_mapping(self, sample_voice_response): + # Test that _id gets mapped to id + voice = Voice.model_validate(sample_voice_response) + assert voice.id == sample_voice_response["_id"] + + +class TestPaginatedResponse: + """Test PaginatedResponse model.""" + + def test_paginated_voices(self, sample_paginated_voices_response): + paginated = PaginatedResponse[Voice].model_validate( + sample_paginated_voices_response + ) + assert paginated.total == 1 + assert len(paginated.items) == 1 + assert isinstance(paginated.items[0], Voice) + assert paginated.items[0].id == "voice123" + + +class TestASRTypes: + """Test ASR-related types.""" + + def test_asr_segment(self): + segment = ASRSegment(text="Hello", start=0.0, end=500.0) + assert segment.text == "Hello" + assert segment.start == 0.0 + assert segment.end == 500.0 + + def test_asr_response(self, sample_asr_response): + response = ASRResponse.model_validate(sample_asr_response) + assert response.text == "Hello world" + assert response.duration == 1500.0 + assert len(response.segments) == 2 + assert isinstance(response.segments[0], ASRSegment) + + +class TestAccountTypes: + """Test account-related types.""" + + def test_credits(self, sample_credits_response): + credits = Credits.model_validate(sample_credits_response) + assert credits.id == "credit123" + assert credits.user_id == "user123" + assert isinstance(credits.credit, Decimal) + assert credits.credit == Decimal("100.50") + + def test_package(self, sample_package_response): + package = Package.model_validate(sample_package_response) + assert package.id == "package123" + assert package.total == 1000 + assert package.balance == 750 + + +class TestTTSTypes: + """Test TTS-related types.""" + + def test_reference_audio(self): + ref = ReferenceAudio(audio=b"audio_data", text="Sample text") + assert ref.audio == b"audio_data" + assert ref.text == "Sample text" + + def test_prosody_defaults(self): + prosody = Prosody() + assert prosody.speed == 1.0 + assert prosody.volume == 0.0 + + def test_prosody_custom(self): + prosody = Prosody(speed=1.5, volume=0.5) + assert prosody.speed == 1.5 + assert prosody.volume == 0.5 diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py new file mode 100644 index 0000000..2c0f30e --- /dev/null +++ b/tests/unit/test_utils.py @@ -0,0 +1,142 @@ +"""Tests for utility functions.""" + +import pytest +from unittest.mock import Mock, patch, mock_open +import subprocess + +from fishaudio.utils import play, save, stream +from fishaudio.exceptions import DependencyError + + +class TestSave: + """Test save() function.""" + + def test_save_bytes(self): + """Test saving bytes to file.""" + audio = b"fake audio data" + + with patch("builtins.open", mock_open()) as m: + save(audio, "output.mp3") + + m.assert_called_once_with("output.mp3", "wb") + m().write.assert_called_once_with(audio) + + def test_save_iterator(self): + """Test saving iterator to file.""" + audio = iter([b"chunk1", b"chunk2", b"chunk3"]) + + with patch("builtins.open", mock_open()) as m: + save(audio, "output.mp3") + + m.assert_called_once_with("output.mp3", "wb") + # Should consolidate chunks + m().write.assert_called_once_with(b"chunk1chunk2chunk3") + + +class TestPlay: + """Test play() function.""" + + def test_play_with_ffmpeg(self): + """Test playing audio with ffplay.""" + # Mock subprocess.run to simulate which and ffplay succeeding + with patch("subprocess.run") as mock_run: + mock_run.return_value = Mock(returncode=0) + + audio = b"fake audio" + play(audio, use_ffmpeg=True) + + # Should call both which (to check) and ffplay + assert mock_run.call_count >= 1 + # At least one call should involve ffplay + calls_str = str(mock_run.call_args_list) + assert "ffplay" in calls_str or "which" in calls_str + + def test_play_ffmpeg_not_installed(self): + """Test error when ffplay not installed.""" + with patch( + "subprocess.run", side_effect=[subprocess.CalledProcessError(1, "which")] + ): + with pytest.raises(DependencyError) as exc_info: + play(b"audio", use_ffmpeg=True) + + assert "ffplay" in str(exc_info.value) + + def test_play_sounddevice_mode(self): + """Test using sounddevice directly.""" + # Since we can't easily test sounddevice without installing it, + # just test that the error is raised when it's not available + with patch( + "subprocess.run", side_effect=[subprocess.CalledProcessError(1, "which")] + ): + with pytest.raises(DependencyError) as exc_info: + play(b"audio", use_ffmpeg=False) + + # Should mention sounddevice in the error + assert "sounddevice" in str(exc_info.value) or "fishaudio[utils]" in str( + exc_info.value + ) + + def test_play_notebook_not_installed(self): + """Test error when IPython not available.""" + # Temporarily modify sys.modules to simulate missing IPython + import sys + + original_modules = sys.modules.copy() + try: + # Remove IPython from modules if present + sys.modules.pop("IPython", None) + sys.modules.pop("IPython.display", None) + + with patch.dict("sys.modules", {"IPython": None, "IPython.display": None}): + with pytest.raises(DependencyError) as exc_info: + play(b"audio", notebook=True) + + assert "IPython" in str(exc_info.value) + finally: + # Restore original modules + sys.modules.update(original_modules) + + +class TestStream: + """Test stream() function.""" + + @patch("subprocess.Popen") + @patch("subprocess.run") + def test_stream_audio(self, mock_run, mock_popen): + """Test streaming audio with mpv.""" + # Mock which command to succeed + mock_run.return_value = Mock(returncode=0) + + # Mock mpv process + mock_process = Mock() + mock_process.stdin = Mock() + mock_popen.return_value = mock_process + + # Stream chunks + audio_chunks = iter([b"chunk1", b"chunk2", b"chunk3"]) + result = stream(audio_chunks) + + # Should launch mpv + mock_popen.assert_called_once() + call_args = mock_popen.call_args + assert "mpv" in call_args[0][0][0] + + # Should write chunks + assert mock_process.stdin.write.call_count == 3 + + # Should return complete audio + assert result == b"chunk1chunk2chunk3" + + # Should cleanup + mock_process.stdin.close.assert_called_once() + mock_process.wait.assert_called_once() + + def test_stream_mpv_not_installed(self): + """Test error when mpv not installed.""" + with patch( + "subprocess.run", side_effect=[subprocess.CalledProcessError(1, "which")] + ): + with pytest.raises(DependencyError) as exc_info: + stream(iter([b"audio"])) + + assert "mpv" in str(exc_info.value) diff --git a/tests/unit/test_voices.py b/tests/unit/test_voices.py new file mode 100644 index 0000000..8a1fb5d --- /dev/null +++ b/tests/unit/test_voices.py @@ -0,0 +1,112 @@ +"""Tests for voices namespace client.""" + +import pytest +from unittest.mock import Mock + +from fishaudio.core import ClientWrapper +from fishaudio.resources.voices import VoicesClient +from fishaudio.types import Voice, PaginatedResponse + + +@pytest.fixture +def mock_client_wrapper(mock_api_key): + """Mock client wrapper.""" + wrapper = Mock(spec=ClientWrapper) + wrapper.api_key = mock_api_key + return wrapper + + +@pytest.fixture +def voices_client(mock_client_wrapper): + """VoicesClient instance with mocked wrapper.""" + return VoicesClient(mock_client_wrapper) + + +class TestVoicesClient: + """Test VoicesClient.""" + + def test_list_voices( + self, voices_client, mock_client_wrapper, sample_paginated_voices_response + ): + # Setup mock response + mock_response = Mock() + mock_response.json.return_value = sample_paginated_voices_response + mock_client_wrapper.request.return_value = mock_response + + # Call method + result = voices_client.list(page_size=10, page_number=1) + + # Verify + assert isinstance(result, PaginatedResponse) + assert result.total == 1 + assert len(result.items) == 1 + assert isinstance(result.items[0], Voice) + + # Verify request was made correctly + mock_client_wrapper.request.assert_called_once() + call_args = mock_client_wrapper.request.call_args + assert call_args[0][0] == "GET" + assert call_args[0][1] == "/model" + + def test_get_voice(self, voices_client, mock_client_wrapper, sample_voice_response): + # Setup mock response + mock_response = Mock() + mock_response.json.return_value = sample_voice_response + mock_client_wrapper.request.return_value = mock_response + + # Call method + result = voices_client.get("voice123") + + # Verify + assert isinstance(result, Voice) + assert result.id == "voice123" + assert result.title == "Test Voice" + + # Verify request + mock_client_wrapper.request.assert_called_once() + call_args = mock_client_wrapper.request.call_args + assert call_args[0][0] == "GET" + assert call_args[0][1] == "/model/voice123" + + def test_delete_voice(self, voices_client, mock_client_wrapper): + # Setup mock response + mock_response = Mock() + mock_client_wrapper.request.return_value = mock_response + + # Call method + voices_client.delete("voice123") + + # Verify request + mock_client_wrapper.request.assert_called_once() + call_args = mock_client_wrapper.request.call_args + assert call_args[0][0] == "DELETE" + assert call_args[0][1] == "/model/voice123" + + def test_create_voice( + self, voices_client, mock_client_wrapper, sample_voice_response + ): + # Setup mock response + mock_response = Mock() + mock_response.json.return_value = sample_voice_response + mock_client_wrapper.request.return_value = mock_response + + # Call method + result = voices_client.create( + title="New Voice", + voices=[b"audio1", b"audio2"], + description="Test description", + tags=["test"], + ) + + # Verify + assert isinstance(result, Voice) + assert result.title == "Test Voice" + + # Verify request + mock_client_wrapper.request.assert_called_once() + call_args = mock_client_wrapper.request.call_args + assert call_args[0][0] == "POST" + assert call_args[0][1] == "/model" + # Check that data and files were passed + assert "data" in call_args[1] + assert "files" in call_args[1] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..02d8590 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1336 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] + +[[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.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288, upload-time = "2025-09-08T23:23:48.404Z" }, + { url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509, upload-time = "2025-09-08T23:23:49.73Z" }, + { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" }, + { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" }, + { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" }, + { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" }, + { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" }, +] + +[[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 = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, + { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, + { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, + { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, + { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version < '3.10'" }, +] + +[[package]] +name = "coverage" +version = "7.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050", size = 811905, upload-time = "2025-10-15T15:15:08.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/95/c49df0aceb5507a80b9fe5172d3d39bf23f05be40c23c8d77d556df96cec/coverage-7.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb53f1e8adeeb2e78962bade0c08bfdc461853c7969706ed901821e009b35e31", size = 215800, upload-time = "2025-10-15T15:12:19.824Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c6/7bb46ce01ed634fff1d7bb53a54049f539971862cc388b304ff3c51b4f66/coverage-7.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9a03ec6cb9f40a5c360f138b88266fd8f58408d71e89f536b4f91d85721d075", size = 216198, upload-time = "2025-10-15T15:12:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/94/b2/75d9d8fbf2900268aca5de29cd0a0fe671b0f69ef88be16767cc3c828b85/coverage-7.11.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d7f0616c557cbc3d1c2090334eddcbb70e1ae3a40b07222d62b3aa47f608fab", size = 242953, upload-time = "2025-10-15T15:12:24.139Z" }, + { url = "https://files.pythonhosted.org/packages/65/ac/acaa984c18f440170525a8743eb4b6c960ace2dbad80dc22056a437fc3c6/coverage-7.11.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e44a86a47bbdf83b0a3ea4d7df5410d6b1a0de984fbd805fa5101f3624b9abe0", size = 244766, upload-time = "2025-10-15T15:12:25.974Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0d/938d0bff76dfa4a6b228c3fc4b3e1c0e2ad4aa6200c141fcda2bd1170227/coverage-7.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:596763d2f9a0ee7eec6e643e29660def2eef297e1de0d334c78c08706f1cb785", size = 246625, upload-time = "2025-10-15T15:12:27.387Z" }, + { url = "https://files.pythonhosted.org/packages/38/54/8f5f5e84bfa268df98f46b2cb396b1009734cfb1e5d6adb663d284893b32/coverage-7.11.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef55537ff511b5e0a43edb4c50a7bf7ba1c3eea20b4f49b1490f1e8e0e42c591", size = 243568, upload-time = "2025-10-15T15:12:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/68/30/8ba337c2877fe3f2e1af0ed7ff4be0c0c4aca44d6f4007040f3ca2255e99/coverage-7.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cbabd8f4d0d3dc571d77ae5bdbfa6afe5061e679a9d74b6797c48d143307088", size = 244665, upload-time = "2025-10-15T15:12:30.297Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fb/c6f1d6d9a665536b7dde2333346f0cc41dc6a60bd1ffc10cd5c33e7eb000/coverage-7.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e24045453384e0ae2a587d562df2a04d852672eb63051d16096d3f08aa4c7c2f", size = 242681, upload-time = "2025-10-15T15:12:32.326Z" }, + { url = "https://files.pythonhosted.org/packages/be/38/1b532319af5f991fa153c20373291dc65c2bf532af7dbcffdeef745c8f79/coverage-7.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:7161edd3426c8d19bdccde7d49e6f27f748f3c31cc350c5de7c633fea445d866", size = 242912, upload-time = "2025-10-15T15:12:34.079Z" }, + { url = "https://files.pythonhosted.org/packages/67/3d/f39331c60ef6050d2a861dc1b514fa78f85f792820b68e8c04196ad733d6/coverage-7.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d4ed4de17e692ba6415b0587bc7f12bc80915031fc9db46a23ce70fc88c9841", size = 243559, upload-time = "2025-10-15T15:12:35.809Z" }, + { url = "https://files.pythonhosted.org/packages/4b/55/cb7c9df9d0495036ce582a8a2958d50c23cd73f84a23284bc23bd4711a6f/coverage-7.11.0-cp310-cp310-win32.whl", hash = "sha256:765c0bc8fe46f48e341ef737c91c715bd2a53a12792592296a095f0c237e09cf", size = 218266, upload-time = "2025-10-15T15:12:37.429Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/b79cb275fa7bd0208767f89d57a1b5f6ba830813875738599741b97c2e04/coverage-7.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:24d6f3128f1b2d20d84b24f4074475457faedc3d4613a7e66b5e769939c7d969", size = 219169, upload-time = "2025-10-15T15:12:39.25Z" }, + { url = "https://files.pythonhosted.org/packages/49/3a/ee1074c15c408ddddddb1db7dd904f6b81bc524e01f5a1c5920e13dbde23/coverage-7.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d58ecaa865c5b9fa56e35efc51d1014d4c0d22838815b9fce57a27dd9576847", size = 215912, upload-time = "2025-10-15T15:12:40.665Z" }, + { url = "https://files.pythonhosted.org/packages/70/c4/9f44bebe5cb15f31608597b037d78799cc5f450044465bcd1ae8cb222fe1/coverage-7.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b679e171f1c104a5668550ada700e3c4937110dbdd153b7ef9055c4f1a1ee3cc", size = 216310, upload-time = "2025-10-15T15:12:42.461Z" }, + { url = "https://files.pythonhosted.org/packages/42/01/5e06077cfef92d8af926bdd86b84fb28bf9bc6ad27343d68be9b501d89f2/coverage-7.11.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca61691ba8c5b6797deb221a0d09d7470364733ea9c69425a640f1f01b7c5bf0", size = 246706, upload-time = "2025-10-15T15:12:44.001Z" }, + { url = "https://files.pythonhosted.org/packages/40/b8/7a3f1f33b35cc4a6c37e759137533119560d06c0cc14753d1a803be0cd4a/coverage-7.11.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aef1747ede4bd8ca9cfc04cc3011516500c6891f1b33a94add3253f6f876b7b7", size = 248634, upload-time = "2025-10-15T15:12:45.768Z" }, + { url = "https://files.pythonhosted.org/packages/7a/41/7f987eb33de386bc4c665ab0bf98d15fcf203369d6aacae74f5dd8ec489a/coverage-7.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1839d08406e4cba2953dcc0ffb312252f14d7c4c96919f70167611f4dee2623", size = 250741, upload-time = "2025-10-15T15:12:47.222Z" }, + { url = "https://files.pythonhosted.org/packages/23/c1/a4e0ca6a4e83069fb8216b49b30a7352061ca0cb38654bd2dc96b7b3b7da/coverage-7.11.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e0eb0a2dcc62478eb5b4cbb80b97bdee852d7e280b90e81f11b407d0b81c4287", size = 246837, upload-time = "2025-10-15T15:12:48.904Z" }, + { url = "https://files.pythonhosted.org/packages/5d/03/ced062a17f7c38b4728ff76c3acb40d8465634b20b4833cdb3cc3a74e115/coverage-7.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fbea96343b53f65d5351d8fd3b34fd415a2670d7c300b06d3e14a5af4f552", size = 248429, upload-time = "2025-10-15T15:12:50.73Z" }, + { url = "https://files.pythonhosted.org/packages/97/af/a7c6f194bb8c5a2705ae019036b8fe7f49ea818d638eedb15fdb7bed227c/coverage-7.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:214b622259dd0cf435f10241f1333d32caa64dbc27f8790ab693428a141723de", size = 246490, upload-time = "2025-10-15T15:12:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c3/aab4df02b04a8fde79068c3c41ad7a622b0ef2b12e1ed154da986a727c3f/coverage-7.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:258d9967520cca899695d4eb7ea38be03f06951d6ca2f21fb48b1235f791e601", size = 246208, upload-time = "2025-10-15T15:12:54.586Z" }, + { url = "https://files.pythonhosted.org/packages/30/d8/e282ec19cd658238d60ed404f99ef2e45eed52e81b866ab1518c0d4163cf/coverage-7.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cf9e6ff4ca908ca15c157c409d608da77a56a09877b97c889b98fb2c32b6465e", size = 247126, upload-time = "2025-10-15T15:12:56.485Z" }, + { url = "https://files.pythonhosted.org/packages/d1/17/a635fa07fac23adb1a5451ec756216768c2767efaed2e4331710342a3399/coverage-7.11.0-cp311-cp311-win32.whl", hash = "sha256:fcc15fc462707b0680cff6242c48625da7f9a16a28a41bb8fd7a4280920e676c", size = 218314, upload-time = "2025-10-15T15:12:58.365Z" }, + { url = "https://files.pythonhosted.org/packages/2a/29/2ac1dfcdd4ab9a70026edc8d715ece9b4be9a1653075c658ee6f271f394d/coverage-7.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:865965bf955d92790f1facd64fe7ff73551bd2c1e7e6b26443934e9701ba30b9", size = 219203, upload-time = "2025-10-15T15:12:59.902Z" }, + { url = "https://files.pythonhosted.org/packages/03/21/5ce8b3a0133179115af4c041abf2ee652395837cb896614beb8ce8ddcfd9/coverage-7.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:5693e57a065760dcbeb292d60cc4d0231a6d4b6b6f6a3191561e1d5e8820b745", size = 217879, upload-time = "2025-10-15T15:13:01.35Z" }, + { url = "https://files.pythonhosted.org/packages/c4/db/86f6906a7c7edc1a52b2c6682d6dd9be775d73c0dfe2b84f8923dfea5784/coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1", size = 216098, upload-time = "2025-10-15T15:13:02.916Z" }, + { url = "https://files.pythonhosted.org/packages/21/54/e7b26157048c7ba555596aad8569ff903d6cd67867d41b75287323678ede/coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007", size = 216331, upload-time = "2025-10-15T15:13:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/b9/19/1ce6bf444f858b83a733171306134a0544eaddf1ca8851ede6540a55b2ad/coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46", size = 247825, upload-time = "2025-10-15T15:13:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/71/0b/d3bcbbc259fcced5fb67c5d78f6e7ee965f49760c14afd931e9e663a83b2/coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893", size = 250573, upload-time = "2025-10-15T15:13:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/58/8d/b0ff3641a320abb047258d36ed1c21d16be33beed4152628331a1baf3365/coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115", size = 251706, upload-time = "2025-10-15T15:13:09.4Z" }, + { url = "https://files.pythonhosted.org/packages/59/c8/5a586fe8c7b0458053d9c687f5cff515a74b66c85931f7fe17a1c958b4ac/coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415", size = 248221, upload-time = "2025-10-15T15:13:10.964Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ff/3a25e3132804ba44cfa9a778cdf2b73dbbe63ef4b0945e39602fc896ba52/coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186", size = 249624, upload-time = "2025-10-15T15:13:12.5Z" }, + { url = "https://files.pythonhosted.org/packages/c5/12/ff10c8ce3895e1b17a73485ea79ebc1896a9e466a9d0f4aef63e0d17b718/coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d", size = 247744, upload-time = "2025-10-15T15:13:14.554Z" }, + { url = "https://files.pythonhosted.org/packages/16/02/d500b91f5471b2975947e0629b8980e5e90786fe316b6d7299852c1d793d/coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d", size = 247325, upload-time = "2025-10-15T15:13:16.438Z" }, + { url = "https://files.pythonhosted.org/packages/77/11/dee0284fbbd9cd64cfce806b827452c6df3f100d9e66188e82dfe771d4af/coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2", size = 249180, upload-time = "2025-10-15T15:13:17.959Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/cdf1def928f0a150a057cab03286774e73e29c2395f0d30ce3d9e9f8e697/coverage-7.11.0-cp312-cp312-win32.whl", hash = "sha256:037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5", size = 218479, upload-time = "2025-10-15T15:13:19.608Z" }, + { url = "https://files.pythonhosted.org/packages/ff/55/e5884d55e031da9c15b94b90a23beccc9d6beee65e9835cd6da0a79e4f3a/coverage-7.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0", size = 219290, upload-time = "2025-10-15T15:13:21.593Z" }, + { url = "https://files.pythonhosted.org/packages/23/a8/faa930cfc71c1d16bc78f9a19bb73700464f9c331d9e547bfbc1dbd3a108/coverage-7.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad", size = 217924, upload-time = "2025-10-15T15:13:23.39Z" }, + { url = "https://files.pythonhosted.org/packages/60/7f/85e4dfe65e400645464b25c036a26ac226cf3a69d4a50c3934c532491cdd/coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1", size = 216129, upload-time = "2025-10-15T15:13:25.371Z" }, + { url = "https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be", size = 216380, upload-time = "2025-10-15T15:13:26.976Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f5/3da9cc9596708273385189289c0e4d8197d37a386bdf17619013554b3447/coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d", size = 247375, upload-time = "2025-10-15T15:13:28.923Z" }, + { url = "https://files.pythonhosted.org/packages/65/6c/f7f59c342359a235559d2bc76b0c73cfc4bac7d61bb0df210965cb1ecffd/coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82", size = 249978, upload-time = "2025-10-15T15:13:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8c/042dede2e23525e863bf1ccd2b92689692a148d8b5fd37c37899ba882645/coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52", size = 251253, upload-time = "2025-10-15T15:13:32.174Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/3c58df67bfa809a7bddd786356d9c5283e45d693edb5f3f55d0986dd905a/coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b", size = 247591, upload-time = "2025-10-15T15:13:34.147Z" }, + { url = "https://files.pythonhosted.org/packages/26/5b/c7f32efd862ee0477a18c41e4761305de6ddd2d49cdeda0c1116227570fd/coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4", size = 249411, upload-time = "2025-10-15T15:13:38.425Z" }, + { url = "https://files.pythonhosted.org/packages/76/b5/78cb4f1e86c1611431c990423ec0768122905b03837e1b4c6a6f388a858b/coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd", size = 247303, upload-time = "2025-10-15T15:13:40.464Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/23c753a8641a330f45f221286e707c427e46d0ffd1719b080cedc984ec40/coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc", size = 247157, upload-time = "2025-10-15T15:13:42.087Z" }, + { url = "https://files.pythonhosted.org/packages/c5/42/6e0cc71dc8a464486e944a4fa0d85bdec031cc2969e98ed41532a98336b9/coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48", size = 248921, upload-time = "2025-10-15T15:13:43.715Z" }, + { url = "https://files.pythonhosted.org/packages/e8/1c/743c2ef665e6858cccb0f84377dfe3a4c25add51e8c7ef19249be92465b6/coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040", size = 218526, upload-time = "2025-10-15T15:13:45.336Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d5/226daadfd1bf8ddbccefbd3aa3547d7b960fb48e1bdac124e2dd13a2b71a/coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05", size = 219317, upload-time = "2025-10-15T15:13:47.401Z" }, + { url = "https://files.pythonhosted.org/packages/97/54/47db81dcbe571a48a298f206183ba8a7ba79200a37cd0d9f4788fcd2af4a/coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a", size = 217948, upload-time = "2025-10-15T15:13:49.096Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8b/cb68425420154e7e2a82fd779a8cc01549b6fa83c2ad3679cd6c088ebd07/coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b", size = 216837, upload-time = "2025-10-15T15:13:51.09Z" }, + { url = "https://files.pythonhosted.org/packages/33/55/9d61b5765a025685e14659c8d07037247de6383c0385757544ffe4606475/coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37", size = 217061, upload-time = "2025-10-15T15:13:52.747Z" }, + { url = "https://files.pythonhosted.org/packages/52/85/292459c9186d70dcec6538f06ea251bc968046922497377bf4a1dc9a71de/coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de", size = 258398, upload-time = "2025-10-15T15:13:54.45Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e2/46edd73fb8bf51446c41148d81944c54ed224854812b6ca549be25113ee0/coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f", size = 260574, upload-time = "2025-10-15T15:13:56.145Z" }, + { url = "https://files.pythonhosted.org/packages/07/5e/1df469a19007ff82e2ca8fe509822820a31e251f80ee7344c34f6cd2ec43/coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c", size = 262797, upload-time = "2025-10-15T15:13:58.635Z" }, + { url = "https://files.pythonhosted.org/packages/f9/50/de216b31a1434b94d9b34a964c09943c6be45069ec704bfc379d8d89a649/coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa", size = 257361, upload-time = "2025-10-15T15:14:00.409Z" }, + { url = "https://files.pythonhosted.org/packages/82/1e/3f9f8344a48111e152e0fd495b6fff13cc743e771a6050abf1627a7ba918/coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740", size = 260349, upload-time = "2025-10-15T15:14:02.188Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/3f52741f9e7d82124272f3070bbe316006a7de1bad1093f88d59bfc6c548/coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef", size = 258114, upload-time = "2025-10-15T15:14:03.907Z" }, + { url = "https://files.pythonhosted.org/packages/0b/8b/918f0e15f0365d50d3986bbd3338ca01178717ac5678301f3f547b6619e6/coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0", size = 256723, upload-time = "2025-10-15T15:14:06.324Z" }, + { url = "https://files.pythonhosted.org/packages/44/9e/7776829f82d3cf630878a7965a7d70cc6ca94f22c7d20ec4944f7148cb46/coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca", size = 259238, upload-time = "2025-10-15T15:14:08.002Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b8/49cf253e1e7a3bedb85199b201862dd7ca4859f75b6cf25ffa7298aa0760/coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2", size = 219180, upload-time = "2025-10-15T15:14:09.786Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e1/1a541703826be7ae2125a0fb7f821af5729d56bb71e946e7b933cc7a89a4/coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268", size = 220241, upload-time = "2025-10-15T15:14:11.471Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/5ee0e0a08621140fd418ec4020f595b4d52d7eb429ae6a0c6542b4ba6f14/coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836", size = 218510, upload-time = "2025-10-15T15:14:13.46Z" }, + { url = "https://files.pythonhosted.org/packages/f4/06/e923830c1985ce808e40a3fa3eb46c13350b3224b7da59757d37b6ce12b8/coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497", size = 216110, upload-time = "2025-10-15T15:14:15.157Z" }, + { url = "https://files.pythonhosted.org/packages/42/82/cdeed03bfead45203fb651ed756dfb5266028f5f939e7f06efac4041dad5/coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e", size = 216395, upload-time = "2025-10-15T15:14:16.863Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ba/e1c80caffc3199aa699813f73ff097bc2df7b31642bdbc7493600a8f1de5/coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1", size = 247433, upload-time = "2025-10-15T15:14:18.589Z" }, + { url = "https://files.pythonhosted.org/packages/80/c0/5b259b029694ce0a5bbc1548834c7ba3db41d3efd3474489d7efce4ceb18/coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca", size = 249970, upload-time = "2025-10-15T15:14:20.307Z" }, + { url = "https://files.pythonhosted.org/packages/8c/86/171b2b5e1aac7e2fd9b43f7158b987dbeb95f06d1fbecad54ad8163ae3e8/coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd", size = 251324, upload-time = "2025-10-15T15:14:22.419Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/7e10414d343385b92024af3932a27a1caf75c6e27ee88ba211221ff1a145/coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43", size = 247445, upload-time = "2025-10-15T15:14:24.205Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3b/e4f966b21f5be8c4bf86ad75ae94efa0de4c99c7bbb8114476323102e345/coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777", size = 249324, upload-time = "2025-10-15T15:14:26.234Z" }, + { url = "https://files.pythonhosted.org/packages/00/a2/8479325576dfcd909244d0df215f077f47437ab852ab778cfa2f8bf4d954/coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2", size = 247261, upload-time = "2025-10-15T15:14:28.42Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d8/3a9e2db19d94d65771d0f2e21a9ea587d11b831332a73622f901157cc24b/coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d", size = 247092, upload-time = "2025-10-15T15:14:30.784Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b1/bbca3c472544f9e2ad2d5116b2379732957048be4b93a9c543fcd0207e5f/coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4", size = 248755, upload-time = "2025-10-15T15:14:32.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/49/638d5a45a6a0f00af53d6b637c87007eb2297042186334e9923a61aa8854/coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721", size = 218793, upload-time = "2025-10-15T15:14:34.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/cc/b675a51f2d068adb3cdf3799212c662239b0ca27f4691d1fff81b92ea850/coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad", size = 219587, upload-time = "2025-10-15T15:14:37.047Z" }, + { url = "https://files.pythonhosted.org/packages/93/98/5ac886876026de04f00820e5094fe22166b98dcb8b426bf6827aaf67048c/coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479", size = 218168, upload-time = "2025-10-15T15:14:38.861Z" }, + { url = "https://files.pythonhosted.org/packages/14/d1/b4145d35b3e3ecf4d917e97fc8895bcf027d854879ba401d9ff0f533f997/coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f", size = 216850, upload-time = "2025-10-15T15:14:40.651Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d1/7f645fc2eccd318369a8a9948acc447bb7c1ade2911e31d3c5620544c22b/coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e", size = 217071, upload-time = "2025-10-15T15:14:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/64d124649db2737ceced1dfcbdcb79898d5868d311730f622f8ecae84250/coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44", size = 258570, upload-time = "2025-10-15T15:14:44.542Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3f/6f5922f80dc6f2d8b2c6f974835c43f53eb4257a7797727e6ca5b7b2ec1f/coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3", size = 260738, upload-time = "2025-10-15T15:14:46.436Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5f/9e883523c4647c860b3812b417a2017e361eca5b635ee658387dc11b13c1/coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b", size = 262994, upload-time = "2025-10-15T15:14:48.3Z" }, + { url = "https://files.pythonhosted.org/packages/07/bb/43b5a8e94c09c8bf51743ffc65c4c841a4ca5d3ed191d0a6919c379a1b83/coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d", size = 257282, upload-time = "2025-10-15T15:14:50.236Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e5/0ead8af411411330b928733e1d201384b39251a5f043c1612970310e8283/coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2", size = 260430, upload-time = "2025-10-15T15:14:52.413Z" }, + { url = "https://files.pythonhosted.org/packages/ae/66/03dd8bb0ba5b971620dcaac145461950f6d8204953e535d2b20c6b65d729/coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e", size = 258190, upload-time = "2025-10-15T15:14:54.268Z" }, + { url = "https://files.pythonhosted.org/packages/45/ae/28a9cce40bf3174426cb2f7e71ee172d98e7f6446dff936a7ccecee34b14/coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996", size = 256658, upload-time = "2025-10-15T15:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/5c/7c/3a44234a8599513684bfc8684878fd7b126c2760f79712bb78c56f19efc4/coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11", size = 259342, upload-time = "2025-10-15T15:14:58.538Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e6/0108519cba871af0351725ebdb8660fd7a0fe2ba3850d56d32490c7d9b4b/coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73", size = 219568, upload-time = "2025-10-15T15:15:00.382Z" }, + { url = "https://files.pythonhosted.org/packages/c9/76/44ba876e0942b4e62fdde23ccb029ddb16d19ba1bef081edd00857ba0b16/coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547", size = 220687, upload-time = "2025-10-15T15:15:02.322Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0c/0df55ecb20d0d0ed5c322e10a441775e1a3a5d78c60f0c4e1abfe6fcf949/coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3", size = 218711, upload-time = "2025-10-15T15:15:04.575Z" }, + { url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "fish-audio" +version = "1.0.0" +source = { editable = "." } +dependencies = [ + { name = "httpx" }, + { name = "httpx-ws", version = "0.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "httpx-ws", version = "0.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ormsgpack", version = "1.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "ormsgpack", version = "1.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pydantic" }, +] + +[package.optional-dependencies] +utils = [ + { name = "sounddevice" }, + { name = "soundfile" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "python-dotenv" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.27.2" }, + { name = "httpx-ws", specifier = ">=0.6.2" }, + { name = "ormsgpack", specifier = ">=1.5.0" }, + { name = "pydantic", specifier = ">=2.9.1" }, + { name = "sounddevice", marker = "extra == 'utils'", specifier = ">=0.4.6" }, + { name = "soundfile", marker = "extra == 'utils'", specifier = ">=0.12.1" }, +] +provides-extras = ["utils"] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.14.1" }, + { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest-asyncio", specifier = ">=0.24.0" }, + { name = "pytest-cov", specifier = ">=5.0.0" }, + { name = "python-dotenv", specifier = ">=1.0.1" }, + { name = "ruff", specifier = ">=0.14.3" }, +] + +[[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 = "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 = "httpx-ws" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version < '3.10'" }, + { name = "httpcore", marker = "python_full_version < '3.10'" }, + { name = "httpx", marker = "python_full_version < '3.10'" }, + { name = "wsproto", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/ba/e310ccdb8f18a2b894bfacd085ef390cf6cc70bb10ff9f109d58d94f6b47/httpx_ws-0.7.2.tar.gz", hash = "sha256:93edea6c8fc313464fc287bff7d2ad20e6196b7754c76f946f73b4af79886d4e", size = 24513, upload-time = "2025-03-28T13:20:03.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/3d/2113a5c7af9a13663fa026882d0302ed4142960388536f885dacd6be7038/httpx_ws-0.7.2-py3-none-any.whl", hash = "sha256:dd7bf9dbaa96dcd5cef1af3a7e1130cfac068bebecce25a74145022f5a8427a3", size = 14424, upload-time = "2025-03-28T13:20:04.238Z" }, +] + +[[package]] +name = "httpx-ws" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.10'" }, + { name = "httpcore", marker = "python_full_version >= '3.10'" }, + { name = "httpx", marker = "python_full_version >= '3.10'" }, + { name = "wsproto", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/3d/93a17bc7d769f8b47facc9c8e2cb5435b04185c1ebfdf49f2c7485f892b6/httpx_ws-0.8.1.tar.gz", hash = "sha256:fdc8f471980d572f371d113a50710bbf397245048e9a542606af0bba2b956ab5", size = 105819, upload-time = "2025-10-28T07:31:42.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/b5/22a5a5b3e0497ec5ddc612474c0b50583bddd543eedc8f7da6eebc032c3d/httpx_ws-0.8.1-py3-none-any.whl", hash = "sha256:edef4173336a49e5cdec9e47ab8ffc896ef36c48a4b43a36c8daf93beeb9955c", size = 15311, upload-time = "2025-10-28T07:31:40.908Z" }, +] + +[[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 = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "mypy" +version = "1.18.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/6f/657961a0743cff32e6c0611b63ff1c1970a0b482ace35b069203bf705187/mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c", size = 12807973, upload-time = "2025-09-19T00:10:35.282Z" }, + { url = "https://files.pythonhosted.org/packages/10/e9/420822d4f661f13ca8900f5fa239b40ee3be8b62b32f3357df9a3045a08b/mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e", size = 11896527, upload-time = "2025-09-19T00:10:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/aa/73/a05b2bbaa7005f4642fcfe40fb73f2b4fb6bb44229bd585b5878e9a87ef8/mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b", size = 12507004, upload-time = "2025-09-19T00:11:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/f6e4b9f0d031c11ccbd6f17da26564f3a0f3c4155af344006434b0a05a9d/mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66", size = 13245947, upload-time = "2025-09-19T00:10:46.923Z" }, + { url = "https://files.pythonhosted.org/packages/d7/97/19727e7499bfa1ae0773d06afd30ac66a58ed7437d940c70548634b24185/mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428", size = 13499217, upload-time = "2025-09-19T00:09:39.472Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4f/90dc8c15c1441bf31cf0f9918bb077e452618708199e530f4cbd5cede6ff/mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed", size = 9766753, upload-time = "2025-09-19T00:10:49.161Z" }, + { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, + { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, + { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, + { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, + { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, + { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, + { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, + { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a6/490ff491d8ecddf8ab91762d4f67635040202f76a44171420bcbe38ceee5/mypy-1.18.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b", size = 12807230, upload-time = "2025-09-19T00:09:49.471Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2e/60076fc829645d167ece9e80db9e8375648d210dab44cc98beb5b322a826/mypy-1.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133", size = 11895666, upload-time = "2025-09-19T00:10:53.678Z" }, + { url = "https://files.pythonhosted.org/packages/97/4a/1e2880a2a5dda4dc8d9ecd1a7e7606bc0b0e14813637eeda40c38624e037/mypy-1.18.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6", size = 12499608, upload-time = "2025-09-19T00:09:36.204Z" }, + { url = "https://files.pythonhosted.org/packages/00/81/a117f1b73a3015b076b20246b1f341c34a578ebd9662848c6b80ad5c4138/mypy-1.18.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac", size = 13244551, upload-time = "2025-09-19T00:10:17.531Z" }, + { url = "https://files.pythonhosted.org/packages/9b/61/b9f48e1714ce87c7bf0358eb93f60663740ebb08f9ea886ffc670cea7933/mypy-1.18.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b", size = 13491552, upload-time = "2025-09-19T00:10:13.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/b2c0af3b684fa80d1b27501a8bdd3d2daa467ea3992a8aa612f5ca17c2db/mypy-1.18.2-cp39-cp39-win_amd64.whl", hash = "sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0", size = 9765635, upload-time = "2025-09-19T00:10:30.993Z" }, + { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "numpy" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015, upload-time = "2024-08-26T20:19:40.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245, upload-time = "2024-08-26T20:04:14.625Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540, upload-time = "2024-08-26T20:04:36.784Z" }, + { url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623, upload-time = "2024-08-26T20:04:46.491Z" }, + { url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774, upload-time = "2024-08-26T20:04:58.173Z" }, + { url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081, upload-time = "2024-08-26T20:05:19.098Z" }, + { url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451, upload-time = "2024-08-26T20:05:47.479Z" }, + { url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572, upload-time = "2024-08-26T20:06:17.137Z" }, + { url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722, upload-time = "2024-08-26T20:06:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170, upload-time = "2024-08-26T20:06:50.361Z" }, + { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558, upload-time = "2024-08-26T20:07:13.881Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137, upload-time = "2024-08-26T20:07:45.345Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552, upload-time = "2024-08-26T20:08:06.666Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957, upload-time = "2024-08-26T20:08:15.83Z" }, + { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573, upload-time = "2024-08-26T20:08:27.185Z" }, + { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330, upload-time = "2024-08-26T20:08:48.058Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895, upload-time = "2024-08-26T20:09:16.536Z" }, + { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253, upload-time = "2024-08-26T20:09:46.263Z" }, + { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074, upload-time = "2024-08-26T20:10:08.483Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640, upload-time = "2024-08-26T20:10:19.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230, upload-time = "2024-08-26T20:10:43.413Z" }, + { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803, upload-time = "2024-08-26T20:11:13.916Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835, upload-time = "2024-08-26T20:11:34.779Z" }, + { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499, upload-time = "2024-08-26T20:11:43.902Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497, upload-time = "2024-08-26T20:11:55.09Z" }, + { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158, upload-time = "2024-08-26T20:12:14.95Z" }, + { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173, upload-time = "2024-08-26T20:12:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174, upload-time = "2024-08-26T20:13:13.634Z" }, + { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701, upload-time = "2024-08-26T20:13:34.851Z" }, + { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313, upload-time = "2024-08-26T20:13:45.653Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179, upload-time = "2024-08-26T20:14:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942, upload-time = "2024-08-26T20:14:40.108Z" }, + { url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512, upload-time = "2024-08-26T20:15:00.985Z" }, + { url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976, upload-time = "2024-08-26T20:15:10.876Z" }, + { url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494, upload-time = "2024-08-26T20:15:22.055Z" }, + { url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596, upload-time = "2024-08-26T20:15:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099, upload-time = "2024-08-26T20:16:11.048Z" }, + { url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823, upload-time = "2024-08-26T20:16:40.171Z" }, + { url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424, upload-time = "2024-08-26T20:17:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809, upload-time = "2024-08-26T20:17:13.553Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314, upload-time = "2024-08-26T20:17:36.72Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288, upload-time = "2024-08-26T20:18:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793, upload-time = "2024-08-26T20:18:19.125Z" }, + { url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885, upload-time = "2024-08-26T20:18:47.237Z" }, + { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784, upload-time = "2024-08-26T20:19:11.19Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187, upload-time = "2025-10-15T16:18:11.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/e7/0e07379944aa8afb49a556a2b54587b828eb41dc9adc56fb7615b678ca53/numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb", size = 21259519, upload-time = "2025-10-15T16:15:19.012Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cb/5a69293561e8819b09e34ed9e873b9a82b5f2ade23dce4c51dc507f6cfe1/numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f", size = 14452796, upload-time = "2025-10-15T16:15:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/ff11611200acd602a1e5129e36cfd25bf01ad8e5cf927baf2e90236eb02e/numpy-2.3.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36", size = 5381639, upload-time = "2025-10-15T16:15:25.572Z" }, + { url = "https://files.pythonhosted.org/packages/ea/77/e95c757a6fe7a48d28a009267408e8aa382630cc1ad1db7451b3bc21dbb4/numpy-2.3.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032", size = 6914296, upload-time = "2025-10-15T16:15:27.079Z" }, + { url = "https://files.pythonhosted.org/packages/a3/d2/137c7b6841c942124eae921279e5c41b1c34bab0e6fc60c7348e69afd165/numpy-2.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7", size = 14591904, upload-time = "2025-10-15T16:15:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/bb/32/67e3b0f07b0aba57a078c4ab777a9e8e6bc62f24fb53a2337f75f9691699/numpy-2.3.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda", size = 16939602, upload-time = "2025-10-15T16:15:31.106Z" }, + { url = "https://files.pythonhosted.org/packages/95/22/9639c30e32c93c4cee3ccdb4b09c2d0fbff4dcd06d36b357da06146530fb/numpy-2.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0", size = 16372661, upload-time = "2025-10-15T16:15:33.546Z" }, + { url = "https://files.pythonhosted.org/packages/12/e9/a685079529be2b0156ae0c11b13d6be647743095bb51d46589e95be88086/numpy-2.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a", size = 18884682, upload-time = "2025-10-15T16:15:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/cf/85/f6f00d019b0cc741e64b4e00ce865a57b6bed945d1bbeb1ccadbc647959b/numpy-2.3.4-cp311-cp311-win32.whl", hash = "sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1", size = 6570076, upload-time = "2025-10-15T16:15:38.225Z" }, + { url = "https://files.pythonhosted.org/packages/7d/10/f8850982021cb90e2ec31990291f9e830ce7d94eef432b15066e7cbe0bec/numpy-2.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996", size = 13089358, upload-time = "2025-10-15T16:15:40.404Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ad/afdd8351385edf0b3445f9e24210a9c3971ef4de8fd85155462fc4321d79/numpy-2.3.4-cp311-cp311-win_arm64.whl", hash = "sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c", size = 10462292, upload-time = "2025-10-15T16:15:42.896Z" }, + { url = "https://files.pythonhosted.org/packages/96/7a/02420400b736f84317e759291b8edaeee9dc921f72b045475a9cbdb26b17/numpy-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11", size = 20957727, upload-time = "2025-10-15T16:15:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/18/90/a014805d627aa5750f6f0e878172afb6454552da929144b3c07fcae1bb13/numpy-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9", size = 14187262, upload-time = "2025-10-15T16:15:47.761Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e4/0a94b09abe89e500dc748e7515f21a13e30c5c3fe3396e6d4ac108c25fca/numpy-2.3.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667", size = 5115992, upload-time = "2025-10-15T16:15:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/db77c75b055c6157cbd4f9c92c4458daef0dd9cbe6d8d2fe7f803cb64c37/numpy-2.3.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef", size = 6648672, upload-time = "2025-10-15T16:15:52.442Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e6/e31b0d713719610e406c0ea3ae0d90760465b086da8783e2fd835ad59027/numpy-2.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e", size = 14284156, upload-time = "2025-10-15T16:15:54.351Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/30a85127bfee6f108282107caf8e06a1f0cc997cb6b52cdee699276fcce4/numpy-2.3.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a", size = 16641271, upload-time = "2025-10-15T16:15:56.67Z" }, + { url = "https://files.pythonhosted.org/packages/06/f2/2e06a0f2adf23e3ae29283ad96959267938d0efd20a2e25353b70065bfec/numpy-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16", size = 16059531, upload-time = "2025-10-15T16:15:59.412Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e7/b106253c7c0d5dc352b9c8fab91afd76a93950998167fa3e5afe4ef3a18f/numpy-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786", size = 18578983, upload-time = "2025-10-15T16:16:01.804Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/04ecc41e71462276ee867ccbef26a4448638eadecf1bc56772c9ed6d0255/numpy-2.3.4-cp312-cp312-win32.whl", hash = "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc", size = 6291380, upload-time = "2025-10-15T16:16:03.938Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a8/566578b10d8d0e9955b1b6cd5db4e9d4592dd0026a941ff7994cedda030a/numpy-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32", size = 12787999, upload-time = "2025-10-15T16:16:05.801Z" }, + { url = "https://files.pythonhosted.org/packages/58/22/9c903a957d0a8071b607f5b1bff0761d6e608b9a965945411f867d515db1/numpy-2.3.4-cp312-cp312-win_arm64.whl", hash = "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db", size = 10197412, upload-time = "2025-10-15T16:16:07.854Z" }, + { url = "https://files.pythonhosted.org/packages/57/7e/b72610cc91edf138bc588df5150957a4937221ca6058b825b4725c27be62/numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966", size = 20950335, upload-time = "2025-10-15T16:16:10.304Z" }, + { url = "https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3", size = 14179878, upload-time = "2025-10-15T16:16:12.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/01/5a67cb785bda60f45415d09c2bc245433f1c68dd82eef9c9002c508b5a65/numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197", size = 5108673, upload-time = "2025-10-15T16:16:14.877Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cd/8428e23a9fcebd33988f4cb61208fda832800ca03781f471f3727a820704/numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e", size = 6641438, upload-time = "2025-10-15T16:16:16.805Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7", size = 14281290, upload-time = "2025-10-15T16:16:18.764Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7e/7d306ff7cb143e6d975cfa7eb98a93e73495c4deabb7d1b5ecf09ea0fd69/numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953", size = 16636543, upload-time = "2025-10-15T16:16:21.072Z" }, + { url = "https://files.pythonhosted.org/packages/47/6a/8cfc486237e56ccfb0db234945552a557ca266f022d281a2f577b98e955c/numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37", size = 16056117, upload-time = "2025-10-15T16:16:23.369Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0e/42cb5e69ea901e06ce24bfcc4b5664a56f950a70efdcf221f30d9615f3f3/numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd", size = 18577788, upload-time = "2025-10-15T16:16:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/86/92/41c3d5157d3177559ef0a35da50f0cda7fa071f4ba2306dd36818591a5bc/numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646", size = 6282620, upload-time = "2025-10-15T16:16:29.811Z" }, + { url = "https://files.pythonhosted.org/packages/09/97/fd421e8bc50766665ad35536c2bb4ef916533ba1fdd053a62d96cc7c8b95/numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d", size = 12784672, upload-time = "2025-10-15T16:16:31.589Z" }, + { url = "https://files.pythonhosted.org/packages/ad/df/5474fb2f74970ca8eb978093969b125a84cc3d30e47f82191f981f13a8a0/numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc", size = 10196702, upload-time = "2025-10-15T16:16:33.902Z" }, + { url = "https://files.pythonhosted.org/packages/11/83/66ac031464ec1767ea3ed48ce40f615eb441072945e98693bec0bcd056cc/numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879", size = 21049003, upload-time = "2025-10-15T16:16:36.101Z" }, + { url = "https://files.pythonhosted.org/packages/5f/99/5b14e0e686e61371659a1d5bebd04596b1d72227ce36eed121bb0aeab798/numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562", size = 14302980, upload-time = "2025-10-15T16:16:39.124Z" }, + { url = "https://files.pythonhosted.org/packages/2c/44/e9486649cd087d9fc6920e3fc3ac2aba10838d10804b1e179fb7cbc4e634/numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a", size = 5231472, upload-time = "2025-10-15T16:16:41.168Z" }, + { url = "https://files.pythonhosted.org/packages/3e/51/902b24fa8887e5fe2063fd61b1895a476d0bbf46811ab0c7fdf4bd127345/numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6", size = 6739342, upload-time = "2025-10-15T16:16:43.777Z" }, + { url = "https://files.pythonhosted.org/packages/34/f1/4de9586d05b1962acdcdb1dc4af6646361a643f8c864cef7c852bf509740/numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7", size = 14354338, upload-time = "2025-10-15T16:16:46.081Z" }, + { url = "https://files.pythonhosted.org/packages/1f/06/1c16103b425de7969d5a76bdf5ada0804b476fed05d5f9e17b777f1cbefd/numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0", size = 16702392, upload-time = "2025-10-15T16:16:48.455Z" }, + { url = "https://files.pythonhosted.org/packages/34/b2/65f4dc1b89b5322093572b6e55161bb42e3e0487067af73627f795cc9d47/numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f", size = 16134998, upload-time = "2025-10-15T16:16:51.114Z" }, + { url = "https://files.pythonhosted.org/packages/d4/11/94ec578896cdb973aaf56425d6c7f2aff4186a5c00fac15ff2ec46998b46/numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64", size = 18651574, upload-time = "2025-10-15T16:16:53.429Z" }, + { url = "https://files.pythonhosted.org/packages/62/b7/7efa763ab33dbccf56dade36938a77345ce8e8192d6b39e470ca25ff3cd0/numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb", size = 6413135, upload-time = "2025-10-15T16:16:55.992Z" }, + { url = "https://files.pythonhosted.org/packages/43/70/aba4c38e8400abcc2f345e13d972fb36c26409b3e644366db7649015f291/numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c", size = 12928582, upload-time = "2025-10-15T16:16:57.943Z" }, + { url = "https://files.pythonhosted.org/packages/67/63/871fad5f0073fc00fbbdd7232962ea1ac40eeaae2bba66c76214f7954236/numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40", size = 10266691, upload-time = "2025-10-15T16:17:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/72/71/ae6170143c115732470ae3a2d01512870dd16e0953f8a6dc89525696069b/numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e", size = 20955580, upload-time = "2025-10-15T16:17:02.509Z" }, + { url = "https://files.pythonhosted.org/packages/af/39/4be9222ffd6ca8a30eda033d5f753276a9c3426c397bb137d8e19dedd200/numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff", size = 14188056, upload-time = "2025-10-15T16:17:04.873Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3d/d85f6700d0a4aa4f9491030e1021c2b2b7421b2b38d01acd16734a2bfdc7/numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f", size = 5116555, upload-time = "2025-10-15T16:17:07.499Z" }, + { url = "https://files.pythonhosted.org/packages/bf/04/82c1467d86f47eee8a19a464c92f90a9bb68ccf14a54c5224d7031241ffb/numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b", size = 6643581, upload-time = "2025-10-15T16:17:09.774Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d3/c79841741b837e293f48bd7db89d0ac7a4f2503b382b78a790ef1dc778a5/numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7", size = 14299186, upload-time = "2025-10-15T16:17:11.937Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7e/4a14a769741fbf237eec5a12a2cbc7a4c4e061852b6533bcb9e9a796c908/numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2", size = 16638601, upload-time = "2025-10-15T16:17:14.391Z" }, + { url = "https://files.pythonhosted.org/packages/93/87/1c1de269f002ff0a41173fe01dcc925f4ecff59264cd8f96cf3b60d12c9b/numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52", size = 16074219, upload-time = "2025-10-15T16:17:17.058Z" }, + { url = "https://files.pythonhosted.org/packages/cd/28/18f72ee77408e40a76d691001ae599e712ca2a47ddd2c4f695b16c65f077/numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26", size = 18576702, upload-time = "2025-10-15T16:17:19.379Z" }, + { url = "https://files.pythonhosted.org/packages/c3/76/95650169b465ececa8cf4b2e8f6df255d4bf662775e797ade2025cc51ae6/numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc", size = 6337136, upload-time = "2025-10-15T16:17:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/a231a5c43ede5d6f77ba4a91e915a87dea4aeea76560ba4d2bf185c683f0/numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9", size = 12920542, upload-time = "2025-10-15T16:17:24.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/0c/ae9434a888f717c5ed2ff2393b3f344f0ff6f1c793519fa0c540461dc530/numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868", size = 10480213, upload-time = "2025-10-15T16:17:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/83/4b/c4a5f0841f92536f6b9592694a5b5f68c9ab37b775ff342649eadf9055d3/numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec", size = 21052280, upload-time = "2025-10-15T16:17:29.638Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/90308845fc93b984d2cc96d83e2324ce8ad1fd6efea81b324cba4b673854/numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3", size = 14302930, upload-time = "2025-10-15T16:17:32.384Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4e/07439f22f2a3b247cec4d63a713faae55e1141a36e77fb212881f7cda3fb/numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365", size = 5231504, upload-time = "2025-10-15T16:17:34.515Z" }, + { url = "https://files.pythonhosted.org/packages/ab/de/1e11f2547e2fe3d00482b19721855348b94ada8359aef5d40dd57bfae9df/numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252", size = 6739405, upload-time = "2025-10-15T16:17:36.128Z" }, + { url = "https://files.pythonhosted.org/packages/3b/40/8cd57393a26cebe2e923005db5134a946c62fa56a1087dc7c478f3e30837/numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e", size = 14354866, upload-time = "2025-10-15T16:17:38.884Z" }, + { url = "https://files.pythonhosted.org/packages/93/39/5b3510f023f96874ee6fea2e40dfa99313a00bf3ab779f3c92978f34aace/numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0", size = 16703296, upload-time = "2025-10-15T16:17:41.564Z" }, + { url = "https://files.pythonhosted.org/packages/41/0d/19bb163617c8045209c1996c4e427bccbc4bbff1e2c711f39203c8ddbb4a/numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0", size = 16136046, upload-time = "2025-10-15T16:17:43.901Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c1/6dba12fdf68b02a21ac411c9df19afa66bed2540f467150ca64d246b463d/numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f", size = 18652691, upload-time = "2025-10-15T16:17:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/f8/73/f85056701dbbbb910c51d846c58d29fd46b30eecd2b6ba760fc8b8a1641b/numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d", size = 6485782, upload-time = "2025-10-15T16:17:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/17/90/28fa6f9865181cb817c2471ee65678afa8a7e2a1fb16141473d5fa6bacc3/numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6", size = 13113301, upload-time = "2025-10-15T16:17:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/54/23/08c002201a8e7e1f9afba93b97deceb813252d9cfd0d3351caed123dcf97/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", size = 10547532, upload-time = "2025-10-15T16:17:53.48Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b6/64898f51a86ec88ca1257a59c1d7fd077b60082a119affefcdf1dd0df8ca/numpy-2.3.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05", size = 21131552, upload-time = "2025-10-15T16:17:55.845Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4c/f135dc6ebe2b6a3c77f4e4838fa63d350f85c99462012306ada1bd4bc460/numpy-2.3.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346", size = 14377796, upload-time = "2025-10-15T16:17:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a4/f33f9c23fcc13dd8412fc8614559b5b797e0aba9d8e01dfa8bae10c84004/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e", size = 5306904, upload-time = "2025-10-15T16:18:00.596Z" }, + { url = "https://files.pythonhosted.org/packages/28/af/c44097f25f834360f9fb960fa082863e0bad14a42f36527b2a121abdec56/numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b", size = 6819682, upload-time = "2025-10-15T16:18:02.32Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8c/cd283b54c3c2b77e188f63e23039844f56b23bba1712318288c13fe86baf/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847", size = 14422300, upload-time = "2025-10-15T16:18:04.271Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f0/8404db5098d92446b3e3695cf41c6f0ecb703d701cb0b7566ee2177f2eee/numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d", size = 16760806, upload-time = "2025-10-15T16:18:06.668Z" }, + { url = "https://files.pythonhosted.org/packages/95/8e/2844c3959ce9a63acc7c8e50881133d86666f0420bcde695e115ced0920f/numpy-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f", size = 12973130, upload-time = "2025-10-15T16:18:09.397Z" }, +] + +[[package]] +name = "ormsgpack" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/65/f8/224c342c0e03e131aaa1a1f19aa2244e167001783a433f4eed10eedd834b/ormsgpack-1.11.0.tar.gz", hash = "sha256:7c9988e78fedba3292541eb3bb274fa63044ef4da2ddb47259ea70c05dee4206", size = 49357, upload-time = "2025-10-08T17:29:15.621Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/3d/6996193cb2babc47fc92456223bef7d141065357ad4204eccf313f47a7b3/ormsgpack-1.11.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:03d4e658dd6e1882a552ce1d13cc7b49157414e7d56a4091fbe7823225b08cba", size = 367965, upload-time = "2025-10-08T17:28:06.736Z" }, + { url = "https://files.pythonhosted.org/packages/35/89/c83b805dd9caebb046f4ceeed3706d0902ed2dbbcf08b8464e89f2c52e05/ormsgpack-1.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bb67eb913c2b703f0ed39607fc56e50724dd41f92ce080a586b4d6149eb3fe4", size = 195209, upload-time = "2025-10-08T17:28:08.395Z" }, + { url = "https://files.pythonhosted.org/packages/3a/17/427d9c4f77b120f0af01d7a71d8144771c9388c2a81f712048320e31353b/ormsgpack-1.11.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e54175b92411f73a238e5653a998627f6660de3def37d9dd7213e0fd264ca56", size = 205868, upload-time = "2025-10-08T17:28:09.688Z" }, + { url = "https://files.pythonhosted.org/packages/82/32/a9ce218478bdbf3fee954159900e24b314ab3064f7b6a217ccb1e3464324/ormsgpack-1.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca2b197f4556e1823d1319869d4c5dc278be335286d2308b0ed88b59a5afcc25", size = 207391, upload-time = "2025-10-08T17:28:11.031Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d3/4413fe7454711596fdf08adabdfa686580e4656702015108e4975f00a022/ormsgpack-1.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc62388262f58c792fe1e450e1d9dbcc174ed2fb0b43db1675dd7c5ff2319d6a", size = 377078, upload-time = "2025-10-08T17:28:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ad/13fae555a45e35ca1ca929a27c9ee0a3ecada931b9d44454658c543f9b9c/ormsgpack-1.11.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c48bc10af74adfbc9113f3fb160dc07c61ad9239ef264c17e449eba3de343dc2", size = 470776, upload-time = "2025-10-08T17:28:13.484Z" }, + { url = "https://files.pythonhosted.org/packages/36/60/51178b093ffc4e2ef3381013a67223e7d56224434fba80047249f4a84b26/ormsgpack-1.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a608d3a1d4fa4acdc5082168a54513cff91f47764cef435e81a483452f5f7647", size = 380862, upload-time = "2025-10-08T17:28:14.747Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e3/1cb6c161335e2ae7d711ecfb007a31a3936603626e347c13e5e53b7c7cf8/ormsgpack-1.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:97217b4f7f599ba45916b9c4c4b1d5656e8e2a4d91e2e191d72a7569d3c30923", size = 112058, upload-time = "2025-10-08T17:28:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7c/90164d00e8e94b48eff8a17bc2f4be6b71ae356a00904bc69d5e8afe80fb/ormsgpack-1.11.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c7be823f47d8e36648d4bc90634b93f02b7d7cc7480081195f34767e86f181fb", size = 367964, upload-time = "2025-10-08T17:28:16.778Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c2/fb6331e880a3446c1341e72c77bd5a46da3e92a8e2edf7ea84a4c6c14fff/ormsgpack-1.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68accf15d1b013812755c0eb7a30e1fc2f81eb603a1a143bf0cda1b301cfa797", size = 195209, upload-time = "2025-10-08T17:28:17.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/50/4943fb5df8cc02da6b7b1ee2c2a7fb13aebc9f963d69280b1bb02b1fb178/ormsgpack-1.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:805d06fb277d9a4e503c0c707545b49cde66cbb2f84e5cf7c58d81dfc20d8658", size = 205869, upload-time = "2025-10-08T17:28:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/e7e06835bfea9adeef43915143ce818098aecab0cbd3df584815adf3e399/ormsgpack-1.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1e57cdf003e77acc43643bda151dc01f97147a64b11cdee1380bb9698a7601c", size = 207391, upload-time = "2025-10-08T17:28:20.352Z" }, + { url = "https://files.pythonhosted.org/packages/33/f0/f28a19e938a14ec223396e94f4782fbcc023f8c91f2ab6881839d3550f32/ormsgpack-1.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:37fc05bdaabd994097c62e2f3e08f66b03f856a640ede6dc5ea340bd15b77f4d", size = 377081, upload-time = "2025-10-08T17:28:21.926Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e3/73d1d7287637401b0b6637e30ba9121e1aa1d9f5ea185ed9834ca15d512c/ormsgpack-1.11.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a6e9db6c73eb46b2e4d97bdffd1368a66f54e6806b563a997b19c004ef165e1d", size = 470779, upload-time = "2025-10-08T17:28:22.993Z" }, + { url = "https://files.pythonhosted.org/packages/9c/46/7ba7f9721e766dd0dfe4cedf444439447212abffe2d2f4538edeeec8ccbd/ormsgpack-1.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e9c44eae5ac0196ffc8b5ed497c75511056508f2303fa4d36b208eb820cf209e", size = 380865, upload-time = "2025-10-08T17:28:24.012Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7d/bb92a0782bbe0626c072c0320001410cf3f6743ede7dc18f034b1a18edef/ormsgpack-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:11d0dfaf40ae7c6de4f7dbd1e4892e2e6a55d911ab1774357c481158d17371e4", size = 112058, upload-time = "2025-10-08T17:28:25.015Z" }, + { url = "https://files.pythonhosted.org/packages/28/1a/f07c6f74142815d67e1d9d98c5b2960007100408ade8242edac96d5d1c73/ormsgpack-1.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:0c63a3f7199a3099c90398a1bdf0cb577b06651a442dc5efe67f2882665e5b02", size = 105894, upload-time = "2025-10-08T17:28:25.93Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/2805ebfb3d2cbb6c661b5fae053960fc90a2611d0d93e2207e753e836117/ormsgpack-1.11.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:3434d0c8d67de27d9010222de07fb6810fb9af3bb7372354ffa19257ac0eb83b", size = 368474, upload-time = "2025-10-08T17:28:27.532Z" }, + { url = "https://files.pythonhosted.org/packages/6f/39/6afae47822dca0ce4465d894c0bbb860a850ce29c157882dbdf77a5dd26e/ormsgpack-1.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2da5bd097e8dbfa4eb0d4ccfe79acd6f538dee4493579e2debfe4fc8f4ca89b", size = 195321, upload-time = "2025-10-08T17:28:28.573Z" }, + { url = "https://files.pythonhosted.org/packages/f6/54/11eda6b59f696d2f16de469bfbe539c9f469c4b9eef5a513996b5879c6e9/ormsgpack-1.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fdbaa0a5a8606a486960b60c24f2d5235d30ac7a8b98eeaea9854bffef14dc3d", size = 206036, upload-time = "2025-10-08T17:28:29.785Z" }, + { url = "https://files.pythonhosted.org/packages/1e/86/890430f704f84c4699ddad61c595d171ea2fd77a51fbc106f83981e83939/ormsgpack-1.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3682f24f800c1837017ee90ce321086b2cbaef88db7d4cdbbda1582aa6508159", size = 207615, upload-time = "2025-10-08T17:28:31.076Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b9/77383e16c991c0ecb772205b966fc68d9c519e0b5f9c3913283cbed30ffe/ormsgpack-1.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fcca21202bb05ccbf3e0e92f560ee59b9331182e4c09c965a28155efbb134993", size = 377195, upload-time = "2025-10-08T17:28:32.436Z" }, + { url = "https://files.pythonhosted.org/packages/20/e2/15f9f045d4947f3c8a5e0535259fddf027b17b1215367488b3565c573b9d/ormsgpack-1.11.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c30e5c4655ba46152d722ec7468e8302195e6db362ec1ae2c206bc64f6030e43", size = 470960, upload-time = "2025-10-08T17:28:33.556Z" }, + { url = "https://files.pythonhosted.org/packages/b8/61/403ce188c4c495bc99dff921a0ad3d9d352dd6d3c4b629f3638b7f0cf79b/ormsgpack-1.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7138a341f9e2c08c59368f03d3be25e8b87b3baaf10d30fb1f6f6b52f3d47944", size = 381174, upload-time = "2025-10-08T17:28:34.781Z" }, + { url = "https://files.pythonhosted.org/packages/14/a8/94c94bc48c68da4374870a851eea03fc5a45eb041182ad4c5ed9acfc05a4/ormsgpack-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:d4bd8589b78a11026d47f4edf13c1ceab9088bb12451f34396afe6497db28a27", size = 112314, upload-time = "2025-10-08T17:28:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/19/d0/aa4cf04f04e4cc180ce7a8d8ddb5a7f3af883329cbc59645d94d3ba157a5/ormsgpack-1.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:e5e746a1223e70f111d4001dab9585ac8639eee8979ca0c8db37f646bf2961da", size = 106072, upload-time = "2025-10-08T17:28:37.518Z" }, + { url = "https://files.pythonhosted.org/packages/8b/35/e34722edb701d053cf2240f55974f17b7dbfd11fdef72bd2f1835bcebf26/ormsgpack-1.11.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e7b36ab7b45cb95217ae1f05f1318b14a3e5ef73cb00804c0f06233f81a14e8", size = 368502, upload-time = "2025-10-08T17:28:38.547Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6a/c2fc369a79d6aba2aa28c8763856c95337ac7fcc0b2742185cd19397212a/ormsgpack-1.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43402d67e03a9a35cc147c8c03f0c377cad016624479e1ee5b879b8425551484", size = 195344, upload-time = "2025-10-08T17:28:39.554Z" }, + { url = "https://files.pythonhosted.org/packages/8b/6a/0f8e24b7489885534c1a93bdba7c7c434b9b8638713a68098867db9f254c/ormsgpack-1.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:64fd992f932764d6306b70ddc755c1bc3405c4c6a69f77a36acf7af1c8f5ada4", size = 206045, upload-time = "2025-10-08T17:28:40.561Z" }, + { url = "https://files.pythonhosted.org/packages/99/71/8b460ba264f3c6f82ef5b1920335720094e2bd943057964ce5287d6df83a/ormsgpack-1.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0362fb7fe4a29c046c8ea799303079a09372653a1ce5a5a588f3bbb8088368d0", size = 207641, upload-time = "2025-10-08T17:28:41.736Z" }, + { url = "https://files.pythonhosted.org/packages/50/cf/f369446abaf65972424ed2651f2df2b7b5c3b735c93fc7fa6cfb81e34419/ormsgpack-1.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:de2f7a65a9d178ed57be49eba3d0fc9b833c32beaa19dbd4ba56014d3c20b152", size = 377211, upload-time = "2025-10-08T17:28:43.12Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3f/948bb0047ce0f37c2efc3b9bb2bcfdccc61c63e0b9ce8088d4903ba39dcf/ormsgpack-1.11.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:f38cfae95461466055af966fc922d06db4e1654966385cda2828653096db34da", size = 470973, upload-time = "2025-10-08T17:28:44.465Z" }, + { url = "https://files.pythonhosted.org/packages/31/a4/92a8114d1d017c14aaa403445060f345df9130ca532d538094f38e535988/ormsgpack-1.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c88396189d238f183cea7831b07a305ab5c90d6d29b53288ae11200bd956357b", size = 381161, upload-time = "2025-10-08T17:28:46.063Z" }, + { url = "https://files.pythonhosted.org/packages/d0/64/5b76447da654798bfcfdfd64ea29447ff2b7f33fe19d0e911a83ad5107fc/ormsgpack-1.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:5403d1a945dd7c81044cebeca3f00a28a0f4248b33242a5d2d82111628043725", size = 112321, upload-time = "2025-10-08T17:28:47.393Z" }, + { url = "https://files.pythonhosted.org/packages/46/5e/89900d06db9ab81e7ec1fd56a07c62dfbdcda398c435718f4252e1dc52a0/ormsgpack-1.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:c57357b8d43b49722b876edf317bdad9e6d52071b523fdd7394c30cd1c67d5a0", size = 106084, upload-time = "2025-10-08T17:28:48.305Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0b/c659e8657085c8c13f6a0224789f422620cef506e26573b5434defe68483/ormsgpack-1.11.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d390907d90fd0c908211592c485054d7a80990697ef4dff4e436ac18e1aab98a", size = 368497, upload-time = "2025-10-08T17:28:49.297Z" }, + { url = "https://files.pythonhosted.org/packages/1b/0e/451e5848c7ed56bd287e8a2b5cb5926e54466f60936e05aec6cb299f9143/ormsgpack-1.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6153c2e92e789509098e04c9aa116b16673bd88ec78fbe0031deeb34ab642d10", size = 195385, upload-time = "2025-10-08T17:28:50.314Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/90f78cbbe494959f2439c2ec571f08cd3464c05a6a380b0d621c622122a9/ormsgpack-1.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2b2c2a065a94d742212b2018e1fecd8f8d72f3c50b53a97d1f407418093446d", size = 206114, upload-time = "2025-10-08T17:28:51.336Z" }, + { url = "https://files.pythonhosted.org/packages/fb/db/34163f4c0923bea32dafe42cd878dcc66795a3e85669bc4b01c1e2b92a7b/ormsgpack-1.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:110e65b5340f3d7ef8b0009deae3c6b169437e6b43ad5a57fd1748085d29d2ac", size = 207679, upload-time = "2025-10-08T17:28:53.627Z" }, + { url = "https://files.pythonhosted.org/packages/b6/14/04ee741249b16f380a9b4a0cc19d4134d0b7c74bab27a2117da09e525eb9/ormsgpack-1.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c27e186fca96ab34662723e65b420919910acbbc50fc8e1a44e08f26268cb0e0", size = 377237, upload-time = "2025-10-08T17:28:56.12Z" }, + { url = "https://files.pythonhosted.org/packages/89/ff/53e588a6aaa833237471caec679582c2950f0e7e1a8ba28c1511b465c1f4/ormsgpack-1.11.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d56b1f877c13d499052d37a3db2378a97d5e1588d264f5040b3412aee23d742c", size = 471021, upload-time = "2025-10-08T17:28:57.299Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f9/f20a6d9ef2be04da3aad05e8f5699957e9a30c6d5c043a10a296afa7e890/ormsgpack-1.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c88e28cd567c0a3269f624b4ade28142d5e502c8e826115093c572007af5be0a", size = 381205, upload-time = "2025-10-08T17:28:58.872Z" }, + { url = "https://files.pythonhosted.org/packages/f8/64/96c07d084b479ac8b7821a77ffc8d3f29d8b5c95ebfdf8db1c03dff02762/ormsgpack-1.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:8811160573dc0a65f62f7e0792c4ca6b7108dfa50771edb93f9b84e2d45a08ae", size = 112374, upload-time = "2025-10-08T17:29:00Z" }, + { url = "https://files.pythonhosted.org/packages/88/a5/5dcc18b818d50213a3cadfe336bb6163a102677d9ce87f3d2f1a1bee0f8c/ormsgpack-1.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:23e30a8d3c17484cf74e75e6134322255bd08bc2b5b295cc9c442f4bae5f3c2d", size = 106056, upload-time = "2025-10-08T17:29:01.29Z" }, + { url = "https://files.pythonhosted.org/packages/19/2b/776d1b411d2be50f77a6e6e94a25825cca55dcacfe7415fd691a144db71b/ormsgpack-1.11.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2905816502adfaf8386a01dd85f936cd378d243f4f5ee2ff46f67f6298dc90d5", size = 368661, upload-time = "2025-10-08T17:29:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0c/81a19e6115b15764db3d241788f9fac093122878aaabf872cc545b0c4650/ormsgpack-1.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c04402fb9a0a9b9f18fbafd6d5f8398ee99b3ec619fb63952d3a954bc9d47daa", size = 195539, upload-time = "2025-10-08T17:29:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/97/86/e5b50247a61caec5718122feb2719ea9d451d30ac0516c288c1dbc6408e8/ormsgpack-1.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a025ec07ac52056ecfd9e57b5cbc6fff163f62cb9805012b56cda599157f8ef2", size = 207718, upload-time = "2025-10-08T17:29:04.545Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9e/a1f32d7b4b03b8af5178cf8990c60bb36997d46089c1fee93eed31c04341/ormsgpack-1.11.0-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:354c6a5039faf63b63d8f42ec7915583a4a56e10b319284370a5a89c4382d985", size = 367967, upload-time = "2025-10-08T17:29:05.691Z" }, + { url = "https://files.pythonhosted.org/packages/d6/49/27d6c8354bc031217874797f888614bd7ff9f8dbd4b9161b47d5931c94c2/ormsgpack-1.11.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7058c85cc13dd329bc7b528e38626c6babcd0066d6e9163330a1509fe0aa4707", size = 195198, upload-time = "2025-10-08T17:29:06.774Z" }, + { url = "https://files.pythonhosted.org/packages/36/27/c9420403119dc975f7009ee8ef468be199360abf62acd0801900fa92ed6a/ormsgpack-1.11.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4e15b634be324fb18dab7aa82ab929a0d57d42c12650ae3dedd07d8d31b17733", size = 205841, upload-time = "2025-10-08T17:29:08.377Z" }, + { url = "https://files.pythonhosted.org/packages/fb/37/185a971de5d101db239b13986bb6682166b12f21f5c4c5fcfb7576e284d6/ormsgpack-1.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6329e6eae9dfe600962739a6e060ea82885ec58b8338875c5ac35080da970f94", size = 207388, upload-time = "2025-10-08T17:29:09.749Z" }, + { url = "https://files.pythonhosted.org/packages/62/e3/a260dea1db84fe34a91ce9bb20288c668e0166a3c7babb49f3d41c2362bd/ormsgpack-1.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b27546c28f92b9eb757620f7f1ed89fb7b07be3b9f4ba1b7de75761ec1c4bcc8", size = 377103, upload-time = "2025-10-08T17:29:10.818Z" }, + { url = "https://files.pythonhosted.org/packages/b3/48/beb244ff3cef07bfb0ca2212afcc672adbd630277b781f4756ce17ac6192/ormsgpack-1.11.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:26a17919d9144b4ac7112dbbadef07927abbe436be2cf99a703a19afe7dd5c8b", size = 470765, upload-time = "2025-10-08T17:29:12.062Z" }, + { url = "https://files.pythonhosted.org/packages/f5/47/e1560bb34d667c2a63f79223e463ef481b30d8add157b542d0736f1949bd/ormsgpack-1.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5352868ee4cdc00656bf216b56bc654f72ac3008eb36e12561f6337bb7104b45", size = 380860, upload-time = "2025-10-08T17:29:13.335Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/5e5f75fec428deb56ee1c18ef1713ad880b0070e3c19cdf5a221c7fb344b/ormsgpack-1.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:2ffe36f1f441a40949e8587f5aa3d3fc9f100576925aab667117403eab494338", size = 112050, upload-time = "2025-10-08T17:29:14.687Z" }, +] + +[[package]] +name = "ormsgpack" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/67/d5ef41c3b4a94400be801984ef7c7fc9623e1a82b643e74eeec367e7462b/ormsgpack-1.12.0.tar.gz", hash = "sha256:94be818fdbb0285945839b88763b269987787cb2f7ef280cad5d6ec815b7e608", size = 49959, upload-time = "2025-11-04T18:30:10.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/0c/8f45fcd22c95190b05d4fba71375d8f783a9e3b0b6aaf476812b693e3868/ormsgpack-1.12.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e08904c232358b94a682ccfbb680bc47d3fd5c424bb7dccb65974dd20c95e8e1", size = 369156, upload-time = "2025-11-04T18:29:17.629Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f8/c7adc093d4ceb05e38786906815f868210f808326c27f8124b4d9466a26b/ormsgpack-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9ed7a4b0037d69c8ba7e670e03ee65ae8d5c5114a409e73c5770d7fb5e4b895", size = 195743, upload-time = "2025-11-04T18:29:18.964Z" }, + { url = "https://files.pythonhosted.org/packages/fe/b8/bf002648fa6c150ed6157837b00303edafb35f65c352429463f83214de18/ormsgpack-1.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:db2928525b684f3f2af0367aef7ae8d20cde37fc5349c700017129d493a755aa", size = 206472, upload-time = "2025-11-04T18:29:19.951Z" }, + { url = "https://files.pythonhosted.org/packages/23/d6/1f445947c95a931bb189b7864a3f9dcbeebe7dcbc1b3c7387427b3779228/ormsgpack-1.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45f911d9c5b23d11e49ff03fc8f9566745a2b1a7d9033733a1c0a2fa9301cd60", size = 207959, upload-time = "2025-11-04T18:29:21.282Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9c/dd8ccd7553a5c1d0b4b69a0541611983a2f9bc2e8c5708a4bfffc0100468/ormsgpack-1.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:98c54ae6fd682b2aceb264505af9b2255f3df9d84e6e4369bc44d2110f1f311d", size = 377659, upload-time = "2025-11-04T18:29:22.561Z" }, + { url = "https://files.pythonhosted.org/packages/3d/08/3282d8f6330e742d4cdbcfbe2c100b403ae5526ae82a1c1b6a11b7967e37/ormsgpack-1.12.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:857ab987c3502de08258cc4baf0e87267cb2c80931601084e13df3c355b1ab9d", size = 471391, upload-time = "2025-11-04T18:29:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/94a2fbfd4837754bda7a099b6a23b9d40aba9e76e1af8b8fb8133612eb54/ormsgpack-1.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:27579d45dc502ee736238e1024559cb0a01aa72a3b68827448b8edf6a2dcdc9c", size = 381501, upload-time = "2025-11-04T18:29:24.771Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c6/1a9fa122cb5deb10b067bbaa43165b12291a914cc0ce364988ff17bbf405/ormsgpack-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:c78379d054760875540cf2e81f28da1bb78d09fda3eabdbeb6c53b3e297158cb", size = 112715, upload-time = "2025-11-04T18:29:26.016Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ba/3cae83cf36420c1c8dd294f16c852c03313aafe2439a165c4c6ac611b1d0/ormsgpack-1.12.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c40d86d77391b18dd34de5295e3de2b8ad818bcab9c9def4121c8ec5c9714ae4", size = 369159, upload-time = "2025-11-04T18:29:27.057Z" }, + { url = "https://files.pythonhosted.org/packages/97/d4/5e176309e01a8b9098d80201aac1eb7db9336c3b5b4fa6254a2bbb0d0fa0/ormsgpack-1.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:777b7fab364dc0f200bb382a98a385c8222ffa6a2333d627d763797326202c86", size = 195744, upload-time = "2025-11-04T18:29:28.069Z" }, + { url = "https://files.pythonhosted.org/packages/4f/83/6d80c8c5571639c000a39f38f77752dfaf9d9e552d775331e8d280f66a4e/ormsgpack-1.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b5089ad9dd5b3d3013b245a55e4abaea2f8ad70f4a78e1b002127b02340004", size = 206474, upload-time = "2025-11-04T18:29:29.034Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/940311e48dc0cfc3e212bd7007a21ed0825158638057687d804f2c5c2cca/ormsgpack-1.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deaf0c87cace7bc08fbf68c5cc66605b593df6427e9f4de235b2da358787e008", size = 207959, upload-time = "2025-11-04T18:29:30.315Z" }, + { url = "https://files.pythonhosted.org/packages/1a/e3/fbe94b0a311815343b86a95a0627e4901b11ff6fd522679ca29a2a88c99b/ormsgpack-1.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f62d476fe28bc5675d9aff30341bfa9f41d7de332c5b63fbbe9aaf6bb7ec74d4", size = 377666, upload-time = "2025-11-04T18:29:31.38Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3b/229cfa28076798ffb619aaa854b842de3f2ed5ea4e6509bf34d14c038c4d/ormsgpack-1.12.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ded7810095b887e28434f32f5a345d354e88cf851bab3c5435aeb86a718618d2", size = 471394, upload-time = "2025-11-04T18:29:32.521Z" }, + { url = "https://files.pythonhosted.org/packages/6b/bd/4eae4ab35586e4175c07acb5f98aec83aa9d8987f71ea0443aa900191bdf/ormsgpack-1.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f72a1dea0c4ae7c4101dcfbe8133f274a9d769d0b87fe5188db4fab07ffabaee", size = 381506, upload-time = "2025-11-04T18:29:33.533Z" }, + { url = "https://files.pythonhosted.org/packages/dd/51/f9d56d6d015cbfa1ce9a4358ca30a41744644f0cf606e060d7203efe5af8/ormsgpack-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:8f479bfef847255d7d0b12c7a198f6a21490155da2da3062e082ba370893d4a1", size = 112707, upload-time = "2025-11-04T18:29:34.898Z" }, + { url = "https://files.pythonhosted.org/packages/f4/07/bb189ef7072979f2f96e8716e952172efdce9c54930aa0814bec73aee19b/ormsgpack-1.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:3583ca410e4502144b2594170542e4bbef7b15643fd1208703ae820f11029036", size = 106533, upload-time = "2025-11-04T18:29:36.112Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f2/c1036b2775fcc0cfa5fd618c53bcd3b862ee07298fb627f03af4c7982f84/ormsgpack-1.12.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e0c1e08b64d99076fee155276097489b82cc56e8d5951c03c721a65a32f44494", size = 369538, upload-time = "2025-11-04T18:29:37.125Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ca/526c4ae02f3cb34621af91bf8282a10d666757c2e0c6ff391ff5d403d607/ormsgpack-1.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fd43bcb299131690b8e0677af172020b2ada8e625169034b42ac0c13adf84aa", size = 195872, upload-time = "2025-11-04T18:29:38.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/0f/83bb7968e9715f6a85be53d041b1e6324a05428f56b8b980dac866886871/ormsgpack-1.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0149d595341e22ead340bf281b2995c4cc7dc8d522a6b5f575fe17aa407604", size = 206469, upload-time = "2025-11-04T18:29:39.749Z" }, + { url = "https://files.pythonhosted.org/packages/02/e3/9e93ca1065f2d4af035804a842b1ff3025bab580c7918239bb225cd1fee2/ormsgpack-1.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f19a1b27d169deb553c80fd10b589fc2be1fc14cee779fae79fcaf40db04de2b", size = 208273, upload-time = "2025-11-04T18:29:40.769Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d8/6d6ef901b3a8b8f3ab8836b135a56eb7f66c559003e251d9530bedb12627/ormsgpack-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6f28896942d655064940dfe06118b7ce1e3468d051483148bf02c99ec157483a", size = 377839, upload-time = "2025-11-04T18:29:42.092Z" }, + { url = "https://files.pythonhosted.org/packages/4c/72/fcb704bfa4c2c3a37b647d597cc45a13cffc9d50baac635a9ad620731d29/ormsgpack-1.12.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9396efcfa48b4abbc06e44c5dbc3c4574a8381a80cb4cd01eea15d28b38c554e", size = 471446, upload-time = "2025-11-04T18:29:43.133Z" }, + { url = "https://files.pythonhosted.org/packages/84/f8/402e4e3eb997c2ee534c99bec4b5bb359c2a1f9edadf043e254a71e11378/ormsgpack-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:96586ed537a5fb386a162c4f9f7d8e6f76e07b38a990d50c73f11131e00ff040", size = 381783, upload-time = "2025-11-04T18:29:44.466Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8d/5897b700360bc00911b70ae5ef1134ee7abf5baa81a92a4be005917d3dfd/ormsgpack-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e70387112fb3870e4844de090014212cdcf1342f5022047aecca01ec7de05d7a", size = 112943, upload-time = "2025-11-04T18:29:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/5b/44/1e73649f79bb96d6cf9e5bcbac68b6216d238bba80af351c4c0cbcf7ee15/ormsgpack-1.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:d71290a23de5d4829610c42665d816c661ecad8979883f3f06b2e3ab9639962e", size = 106688, upload-time = "2025-11-04T18:29:46.411Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e8/35f11ce9313111488b26b3035e4cbe55caa27909c0b6c8b5b5cd59f9661e/ormsgpack-1.12.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:766f2f3b512d85cd375b26a8b1329b99843560b50b93d3880718e634ad4a5de5", size = 369574, upload-time = "2025-11-04T18:29:47.431Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/77461587f412d4e598d3687bafe23455ed0f26269f44be20252eddaa624e/ormsgpack-1.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84b285b1f3f185aad7da45641b873b30acfd13084cf829cf668c4c6480a81583", size = 195893, upload-time = "2025-11-04T18:29:48.735Z" }, + { url = "https://files.pythonhosted.org/packages/c6/67/e197ceb04c3b550589e5407fc9fdae10f4e2e2eba5fdac921a269e02e974/ormsgpack-1.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e23604fc79fe110292cb365f4c8232e64e63a34f470538be320feae3921f271b", size = 206503, upload-time = "2025-11-04T18:29:49.99Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b1/7fa8ba82a25cef678983c7976f85edeef5014f5c26495f338258e6a3cf1c/ormsgpack-1.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc32b156c113a0fae2975051417d8d9a7a5247c34b2d7239410c46b75ce9348a", size = 208257, upload-time = "2025-11-04T18:29:51.007Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b1/759e999390000d2589e6d0797f7265e6ec28378547075d28d3736248ab63/ormsgpack-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:94ac500dd10c20fa8b8a23bc55606250bfe711bf9716828d9f3d44dfd1f25668", size = 377852, upload-time = "2025-11-04T18:29:52.103Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/0af737c94272494d9d84a3c29cc42c973ef7fd2342917020906596db863c/ormsgpack-1.12.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c5201ff7ec24f721f813a182885a17064cffdbe46b2412685a52e6374a872c8f", size = 471456, upload-time = "2025-11-04T18:29:53.336Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ba/c81f0aa4f19fbf457213395945b672e6fde3ce777e3587456e7f0fca2147/ormsgpack-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a9740bb3839c9368aacae1cbcfc474ee6976458f41cc135372b7255d5206c953", size = 381813, upload-time = "2025-11-04T18:29:54.394Z" }, + { url = "https://files.pythonhosted.org/packages/ce/15/429c72d64323503fd42cc4ca8398930ded8aa8b3470df8a86b3bbae7a35c/ormsgpack-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ed37f29772432048b58174e920a1d4c4cde0404a5d448d3d8bbcc95d86a6918", size = 112949, upload-time = "2025-11-04T18:29:55.371Z" }, + { url = "https://files.pythonhosted.org/packages/55/b9/e72c451a40f8c57bfc229e0b8e536ecea7203c8f0a839676df2ffb605c62/ormsgpack-1.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:b03994bbec5d6d42e03d6604e327863f885bde67aa61e06107ce1fa5bdd3e71d", size = 106689, upload-time = "2025-11-04T18:29:56.262Z" }, + { url = "https://files.pythonhosted.org/packages/13/16/13eab1a75da531b359105fdee90dda0b6bd1ca0a09880250cf91d8bdfdea/ormsgpack-1.12.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0f3981ba3cba80656012090337e548e597799e14b41e3d0b595ab5ab05a23d7f", size = 369620, upload-time = "2025-11-04T18:29:57.255Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c1/cbcc38b7af4ce58d8893e56d3595c0c8dcd117093bf048f889cf351bdba0/ormsgpack-1.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:901f6f55184d6776dbd5183cbce14caf05bf7f467eef52faf9b094686980bf71", size = 195925, upload-time = "2025-11-04T18:29:58.34Z" }, + { url = "https://files.pythonhosted.org/packages/5c/59/4fa4dc0681490e12b75333440a1c0fd9741b0ebff272b1db4a29d35c2021/ormsgpack-1.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e13b15412571422b711b40f45e3fe6d993ea3314b5e97d1a853fe99226c5effc", size = 206594, upload-time = "2025-11-04T18:29:59.329Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/249770896bc32bb91b22c30256961f935d0915cbcf6e289a7fc961d9b14c/ormsgpack-1.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91fa8a452553a62e5fb3fbab471e7faf7b3bec3c87a2f355ebf3d7aab290fe4f", size = 208307, upload-time = "2025-11-04T18:30:00.377Z" }, + { url = "https://files.pythonhosted.org/packages/07/0a/e041a248cd72f2f4c07e155913e0a3ede4c86cf21a40ae6cd79f135f2847/ormsgpack-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74ec101f69624695eec4ce7c953192d97748254abe78fb01b591f06d529e1952", size = 377844, upload-time = "2025-11-04T18:30:01.389Z" }, + { url = "https://files.pythonhosted.org/packages/d8/71/6f7773e4ffda73a358ce4bba69b3e8bee9d40a7a06315e4c1cd7a3ea9d02/ormsgpack-1.12.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9bbf7896580848326c1f9bd7531f264e561f98db7e08e15aa75963d83832c717", size = 471572, upload-time = "2025-11-04T18:30:02.486Z" }, + { url = "https://files.pythonhosted.org/packages/65/29/af6769a4289c07acc71e7bda1d64fb31800563147d73142686e185e82348/ormsgpack-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7567917da613b8f8d591c1674e411fd3404bea41ef2b9a0e0a1e049c0f9406d7", size = 381842, upload-time = "2025-11-04T18:30:03.799Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/0a86195ee7a1a96c088aefc8504385e881cf56f4563ed81bafe21cbf1fb0/ormsgpack-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e418256c5d8622b8bc92861936f7c6a0131355e7bcad88a42102ae8227f8a1c", size = 113008, upload-time = "2025-11-04T18:30:04.777Z" }, + { url = "https://files.pythonhosted.org/packages/4c/57/fafc79e32f3087f6f26f509d80b8167516326bfea38d30502627c01617e0/ormsgpack-1.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:433ace29aa02713554f714c62a4e4dcad0c9e32674ba4f66742c91a4c3b1b969", size = 106648, upload-time = "2025-11-04T18:30:05.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cf/5d58d9b132128d2fe5d586355dde76af386554abef00d608f66b913bff1f/ormsgpack-1.12.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e57164be4ca34b64e210ec515059193280ac84df4d6f31a6fcbfb2fc8436de55", size = 369803, upload-time = "2025-11-04T18:30:06.728Z" }, + { url = "https://files.pythonhosted.org/packages/67/42/968a2da361eaff2e4cbb17c82c7599787babf16684110ad70409646cc1e4/ormsgpack-1.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:904f96289deaa92fc6440b122edc27c5bdc28234edd63717f6d853d88c823a83", size = 195991, upload-time = "2025-11-04T18:30:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/03/f0/9696c6c6cf8ad35170f0be8d0ef3523cc258083535f6c8071cb8235ebb8b/ormsgpack-1.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b291d086e524a1062d57d1b7b5a8bcaaf29caebf0212fec12fd86240bd33633", size = 208316, upload-time = "2025-11-04T18:30:08.663Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.3" +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/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/3d/9b8ca77b0f76fcdbf8bc6b72474e264283f461284ca84ac3fde570c6c49a/pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e", size = 2111197, upload-time = "2025-10-14T10:19:43.303Z" }, + { url = "https://files.pythonhosted.org/packages/59/92/b7b0fe6ed4781642232755cb7e56a86e2041e1292f16d9ae410a0ccee5ac/pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b", size = 1917909, upload-time = "2025-10-14T10:19:45.194Z" }, + { url = "https://files.pythonhosted.org/packages/52/8c/3eb872009274ffa4fb6a9585114e161aa1a0915af2896e2d441642929fe4/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd", size = 1969905, upload-time = "2025-10-14T10:19:46.567Z" }, + { url = "https://files.pythonhosted.org/packages/f4/21/35adf4a753bcfaea22d925214a0c5b880792e3244731b3f3e6fec0d124f7/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945", size = 2051938, upload-time = "2025-10-14T10:19:48.237Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d0/cdf7d126825e36d6e3f1eccf257da8954452934ede275a8f390eac775e89/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706", size = 2250710, upload-time = "2025-10-14T10:19:49.619Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1c/af1e6fd5ea596327308f9c8d1654e1285cc3d8de0d584a3c9d7705bf8a7c/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba", size = 2367445, upload-time = "2025-10-14T10:19:51.269Z" }, + { url = "https://files.pythonhosted.org/packages/d3/81/8cece29a6ef1b3a92f956ea6da6250d5b2d2e7e4d513dd3b4f0c7a83dfea/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b", size = 2072875, upload-time = "2025-10-14T10:19:52.671Z" }, + { url = "https://files.pythonhosted.org/packages/e3/37/a6a579f5fc2cd4d5521284a0ab6a426cc6463a7b3897aeb95b12f1ba607b/pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d", size = 2191329, upload-time = "2025-10-14T10:19:54.214Z" }, + { url = "https://files.pythonhosted.org/packages/ae/03/505020dc5c54ec75ecba9f41119fd1e48f9e41e4629942494c4a8734ded1/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700", size = 2151658, upload-time = "2025-10-14T10:19:55.843Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5d/2c0d09fb53aa03bbd2a214d89ebfa6304be7df9ed86ee3dc7770257f41ee/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6", size = 2316777, upload-time = "2025-10-14T10:19:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/ea/4b/c2c9c8f5e1f9c864b57d08539d9d3db160e00491c9f5ee90e1bfd905e644/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9", size = 2320705, upload-time = "2025-10-14T10:19:59.016Z" }, + { url = "https://files.pythonhosted.org/packages/28/c3/a74c1c37f49c0a02c89c7340fafc0ba816b29bd495d1a31ce1bdeacc6085/pydantic_core-2.41.4-cp310-cp310-win32.whl", hash = "sha256:0cf2a1f599efe57fa0051312774280ee0f650e11152325e41dfd3018ef2c1b57", size = 1975464, upload-time = "2025-10-14T10:20:00.581Z" }, + { url = "https://files.pythonhosted.org/packages/d6/23/5dd5c1324ba80303368f7569e2e2e1a721c7d9eb16acb7eb7b7f85cb1be2/pydantic_core-2.41.4-cp310-cp310-win_amd64.whl", hash = "sha256:a8c2e340d7e454dc3340d3d2e8f23558ebe78c98aa8f68851b04dcb7bc37abdc", size = 2024497, upload-time = "2025-10-14T10:20:03.018Z" }, + { url = "https://files.pythonhosted.org/packages/62/4c/f6cbfa1e8efacd00b846764e8484fe173d25b8dab881e277a619177f3384/pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80", size = 2109062, upload-time = "2025-10-14T10:20:04.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/40b72d3868896bfcd410e1bd7e516e762d326201c48e5b4a06446f6cf9e8/pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae", size = 1916301, upload-time = "2025-10-14T10:20:06.857Z" }, + { url = "https://files.pythonhosted.org/packages/94/4d/d203dce8bee7faeca791671c88519969d98d3b4e8f225da5b96dad226fc8/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827", size = 1968728, upload-time = "2025-10-14T10:20:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/65/f5/6a66187775df87c24d526985b3a5d78d861580ca466fbd9d4d0e792fcf6c/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f", size = 2050238, upload-time = "2025-10-14T10:20:09.766Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b9/78336345de97298cf53236b2f271912ce11f32c1e59de25a374ce12f9cce/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def", size = 2249424, upload-time = "2025-10-14T10:20:11.732Z" }, + { url = "https://files.pythonhosted.org/packages/99/bb/a4584888b70ee594c3d374a71af5075a68654d6c780369df269118af7402/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2", size = 2366047, upload-time = "2025-10-14T10:20:13.647Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8d/17fc5de9d6418e4d2ae8c675f905cdafdc59d3bf3bf9c946b7ab796a992a/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8", size = 2071163, upload-time = "2025-10-14T10:20:15.307Z" }, + { url = "https://files.pythonhosted.org/packages/54/e7/03d2c5c0b8ed37a4617430db68ec5e7dbba66358b629cd69e11b4d564367/pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265", size = 2190585, upload-time = "2025-10-14T10:20:17.3Z" }, + { url = "https://files.pythonhosted.org/packages/be/fc/15d1c9fe5ad9266a5897d9b932b7f53d7e5cfc800573917a2c5d6eea56ec/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c", size = 2150109, upload-time = "2025-10-14T10:20:19.143Z" }, + { url = "https://files.pythonhosted.org/packages/26/ef/e735dd008808226c83ba56972566138665b71477ad580fa5a21f0851df48/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a", size = 2315078, upload-time = "2025-10-14T10:20:20.742Z" }, + { url = "https://files.pythonhosted.org/packages/90/00/806efdcf35ff2ac0f938362350cd9827b8afb116cc814b6b75cf23738c7c/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e", size = 2318737, upload-time = "2025-10-14T10:20:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/41/7e/6ac90673fe6cb36621a2283552897838c020db343fa86e513d3f563b196f/pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03", size = 1974160, upload-time = "2025-10-14T10:20:23.817Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9d/7c5e24ee585c1f8b6356e1d11d40ab807ffde44d2db3b7dfd6d20b09720e/pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e", size = 2021883, upload-time = "2025-10-14T10:20:25.48Z" }, + { url = "https://files.pythonhosted.org/packages/33/90/5c172357460fc28b2871eb4a0fb3843b136b429c6fa827e4b588877bf115/pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db", size = 1968026, upload-time = "2025-10-14T10:20:27.039Z" }, + { url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887", size = 2099043, upload-time = "2025-10-14T10:20:28.561Z" }, + { url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2", size = 1910699, upload-time = "2025-10-14T10:20:30.217Z" }, + { url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999", size = 1952121, upload-time = "2025-10-14T10:20:32.246Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4", size = 2041590, upload-time = "2025-10-14T10:20:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f", size = 2219869, upload-time = "2025-10-14T10:20:35.965Z" }, + { url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b", size = 2345169, upload-time = "2025-10-14T10:20:37.627Z" }, + { url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47", size = 2070165, upload-time = "2025-10-14T10:20:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970", size = 2189067, upload-time = "2025-10-14T10:20:41.015Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed", size = 2132997, upload-time = "2025-10-14T10:20:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8", size = 2307187, upload-time = "2025-10-14T10:20:44.849Z" }, + { url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431", size = 2305204, upload-time = "2025-10-14T10:20:46.781Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd", size = 1972536, upload-time = "2025-10-14T10:20:48.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff", size = 2031132, upload-time = "2025-10-14T10:20:50.421Z" }, + { url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8", size = 1969483, upload-time = "2025-10-14T10:20:52.35Z" }, + { url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" }, + { url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" }, + { url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" }, + { url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" }, + { url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" }, + { url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" }, + { url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" }, + { url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" }, + { url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" }, + { url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" }, + { url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" }, + { url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" }, + { url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" }, + { url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" }, + { url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" }, + { url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" }, + { url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" }, + { url = "https://files.pythonhosted.org/packages/2c/36/f86d582be5fb47d4014506cd9ddd10a3979b6d0f2d237aa6ad3e7033b3ea/pydantic_core-2.41.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:646e76293345954acea6966149683047b7b2ace793011922208c8e9da12b0062", size = 2112444, upload-time = "2025-10-14T10:22:16.165Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e5/63c521dc2dd106ba6b5941c080617ea9db252f8a7d5625231e9d761bc28c/pydantic_core-2.41.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cc8e85a63085a137d286e2791037f5fdfff0aabb8b899483ca9c496dd5797338", size = 1938218, upload-time = "2025-10-14T10:22:19.443Z" }, + { url = "https://files.pythonhosted.org/packages/30/56/c84b638a3e6e9f5a612b9f5abdad73182520423de43669d639ed4f14b011/pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:692c622c8f859a17c156492783902d8370ac7e121a611bd6fe92cc71acf9ee8d", size = 1971449, upload-time = "2025-10-14T10:22:21.567Z" }, + { url = "https://files.pythonhosted.org/packages/99/c6/e974aade34fc7a0248fdfd0a373d62693502a407c596ab3470165e38183c/pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1e2906efb1031a532600679b424ef1d95d9f9fb507f813951f23320903adbd7", size = 2054023, upload-time = "2025-10-14T10:22:24.229Z" }, + { url = "https://files.pythonhosted.org/packages/4f/91/2507dda801f50980a38d1353c313e8f51349a42b008e63a4e45bf4620562/pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04e2f7f8916ad3ddd417a7abdd295276a0bf216993d9318a5d61cc058209166", size = 2251614, upload-time = "2025-10-14T10:22:26.498Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ad/05d886bc96938f4d31bed24e8d3fc3496d9aea7e77bcff6e4b93127c6de7/pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df649916b81822543d1c8e0e1d079235f68acdc7d270c911e8425045a8cfc57e", size = 2378807, upload-time = "2025-10-14T10:22:28.733Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0a/d26e1bb9a80b9fc12cc30d9288193fbc9e60a799e55843804ee37bd38a9c/pydantic_core-2.41.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c529f862fdba70558061bb936fe00ddbaaa0c647fd26e4a4356ef1d6561891", size = 2076891, upload-time = "2025-10-14T10:22:30.853Z" }, + { url = "https://files.pythonhosted.org/packages/d9/66/af014e3a294d9933ebfecf11a5d858709014bd2315fa9616195374dd82f0/pydantic_core-2.41.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3b4c5a1fd3a311563ed866c2c9b62da06cb6398bee186484ce95c820db71cb", size = 2192179, upload-time = "2025-10-14T10:22:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3e/79783f97024037d0ea6e1b3ebcd761463a925199e04ce2625727e9f27d06/pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6e0fc40d84448f941df9b3334c4b78fe42f36e3bf631ad54c3047a0cdddc2514", size = 2153067, upload-time = "2025-10-14T10:22:35.792Z" }, + { url = "https://files.pythonhosted.org/packages/b3/97/ea83b0f87d9e742405fb687d5682e7a26334eef2c82a2de06bfbdc305fab/pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:44e7625332683b6c1c8b980461475cde9595eff94447500e80716db89b0da005", size = 2319048, upload-time = "2025-10-14T10:22:38.144Z" }, + { url = "https://files.pythonhosted.org/packages/64/4a/36d8c966a0b086362ac10a7ee75978ed15c5f2dfdfc02a1578d19d3802fb/pydantic_core-2.41.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:170ee6835f6c71081d031ef1c3b4dc4a12b9efa6a9540f93f95b82f3c7571ae8", size = 2321830, upload-time = "2025-10-14T10:22:40.337Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6e/d80cc4909dde5f6842861288aa1a7181e7afbfc50940c862ed2848df15bd/pydantic_core-2.41.4-cp39-cp39-win32.whl", hash = "sha256:3adf61415efa6ce977041ba9745183c0e1f637ca849773afa93833e04b163feb", size = 1976706, upload-time = "2025-10-14T10:22:42.61Z" }, + { url = "https://files.pythonhosted.org/packages/29/ee/5bda8d960d4a8b24a7eeb8a856efa9c865a7a6cab714ed387b29507dc278/pydantic_core-2.41.4-cp39-cp39-win_amd64.whl", hash = "sha256:a238dd3feee263eeaeb7dc44aea4ba1364682c4f9f9467e6af5596ba322c2332", size = 2027640, upload-time = "2025-10-14T10:22:44.907Z" }, + { url = "https://files.pythonhosted.org/packages/b0/12/5ba58daa7f453454464f92b3ca7b9d7c657d8641c48e370c3ebc9a82dd78/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a1b2cfec3879afb742a7b0bcfa53e4f22ba96571c9e54d6a3afe1052d17d843b", size = 2122139, upload-time = "2025-10-14T10:22:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/21/fb/6860126a77725c3108baecd10fd3d75fec25191d6381b6eb2ac660228eac/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:d175600d975b7c244af6eb9c9041f10059f20b8bbffec9e33fdd5ee3f67cdc42", size = 1936674, upload-time = "2025-10-14T10:22:49.555Z" }, + { url = "https://files.pythonhosted.org/packages/de/be/57dcaa3ed595d81f8757e2b44a38240ac5d37628bce25fb20d02c7018776/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f184d657fa4947ae5ec9c47bd7e917730fa1cbb78195037e32dcbab50aca5ee", size = 1956398, upload-time = "2025-10-14T10:22:52.19Z" }, + { url = "https://files.pythonhosted.org/packages/2f/1d/679a344fadb9695f1a6a294d739fbd21d71fa023286daeea8c0ed49e7c2b/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed810568aeffed3edc78910af32af911c835cc39ebbfacd1f0ab5dd53028e5c", size = 2138674, upload-time = "2025-10-14T10:22:54.499Z" }, + { url = "https://files.pythonhosted.org/packages/c4/48/ae937e5a831b7c0dc646b2ef788c27cd003894882415300ed21927c21efa/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537", size = 2112087, upload-time = "2025-10-14T10:22:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/5e/db/6db8073e3d32dae017da7e0d16a9ecb897d0a4d92e00634916e486097961/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94", size = 1920387, upload-time = "2025-10-14T10:22:59.342Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c1/dd3542d072fcc336030d66834872f0328727e3b8de289c662faa04aa270e/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c", size = 1951495, upload-time = "2025-10-14T10:23:02.089Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c6/db8d13a1f8ab3f1eb08c88bd00fd62d44311e3456d1e85c0e59e0a0376e7/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335", size = 2139008, upload-time = "2025-10-14T10:23:04.539Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d4/912e976a2dd0b49f31c98a060ca90b353f3b73ee3ea2fd0030412f6ac5ec/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00", size = 2106739, upload-time = "2025-10-14T10:23:06.934Z" }, + { url = "https://files.pythonhosted.org/packages/71/f0/66ec5a626c81eba326072d6ee2b127f8c139543f1bf609b4842978d37833/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9", size = 1932549, upload-time = "2025-10-14T10:23:09.24Z" }, + { url = "https://files.pythonhosted.org/packages/c4/af/625626278ca801ea0a658c2dcf290dc9f21bb383098e99e7c6a029fccfc0/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2", size = 2135093, upload-time = "2025-10-14T10:23:11.626Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/2fba049f54e0f4975fef66be654c597a1d005320fa141863699180c7697d/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258", size = 2187971, upload-time = "2025-10-14T10:23:14.437Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/65ab839a2dfcd3b949202f9d920c34f9de5a537c3646662bdf2f7d999680/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347", size = 2147939, upload-time = "2025-10-14T10:23:16.831Z" }, + { url = "https://files.pythonhosted.org/packages/44/58/627565d3d182ce6dfda18b8e1c841eede3629d59c9d7cbc1e12a03aeb328/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa", size = 2311400, upload-time = "2025-10-14T10:23:19.234Z" }, + { url = "https://files.pythonhosted.org/packages/24/06/8a84711162ad5a5f19a88cead37cca81b4b1f294f46260ef7334ae4f24d3/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a", size = 2316840, upload-time = "2025-10-14T10:23:21.738Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8b/b7bb512a4682a2f7fbfae152a755d37351743900226d29bd953aaf870eaa/pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3619320641fd212aaf5997b6ca505e97540b7e16418f4a241f44cdf108ffb50d", size = 2149135, upload-time = "2025-10-14T10:23:24.379Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7d/138e902ed6399b866f7cfe4435d22445e16fff888a1c00560d9dc79a780f/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5", size = 2104721, upload-time = "2025-10-14T10:23:26.906Z" }, + { url = "https://files.pythonhosted.org/packages/47/13/0525623cf94627f7b53b4c2034c81edc8491cbfc7c28d5447fa318791479/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2", size = 1931608, upload-time = "2025-10-14T10:23:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f9/744bc98137d6ef0a233f808bfc9b18cf94624bf30836a18d3b05d08bf418/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd", size = 2132986, upload-time = "2025-10-14T10:23:32.057Z" }, + { url = "https://files.pythonhosted.org/packages/17/c8/629e88920171173f6049386cc71f893dff03209a9ef32b4d2f7e7c264bcf/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c", size = 2187516, upload-time = "2025-10-14T10:23:34.871Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0f/4f2734688d98488782218ca61bcc118329bf5de05bb7fe3adc7dd79b0b86/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405", size = 2146146, upload-time = "2025-10-14T10:23:37.342Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f2/ab385dbd94a052c62224b99cf99002eee99dbec40e10006c78575aead256/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8", size = 2311296, upload-time = "2025-10-14T10:23:40.145Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8e/e4f12afe1beeb9823bba5375f8f258df0cc61b056b0195fb1cf9f62a1a58/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308", size = 2315386, upload-time = "2025-10-14T10:23:42.624Z" }, + { url = "https://files.pythonhosted.org/packages/48/f7/925f65d930802e3ea2eb4d5afa4cb8730c8dc0d2cb89a59dc4ed2fcb2d74/pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f", size = 2147775, upload-time = "2025-10-14T10:23:45.406Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, + { name = "coverage", version = "7.11.0", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/62/50b7727004dfe361104dfbf898c45a9a2fdfad8c72c04ae62900224d6ecf/ruff-0.14.3.tar.gz", hash = "sha256:4ff876d2ab2b161b6de0aa1f5bd714e8e9b4033dc122ee006925fbacc4f62153", size = 5558687, upload-time = "2025-10-31T00:26:26.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8e/0c10ff1ea5d4360ab8bfca4cb2c9d979101a391f3e79d2616c9bf348cd26/ruff-0.14.3-py3-none-linux_armv6l.whl", hash = "sha256:876b21e6c824f519446715c1342b8e60f97f93264012de9d8d10314f8a79c371", size = 12535613, upload-time = "2025-10-31T00:25:44.302Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c8/6724f4634c1daf52409fbf13fefda64aa9c8f81e44727a378b7b73dc590b/ruff-0.14.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6fd8c79b457bedd2abf2702b9b472147cd860ed7855c73a5247fa55c9117654", size = 12855812, upload-time = "2025-10-31T00:25:47.793Z" }, + { url = "https://files.pythonhosted.org/packages/de/03/db1bce591d55fd5f8a08bb02517fa0b5097b2ccabd4ea1ee29aa72b67d96/ruff-0.14.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:71ff6edca490c308f083156938c0c1a66907151263c4abdcb588602c6e696a14", size = 11944026, upload-time = "2025-10-31T00:25:49.657Z" }, + { url = "https://files.pythonhosted.org/packages/0b/75/4f8dbd48e03272715d12c87dc4fcaaf21b913f0affa5f12a4e9c6f8a0582/ruff-0.14.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:786ee3ce6139772ff9272aaf43296d975c0217ee1b97538a98171bf0d21f87ed", size = 12356818, upload-time = "2025-10-31T00:25:51.949Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9b/506ec5b140c11d44a9a4f284ea7c14ebf6f8b01e6e8917734a3325bff787/ruff-0.14.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cd6291d0061811c52b8e392f946889916757610d45d004e41140d81fb6cd5ddc", size = 12336745, upload-time = "2025-10-31T00:25:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e1/c560d254048c147f35e7f8131d30bc1f63a008ac61595cf3078a3e93533d/ruff-0.14.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a497ec0c3d2c88561b6d90f9c29f5ae68221ac00d471f306fa21fa4264ce5fcd", size = 13101684, upload-time = "2025-10-31T00:25:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/a5/32/e310133f8af5cd11f8cc30f52522a3ebccc5ea5bff4b492f94faceaca7a8/ruff-0.14.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e231e1be58fc568950a04fbe6887c8e4b85310e7889727e2b81db205c45059eb", size = 14535000, upload-time = "2025-10-31T00:25:58.397Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a1/7b0470a22158c6d8501eabc5e9b6043c99bede40fa1994cadf6b5c2a61c7/ruff-0.14.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:469e35872a09c0e45fecf48dd960bfbce056b5db2d5e6b50eca329b4f853ae20", size = 14156450, upload-time = "2025-10-31T00:26:00.889Z" }, + { url = "https://files.pythonhosted.org/packages/0a/96/24bfd9d1a7f532b560dcee1a87096332e461354d3882124219bcaff65c09/ruff-0.14.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d6bc90307c469cb9d28b7cfad90aaa600b10d67c6e22026869f585e1e8a2db0", size = 13568414, upload-time = "2025-10-31T00:26:03.291Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e7/138b883f0dfe4ad5b76b58bf4ae675f4d2176ac2b24bdd81b4d966b28c61/ruff-0.14.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2f8a0bbcffcfd895df39c9a4ecd59bb80dca03dc43f7fb63e647ed176b741e", size = 13315293, upload-time = "2025-10-31T00:26:05.708Z" }, + { url = "https://files.pythonhosted.org/packages/33/f4/c09bb898be97b2eb18476b7c950df8815ef14cf956074177e9fbd40b7719/ruff-0.14.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:678fdd7c7d2d94851597c23ee6336d25f9930b460b55f8598e011b57c74fd8c5", size = 13539444, upload-time = "2025-10-31T00:26:08.09Z" }, + { url = "https://files.pythonhosted.org/packages/9c/aa/b30a1db25fc6128b1dd6ff0741fa4abf969ded161599d07ca7edd0739cc0/ruff-0.14.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1ec1ac071e7e37e0221d2f2dbaf90897a988c531a8592a6a5959f0603a1ecf5e", size = 12252581, upload-time = "2025-10-31T00:26:10.297Z" }, + { url = "https://files.pythonhosted.org/packages/da/13/21096308f384d796ffe3f2960b17054110a9c3828d223ca540c2b7cc670b/ruff-0.14.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afcdc4b5335ef440d19e7df9e8ae2ad9f749352190e96d481dc501b753f0733e", size = 12307503, upload-time = "2025-10-31T00:26:12.646Z" }, + { url = "https://files.pythonhosted.org/packages/cb/cc/a350bac23f03b7dbcde3c81b154706e80c6f16b06ff1ce28ed07dc7b07b0/ruff-0.14.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7bfc42f81862749a7136267a343990f865e71fe2f99cf8d2958f684d23ce3dfa", size = 12675457, upload-time = "2025-10-31T00:26:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/cb/76/46346029fa2f2078826bc88ef7167e8c198e58fe3126636e52f77488cbba/ruff-0.14.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a65e448cfd7e9c59fae8cf37f9221585d3354febaad9a07f29158af1528e165f", size = 13403980, upload-time = "2025-10-31T00:26:17.81Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a4/35f1ef68c4e7b236d4a5204e3669efdeefaef21f0ff6a456792b3d8be438/ruff-0.14.3-py3-none-win32.whl", hash = "sha256:f3d91857d023ba93e14ed2d462ab62c3428f9bbf2b4fbac50a03ca66d31991f7", size = 12500045, upload-time = "2025-10-31T00:26:20.503Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/51960ae340823c9859fb60c63301d977308735403e2134e17d1d2858c7fb/ruff-0.14.3-py3-none-win_amd64.whl", hash = "sha256:d7b7006ac0756306db212fd37116cce2bd307e1e109375e1c6c106002df0ae5f", size = 13594005, upload-time = "2025-10-31T00:26:22.533Z" }, + { url = "https://files.pythonhosted.org/packages/b7/73/4de6579bac8e979fca0a77e54dec1f1e011a0d268165eb8a9bc0982a6564/ruff-0.14.3-py3-none-win_arm64.whl", hash = "sha256:26eb477ede6d399d898791d01961e16b86f02bc2486d0d1a7a9bb2379d055dc1", size = 12590017, upload-time = "2025-10-31T00:26:24.52Z" }, +] + +[[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 = "sounddevice" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/4f/28e734898b870db15b6474453f19813d3c81b91c806d9e6f867bd6e4dd03/sounddevice-0.5.3.tar.gz", hash = "sha256:cbac2b60198fbab84533697e7c4904cc895ec69d5fb3973556c9eb74a4629b2c", size = 53465, upload-time = "2025-10-19T13:23:57.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/e7/9020e9f0f3df00432728f4c4044387468a743e3d9a4f91123d77be10010e/sounddevice-0.5.3-py3-none-any.whl", hash = "sha256:ea7738baa0a9f9fef7390f649e41c9f2c8ada776180e56c2ffd217133c92a806", size = 32670, upload-time = "2025-10-19T13:23:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/2f/39/714118f8413e0e353436914f2b976665161f1be2b6483ac15a8f61484c14/sounddevice-0.5.3-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:278dc4451fff70934a176df048b77d80d7ce1623a6ec9db8b34b806f3112f9c2", size = 108306, upload-time = "2025-10-19T13:23:53.277Z" }, + { url = "https://files.pythonhosted.org/packages/f5/74/52186e3e5c833d00273f7949a9383adff93692c6e02406bf359cb4d3e921/sounddevice-0.5.3-py3-none-win32.whl", hash = "sha256:845d6927bcf14e84be5292a61ab3359cf8e6b9145819ec6f3ac2619ff089a69c", size = 312882, upload-time = "2025-10-19T13:23:54.829Z" }, + { url = "https://files.pythonhosted.org/packages/66/c7/16123d054aef6d445176c9122bfbe73c11087589b2413cab22aff5a7839a/sounddevice-0.5.3-py3-none-win_amd64.whl", hash = "sha256:f55ad20082efc2bdec06928e974fbcae07bc6c405409ae1334cefe7d377eb687", size = 364025, upload-time = "2025-10-19T13:23:56.362Z" }, +] + +[[package]] +name = "soundfile" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/28/e2a36573ccbcf3d57c00626a21fe51989380636e821b341d36ccca0c1c3a/soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445", size = 25751, upload-time = "2025-01-25T09:16:44.235Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/73e97a5b3cc46bba7ff8650a1504348fa1863a6f9d57d7001c6b67c5f20e/soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33", size = 1142250, upload-time = "2025-01-25T09:16:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e5/58fd1a8d7b26fc113af244f966ee3aecf03cb9293cb935daaddc1e455e18/soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593", size = 1101406, upload-time = "2025-01-25T09:16:49.662Z" }, + { url = "https://files.pythonhosted.org/packages/58/ae/c0e4a53d77cf6e9a04179535766b3321b0b9ced5f70522e4caf9329f0046/soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb", size = 1235729, upload-time = "2025-01-25T09:16:53.018Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/70bdd9579b35003a489fc850b5047beeda26328053ebadc1fb60f320f7db/soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618", size = 1313646, upload-time = "2025-01-25T09:16:54.872Z" }, + { url = "https://files.pythonhosted.org/packages/fe/df/8c11dc4dfceda14e3003bb81a0d0edcaaf0796dd7b4f826ea3e532146bba/soundfile-0.13.1-py2.py3-none-win32.whl", hash = "sha256:c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5", size = 899881, upload-time = "2025-01-25T09:16:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/14/e9/6b761de83277f2f02ded7e7ea6f07828ec78e4b229b80e4ca55dd205b9dc/soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9", size = 1019162, upload-time = "2025-01-25T09:16:59.573Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[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 = "wsproto" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425, upload-time = "2022-08-23T19:58:21.447Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226, upload-time = "2022-08-23T19:58:19.96Z" }, +] From d579bfed79608941249fe4717a21b101eb6c735e Mon Sep 17 00:00:00 2001 From: James Ding Date: Sat, 8 Nov 2025 01:26:20 -0600 Subject: [PATCH 3/5] feat: add output directory and save audio fixture for integration tests Signed-off-by: James Ding --- .github/workflows/python.yml | 7 ++++ .gitignore | 5 ++- tests/integration/conftest.py | 31 ++++++++++++++++ tests/integration/test_tts_integration.py | 45 +++++++++++++++++------ 4 files changed, 75 insertions(+), 13 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 3f404f4..f6fc0a4 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -85,6 +85,13 @@ jobs: env: FISH_AUDIO_API_KEY: ${{ secrets.FISH_AUDIO_API_KEY }} + - name: Upload Test Artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-audio-output + path: output/ + build: name: Build Package runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 3a8816c..259de84 100644 --- a/.gitignore +++ b/.gitignore @@ -159,4 +159,7 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ + +# Test output +output/ diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 2d95aae..b0bf5d4 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,6 +1,7 @@ """Fixtures for integration tests.""" import os +from pathlib import Path import pytest from dotenv import load_dotenv @@ -9,6 +10,10 @@ load_dotenv() +# Create output directory for test audio files +OUTPUT_DIR = Path("output") +OUTPUT_DIR.mkdir(exist_ok=True) + @pytest.fixture def api_key(): @@ -33,3 +38,29 @@ async def async_client(api_key): client = AsyncFishAudio(api_key=api_key) yield client await client.close() + + +@pytest.fixture +def save_audio(): + """Fixture that provides a helper to save audio chunks to the output directory. + + Returns: + A callable that takes audio chunks and filename and saves to output/ + """ + + def _save(audio_chunks: list[bytes], filename: str) -> Path: + """Save audio chunks to output directory. + + Args: + audio_chunks: List of audio byte chunks + filename: Name of the output file (including extension) + + Returns: + Path to the saved file + """ + complete_audio = b"".join(audio_chunks) + output_file = OUTPUT_DIR / filename + output_file.write_bytes(complete_audio) + return output_file + + return _save diff --git a/tests/integration/test_tts_integration.py b/tests/integration/test_tts_integration.py index 1d4890f..8d00d77 100644 --- a/tests/integration/test_tts_integration.py +++ b/tests/integration/test_tts_integration.py @@ -5,13 +5,13 @@ import pytest from fishaudio.types import Prosody, TTSConfig -from fishaudio.types.shared import Model +from fishaudio.types.shared import AudioFormat, Model class TestTTSIntegration: """Test TTS with real API.""" - def test_basic_tts(self, client): + def test_basic_tts(self, client, save_audio): """Test basic text-to-speech generation.""" audio_chunks = list(client.tts.convert(text="Hello, this is a test.")) @@ -20,18 +20,24 @@ def test_basic_tts(self, client): complete_audio = b"".join(audio_chunks) assert len(complete_audio) > 1000 # Should have substantial audio data - def test_tts_with_different_formats(self, client): + # Write to output directory + save_audio(audio_chunks, "test_basic_tts.mp3") + + def test_tts_with_different_formats(self, client, save_audio): """Test TTS with different audio formats.""" - formats = ["mp3", "wav", "pcm"] + formats = get_args(AudioFormat) for fmt in formats: config = TTSConfig(format=fmt, chunk_length=100) audio_chunks = list( - client.tts.convert(text="Testing format", config=config) + client.tts.convert(text=f"Testing format {fmt}", config=config) ) assert len(audio_chunks) > 0, f"Failed for format: {fmt}" - def test_tts_with_prosody(self, client): + # Write to output directory + save_audio(audio_chunks, f"test_format_{fmt}.{fmt}") + + def test_tts_with_prosody(self, client, save_audio): """Test TTS with prosody settings.""" prosody = Prosody(speed=1.2, volume=0.5) config = TTSConfig(prosody=prosody) @@ -42,21 +48,27 @@ def test_tts_with_prosody(self, client): assert len(audio_chunks) > 0 - def test_tts_with_different_backends(self, client): - """Test TTS with different backend models.""" + # Write to output directory + save_audio(audio_chunks, "test_prosody.mp3") + + def test_tts_with_different_models(self, client, save_audio): + """Test TTS with different models.""" models = get_args(Model) for model in models: try: audio_chunks = list( - client.tts.convert(text="Testing model", model=model) + client.tts.convert(text=f"Testing model {model}", model=model) ) assert len(audio_chunks) > 0, f"Failed for model: {model}" + + # Write to output directory + save_audio(audio_chunks, f"test_model_{model}.mp3") except Exception as e: # Some models might not be available pytest.skip(f"Model {model} not available: {e}") - def test_tts_longer_text(self, client): + def test_tts_longer_text(self, client, save_audio): """Test TTS with longer text.""" long_text = "This is a longer piece of text for testing. " * 10 config = TTSConfig(chunk_length=200) @@ -68,6 +80,9 @@ def test_tts_longer_text(self, client): # Longer text should produce more audio assert len(complete_audio) > 5000 + # Write to output directory + save_audio(audio_chunks, "test_longer_text.mp3") + def test_tts_empty_text_should_fail(self, client): """Test that empty text is handled.""" # This might succeed with silence or fail - test behavior @@ -84,7 +99,7 @@ class TestAsyncTTSIntegration: """Test async TTS with real API.""" @pytest.mark.asyncio - async def test_basic_async_tts(self, async_client): + async def test_basic_async_tts(self, async_client, save_audio): """Test basic async text-to-speech generation.""" audio_chunks = [] async for chunk in async_client.tts.convert(text="Hello from async"): @@ -94,8 +109,11 @@ async def test_basic_async_tts(self, async_client): complete_audio = b"".join(audio_chunks) assert len(complete_audio) > 1000 + # Write to output directory + save_audio(audio_chunks, "test_async_basic.mp3") + @pytest.mark.asyncio - async def test_async_tts_with_prosody(self, async_client): + async def test_async_tts_with_prosody(self, async_client, save_audio): """Test async TTS with prosody.""" prosody = Prosody(speed=0.8, volume=-0.2) config = TTSConfig(prosody=prosody) @@ -107,3 +125,6 @@ async def test_async_tts_with_prosody(self, async_client): audio_chunks.append(chunk) assert len(audio_chunks) > 0 + + # Write to output directory + save_audio(audio_chunks, "test_async_prosody.mp3") From 10e18b04aba2f81aaebfa6152e3f60f1ff01af40 Mon Sep 17 00:00:00 2001 From: James Ding Date: Sat, 8 Nov 2025 10:20:35 -0600 Subject: [PATCH 4/5] chore: add Apache 2.0 license and update pyproject.toml to reflect new licensing --- LICENSE | 201 +++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 4 +- 2 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/pyproject.toml b/pyproject.toml index 86e9bca..a18d34e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,12 +14,12 @@ dependencies = [ ] requires-python = ">=3.9" readme = "README.md" -license = {text = "MIT"} +license = {text = "Apache-2.0"} keywords = ["fish-audio", "tts", "text-to-speech", "voice-cloning", "ai", "speech-synthesis"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", + "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", From 58b14580d850ca1bf02767af5904350f8911c5a7 Mon Sep 17 00:00:00 2001 From: James Ding Date: Sat, 8 Nov 2025 15:39:37 -0600 Subject: [PATCH 5/5] feat: refactor WebSocket client handling and improve audio event processing --- src/fishaudio/core/client_wrapper.py | 30 +- src/fishaudio/resources/realtime.py | 45 ++- src/fishaudio/resources/tts.py | 62 ++-- tests/integration/conftest.py | 11 +- .../test_tts_websocket_integration.py | 258 +++++++++++++ tests/unit/test_realtime.py | 339 ++++++++++++++++++ tests/unit/test_tts_realtime.py | 50 +-- uv.lock | 4 +- 8 files changed, 683 insertions(+), 116 deletions(-) create mode 100644 tests/integration/test_tts_websocket_integration.py create mode 100644 tests/unit/test_realtime.py diff --git a/src/fishaudio/core/client_wrapper.py b/src/fishaudio/core/client_wrapper.py index 7fa78c9..2173f28 100644 --- a/src/fishaudio/core/client_wrapper.py +++ b/src/fishaudio/core/client_wrapper.py @@ -150,17 +150,10 @@ def request( return response - def create_websocket_client(self) -> httpx.Client: - """ - Create an httpx.Client configured for WebSocket connections. - - Returns: - Configured httpx.Client with authentication - """ - return httpx.Client( - base_url=self.base_url, - headers={"Authorization": f"Bearer {self.api_key}"}, - ) + @property + def client(self) -> httpx.Client: + """Get underlying httpx.Client for advanced usage (e.g., WebSockets).""" + return self._client def close(self) -> None: """Close the HTTP client.""" @@ -229,17 +222,10 @@ async def request( return response - def create_websocket_client(self) -> httpx.AsyncClient: - """ - Create an httpx.AsyncClient configured for WebSocket connections. - - Returns: - Configured httpx.AsyncClient with authentication - """ - return httpx.AsyncClient( - base_url=self.base_url, - headers={"Authorization": f"Bearer {self.api_key}"}, - ) + @property + def client(self) -> httpx.AsyncClient: + """Get underlying httpx.AsyncClient for advanced usage (e.g., WebSockets).""" + return self._client async def close(self) -> None: """Close the HTTP client.""" diff --git a/src/fishaudio/resources/realtime.py b/src/fishaudio/resources/realtime.py index 2821b48..5036549 100644 --- a/src/fishaudio/resources/realtime.py +++ b/src/fishaudio/resources/realtime.py @@ -8,6 +8,19 @@ from ..exceptions import WebSocketError +def _should_stop(data: Dict[str, Any]) -> bool: + """ + Check if WebSocket event signals stream should stop. + + Args: + data: Unpacked WebSocket message data + + Returns: + True if stream should stop, False otherwise + """ + return data.get("event") == "finish" and data.get("reason") == "stop" + + def _process_audio_event(data: Dict[str, Any]) -> Optional[bytes]: """ Process a WebSocket audio event. @@ -16,17 +29,15 @@ def _process_audio_event(data: Dict[str, Any]) -> Optional[bytes]: data: Unpacked WebSocket message data Returns: - Audio bytes if audio event, None if should stop + Audio bytes if audio event, None for unknown events Raises: WebSocketError: If finish event has error reason """ - if data["event"] == "audio": - return data["audio"] - elif data["event"] == "finish" and data["reason"] == "error": + if data.get("event") == "audio": + return data.get("audio") + elif data.get("event") == "finish" and data.get("reason") == "error": raise WebSocketError("WebSocket stream ended with error") - elif data["event"] == "finish" and data["reason"] == "stop": - return None # Signal to stop return None # Ignore unknown events @@ -35,6 +46,7 @@ def iter_websocket_audio(ws) -> Iterator[bytes]: Process WebSocket audio messages (sync). Receives messages from WebSocket, yields audio chunks, handles errors. + Unknown events are ignored and iteration continues. Args: ws: WebSocket connection from httpx_ws.connect_ws @@ -49,10 +61,14 @@ def iter_websocket_audio(ws) -> Iterator[bytes]: try: message = ws.receive_bytes() data = ormsgpack.unpackb(message) - audio = _process_audio_event(data) - if audio is None: + + if _should_stop(data): break - yield audio + + audio = _process_audio_event(data) + if audio is not None: + yield audio + except WebSocketDisconnect: raise WebSocketError("WebSocket disconnected unexpectedly") @@ -62,6 +78,7 @@ async def aiter_websocket_audio(ws) -> AsyncIterator[bytes]: Process WebSocket audio messages (async). Receives messages from WebSocket, yields audio chunks, handles errors. + Unknown events are ignored and iteration continues. Args: ws: WebSocket connection from httpx_ws.aconnect_ws @@ -76,9 +93,13 @@ async def aiter_websocket_audio(ws) -> AsyncIterator[bytes]: try: message = await ws.receive_bytes() data = ormsgpack.unpackb(message) - audio = _process_audio_event(data) - if audio is None: + + if _should_stop(data): break - yield audio + + audio = _process_audio_event(data) + if audio is not None: + yield audio + except WebSocketDisconnect: raise WebSocketError("WebSocket disconnected unexpectedly") diff --git a/src/fishaudio/resources/tts.py b/src/fishaudio/resources/tts.py index 73cb7f5..ea73141 100644 --- a/src/fishaudio/resources/tts.py +++ b/src/fishaudio/resources/tts.py @@ -158,14 +158,17 @@ def text_generator(): # Build TTSRequest from config tts_request = _config_to_tts_request(config, text="") - # Create WebSocket client - ws_client = self._client.create_websocket_client() executor = ThreadPoolExecutor(max_workers=max_workers) try: ws: WebSocketSession with connect_ws( - "/v1/tts/live", client=ws_client, headers={"model": model} + "/v1/tts/live", + client=self._client.client, + headers={ + "model": model, + "Authorization": f"Bearer {self._client.api_key}", + }, ) as ws: def sender(): @@ -186,7 +189,6 @@ def sender(): sender_future.result() finally: - ws_client.close() executor.shutdown(wait=False) @@ -298,31 +300,27 @@ async def text_generator(): # Build TTSRequest from config tts_request = _config_to_tts_request(config, text="") - # Create WebSocket client - ws_client = self._client.create_websocket_client() - - try: - ws: AsyncWebSocketSession - async with aconnect_ws( - "/v1/tts/live", client=ws_client, headers={"model": model} - ) as ws: - - async def sender(): - await ws.send_bytes( - ormsgpack.packb(StartEvent(request=tts_request).model_dump()) - ) - # Normalize strings to TextEvent - async for item in text_stream: - event = _normalize_to_event(item) - await ws.send_bytes(ormsgpack.packb(event.model_dump())) - await ws.send_bytes(ormsgpack.packb(CloseEvent().model_dump())) - - sender_task = asyncio.create_task(sender()) - - # Process incoming audio messages - async for audio_chunk in aiter_websocket_audio(ws): - yield audio_chunk - - await sender_task - finally: - await ws_client.aclose() + ws: AsyncWebSocketSession + async with aconnect_ws( + "/v1/tts/live", + client=self._client.client, + headers={"model": model, "Authorization": f"Bearer {self._client.api_key}"}, + ) as ws: + + async def sender(): + await ws.send_bytes( + ormsgpack.packb(StartEvent(request=tts_request).model_dump()) + ) + # Normalize strings to TextEvent + async for item in text_stream: + event = _normalize_to_event(item) + await ws.send_bytes(ormsgpack.packb(event.model_dump())) + await ws.send_bytes(ormsgpack.packb(CloseEvent().model_dump())) + + sender_task = asyncio.create_task(sender()) + + # Process incoming audio messages + async for audio_chunk in aiter_websocket_audio(ws): + yield audio_chunk + + await sender_task diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index b0bf5d4..00ec2d8 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -14,6 +14,9 @@ OUTPUT_DIR = Path("output") OUTPUT_DIR.mkdir(exist_ok=True) +# Test reference voice ID for testing reference voice features +TEST_REFERENCE_ID = "ca3007f96ae7499ab87d27ea3599956a" + @pytest.fixture def api_key(): @@ -24,12 +27,16 @@ def api_key(): return key -@pytest.fixture +@pytest.fixture(scope="function") def client(api_key): - """Sync Fish Audio client.""" + """Sync Fish Audio client (function-scoped for test isolation).""" + import time + client = FishAudio(api_key=api_key) yield client client.close() + # Brief delay to avoid API rate limits on WebSocket connections + time.sleep(0.3) @pytest.fixture diff --git a/tests/integration/test_tts_websocket_integration.py b/tests/integration/test_tts_websocket_integration.py new file mode 100644 index 0000000..8cfe555 --- /dev/null +++ b/tests/integration/test_tts_websocket_integration.py @@ -0,0 +1,258 @@ +"""Integration tests for TTS WebSocket streaming functionality.""" + +import pytest + +from fishaudio.types import Prosody, TTSConfig, TextEvent, FlushEvent +from .conftest import TEST_REFERENCE_ID + + +class TestTTSWebSocketIntegration: + """Test TTS WebSocket streaming with real API.""" + + def test_basic_websocket_streaming(self, client, save_audio): + """Test basic WebSocket streaming with simple text.""" + + # Create a simple text stream + def text_stream(): + yield "Hello, " + yield "this is " + yield "a streaming test." + + # Stream audio via WebSocket + audio_chunks = list(client.tts.stream_websocket(text_stream())) + + assert len(audio_chunks) > 0, "Should receive audio chunks" + + # Verify we got audio data + complete_audio = b"".join(audio_chunks) + assert len(complete_audio) > 1000, "Should have substantial audio data" + + # Save the audio + save_audio(audio_chunks, "test_websocket_streaming.mp3") + + def test_websocket_streaming_with_wav_format(self, client, save_audio): + """Test WebSocket streaming with WAV format.""" + config = TTSConfig(format="wav", chunk_length=200) + + def text_stream(): + yield "Testing WAV format." + + audio_chunks = list(client.tts.stream_websocket(text_stream(), config=config)) + + assert len(audio_chunks) > 0 + save_audio(audio_chunks, "test_websocket_wav.wav") + + def test_websocket_streaming_with_prosody(self, client, save_audio): + """Test WebSocket streaming with prosody settings.""" + prosody = Prosody(speed=1.3, volume=0.8) + config = TTSConfig(prosody=prosody) + + def text_stream(): + yield "This audio " + yield "should have " + yield "faster speed." + + audio_chunks = list(client.tts.stream_websocket(text_stream(), config=config)) + + assert len(audio_chunks) > 0 + save_audio(audio_chunks, "test_websocket_prosody.mp3") + + def test_websocket_streaming_with_text_events(self, client, save_audio): + """Test WebSocket streaming with TextEvent and FlushEvent.""" + + def text_stream(): + yield "First sentence. " + yield TextEvent(text="Second sentence with event. ") + yield FlushEvent() + yield "Third sentence after flush." + + audio_chunks = list(client.tts.stream_websocket(text_stream())) + + assert len(audio_chunks) > 0 + save_audio(audio_chunks, "test_websocket_text_events.mp3") + + def test_websocket_streaming_long_text(self, client, save_audio): + """Test WebSocket streaming with longer text chunks.""" + + def text_stream(): + sentences = [ + "The quick brown fox jumps over the lazy dog. ", + "This is a longer piece of text to test streaming. ", + "We want to make sure the WebSocket can handle multiple chunks. ", + "And that all audio is received correctly. ", + "Finally, we end the stream here.", + ] + for sentence in sentences: + yield sentence + + audio_chunks = list(client.tts.stream_websocket(text_stream())) + + assert len(audio_chunks) > 0 + complete_audio = b"".join(audio_chunks) + assert len(complete_audio) > 5000, "Should have more audio for longer text" + save_audio(audio_chunks, "test_websocket_long_text.mp3") + + def test_websocket_streaming_with_reference(self, client, save_audio): + """Test WebSocket streaming with reference voice.""" + config = TTSConfig( + reference_id=TEST_REFERENCE_ID, + chunk_length=200, + ) + + def text_stream(): + yield "Testing with reference voice." + + audio_chunks = list(client.tts.stream_websocket(text_stream(), config=config)) + assert len(audio_chunks) > 0 + save_audio(audio_chunks, "test_websocket_reference.mp3") + + def test_websocket_streaming_empty_text(self, client, save_audio): + """Test WebSocket streaming with empty text stream raises error.""" + from fishaudio.exceptions import WebSocketError + + def text_stream(): + return + yield # Make it a generator + + # Empty stream should raise WebSocketError as API returns error + with pytest.raises(WebSocketError, match="WebSocket stream ended with error"): + list(client.tts.stream_websocket(text_stream())) + + +class TestAsyncTTSWebSocketIntegration: + """Test async TTS WebSocket streaming with real API.""" + + @pytest.mark.asyncio + async def test_basic_async_websocket_streaming(self, async_client, save_audio): + """Test basic async WebSocket streaming.""" + + async def text_stream(): + yield "Hello, " + yield "this is " + yield "async streaming." + + audio_chunks = [] + async for chunk in async_client.tts.stream_websocket(text_stream()): + audio_chunks.append(chunk) + + assert len(audio_chunks) > 0, "Should receive audio chunks" + complete_audio = b"".join(audio_chunks) + assert len(complete_audio) > 1000, "Should have substantial audio data" + + save_audio(audio_chunks, "test_async_websocket_streaming.mp3") + + @pytest.mark.asyncio + async def test_async_websocket_streaming_with_format( + self, async_client, save_audio + ): + """Test async WebSocket streaming with different formats.""" + config = TTSConfig(format="wav", chunk_length=200) + + async def text_stream(): + yield "Testing async " + yield "WAV format." + + audio_chunks = [] + async for chunk in async_client.tts.stream_websocket( + text_stream(), config=config + ): + audio_chunks.append(chunk) + + assert len(audio_chunks) > 0 + save_audio(audio_chunks, "test_async_websocket_wav.wav") + + @pytest.mark.asyncio + async def test_async_websocket_streaming_with_prosody( + self, async_client, save_audio + ): + """Test async WebSocket streaming with prosody.""" + prosody = Prosody(speed=0.8, volume=1.0) + config = TTSConfig(prosody=prosody) + + async def text_stream(): + yield "This should " + yield "sound slower " + yield "than normal." + + audio_chunks = [] + async for chunk in async_client.tts.stream_websocket( + text_stream(), config=config + ): + audio_chunks.append(chunk) + + assert len(audio_chunks) > 0 + save_audio(audio_chunks, "test_async_websocket_prosody.mp3") + + @pytest.mark.asyncio + async def test_async_websocket_streaming_with_events( + self, async_client, save_audio + ): + """Test async WebSocket streaming with text events.""" + + async def text_stream(): + yield "First part. " + yield TextEvent(text="Second part with event. ") + yield FlushEvent() + yield "Third part." + + audio_chunks = [] + async for chunk in async_client.tts.stream_websocket(text_stream()): + audio_chunks.append(chunk) + + assert len(audio_chunks) > 0 + save_audio(audio_chunks, "test_async_websocket_events.mp3") + + @pytest.mark.asyncio + async def test_async_websocket_streaming_long_text(self, async_client, save_audio): + """Test async WebSocket streaming with longer text.""" + + async def text_stream(): + sentences = [ + "The quick brown fox jumps over the lazy dog. ", + "This is an async streaming test with multiple chunks. ", + "We're testing the async WebSocket implementation. ", + "It should handle all chunks correctly. ", + "And return complete audio data.", + ] + for sentence in sentences: + yield sentence + + audio_chunks = [] + async for chunk in async_client.tts.stream_websocket(text_stream()): + audio_chunks.append(chunk) + + assert len(audio_chunks) > 0 + complete_audio = b"".join(audio_chunks) + assert len(complete_audio) > 5000, "Should have more audio for longer text" + save_audio(audio_chunks, "test_async_websocket_long_text.mp3") + + @pytest.mark.asyncio + async def test_async_websocket_streaming_multiple_calls( + self, async_client, save_audio + ): + """Test multiple async WebSocket streaming calls in sequence.""" + for i in range(3): + + async def text_stream(): + yield f"This is call number {i + 1}." + + audio_chunks = [] + async for chunk in async_client.tts.stream_websocket(text_stream()): + audio_chunks.append(chunk) + + assert len(audio_chunks) > 0, f"Call {i + 1} should return audio" + save_audio(audio_chunks, f"test_async_websocket_call_{i + 1}.mp3") + + @pytest.mark.asyncio + async def test_async_websocket_streaming_empty_text(self, async_client, save_audio): + """Test async WebSocket streaming with empty text stream raises error.""" + from fishaudio.exceptions import WebSocketError + + async def text_stream(): + return + yield # Make it an async generator + + # Empty stream should raise WebSocketError as API returns error + with pytest.raises(WebSocketError, match="WebSocket stream ended with error"): + async for chunk in async_client.tts.stream_websocket(text_stream()): + pass diff --git a/tests/unit/test_realtime.py b/tests/unit/test_realtime.py new file mode 100644 index 0000000..f9e47b4 --- /dev/null +++ b/tests/unit/test_realtime.py @@ -0,0 +1,339 @@ +"""Tests for realtime WebSocket streaming helpers.""" + +import pytest +from unittest.mock import Mock +import ormsgpack +from httpx_ws import WebSocketDisconnect + +from fishaudio.resources.realtime import ( + _should_stop, + _process_audio_event, + iter_websocket_audio, + aiter_websocket_audio, +) +from fishaudio.exceptions import WebSocketError + + +class TestShouldStop: + """Test _should_stop function.""" + + def test_returns_true_for_finish_stop_event(self): + """Test that finish event with stop reason returns True.""" + data = {"event": "finish", "reason": "stop"} + assert _should_stop(data) is True + + def test_returns_false_for_finish_error_event(self): + """Test that finish event with error reason returns False.""" + data = {"event": "finish", "reason": "error"} + assert _should_stop(data) is False + + def test_returns_false_for_audio_event(self): + """Test that audio event returns False.""" + data = {"event": "audio", "audio": b"data"} + assert _should_stop(data) is False + + def test_returns_false_for_unknown_event(self): + """Test that unknown event returns False.""" + data = {"event": "unknown", "data": "something"} + assert _should_stop(data) is False + + def test_returns_false_for_missing_fields(self): + """Test that missing event/reason fields returns False.""" + assert _should_stop({}) is False + assert _should_stop({"event": "finish"}) is False + assert _should_stop({"reason": "stop"}) is False + + +class TestProcessAudioEvent: + """Test _process_audio_event function.""" + + def test_audio_event_returns_audio_bytes(self): + """Test that audio event returns audio bytes.""" + data = {"event": "audio", "audio": b"test_audio_data"} + result = _process_audio_event(data) + assert result == b"test_audio_data" + + def test_finish_event_with_error_raises_exception(self): + """Test that finish event with error reason raises WebSocketError.""" + data = {"event": "finish", "reason": "error"} + with pytest.raises(WebSocketError, match="WebSocket stream ended with error"): + _process_audio_event(data) + + def test_finish_event_with_stop_returns_none(self): + """Test that finish event with stop reason returns None.""" + data = {"event": "finish", "reason": "stop"} + result = _process_audio_event(data) + assert result is None + + def test_unknown_event_returns_none(self): + """Test that unknown event types return None.""" + data = {"event": "unknown_event", "data": "something"} + result = _process_audio_event(data) + assert result is None + + def test_finish_event_with_unknown_reason_returns_none(self): + """Test that finish event with unknown reason returns None.""" + data = {"event": "finish", "reason": "unknown_reason"} + result = _process_audio_event(data) + assert result is None + + +class TestIterWebsocketAudio: + """Test iter_websocket_audio function (sync).""" + + def test_yields_audio_chunks(self): + """Test that audio chunks are yielded correctly.""" + # Create mock WebSocket + mock_ws = Mock() + + # Prepare messages + messages = [ + ormsgpack.packb({"event": "audio", "audio": b"chunk1"}), + ormsgpack.packb({"event": "audio", "audio": b"chunk2"}), + ormsgpack.packb({"event": "audio", "audio": b"chunk3"}), + ormsgpack.packb({"event": "finish", "reason": "stop"}), + ] + + mock_ws.receive_bytes = Mock(side_effect=messages) + + # Collect yielded audio + audio_chunks = list(iter_websocket_audio(mock_ws)) + + assert audio_chunks == [b"chunk1", b"chunk2", b"chunk3"] + assert mock_ws.receive_bytes.call_count == 4 + + def test_stops_on_finish_stop_event(self): + """Test that iteration stops on finish stop event.""" + mock_ws = Mock() + + messages = [ + ormsgpack.packb({"event": "audio", "audio": b"chunk1"}), + ormsgpack.packb({"event": "finish", "reason": "stop"}), + ] + + mock_ws.receive_bytes = Mock(side_effect=messages) + + audio_chunks = list(iter_websocket_audio(mock_ws)) + + assert audio_chunks == [b"chunk1"] + assert mock_ws.receive_bytes.call_count == 2 + + def test_raises_on_finish_error_event(self): + """Test that WebSocketError is raised on error finish event.""" + mock_ws = Mock() + + messages = [ + ormsgpack.packb({"event": "audio", "audio": b"chunk1"}), + ormsgpack.packb({"event": "finish", "reason": "error"}), + ] + + mock_ws.receive_bytes = Mock(side_effect=messages) + + with pytest.raises(WebSocketError, match="WebSocket stream ended with error"): + list(iter_websocket_audio(mock_ws)) + + def test_raises_on_websocket_disconnect(self): + """Test that WebSocketError is raised on unexpected disconnect.""" + mock_ws = Mock() + + messages = [ + ormsgpack.packb({"event": "audio", "audio": b"chunk1"}), + WebSocketDisconnect(), + ] + + mock_ws.receive_bytes = Mock(side_effect=messages) + + with pytest.raises(WebSocketError, match="WebSocket disconnected unexpectedly"): + list(iter_websocket_audio(mock_ws)) + + def test_ignores_unknown_events(self): + """Test that unknown events are ignored and iteration continues.""" + mock_ws = Mock() + + messages = [ + ormsgpack.packb({"event": "audio", "audio": b"chunk1"}), + ormsgpack.packb({"event": "unknown", "data": "ignored"}), + ormsgpack.packb({"event": "audio", "audio": b"chunk2"}), + ormsgpack.packb({"event": "finish", "reason": "stop"}), + ] + + mock_ws.receive_bytes = Mock(side_effect=messages) + + audio_chunks = list(iter_websocket_audio(mock_ws)) + + # Unknown event should be ignored, iteration continues + assert audio_chunks == [b"chunk1", b"chunk2"] + assert mock_ws.receive_bytes.call_count == 4 + + def test_empty_stream_with_immediate_finish(self): + """Test handling of stream that immediately finishes.""" + mock_ws = Mock() + + messages = [ + ormsgpack.packb({"event": "finish", "reason": "stop"}), + ] + + mock_ws.receive_bytes = Mock(side_effect=messages) + + audio_chunks = list(iter_websocket_audio(mock_ws)) + + assert audio_chunks == [] + + +class TestAiterWebsocketAudio: + """Test aiter_websocket_audio function (async).""" + + @pytest.mark.asyncio + async def test_yields_audio_chunks(self): + """Test that audio chunks are yielded correctly.""" + # Create mock async WebSocket + mock_ws = Mock() + + # Prepare messages + messages = [ + ormsgpack.packb({"event": "audio", "audio": b"chunk1"}), + ormsgpack.packb({"event": "audio", "audio": b"chunk2"}), + ormsgpack.packb({"event": "audio", "audio": b"chunk3"}), + ormsgpack.packb({"event": "finish", "reason": "stop"}), + ] + + async def mock_receive_bytes(): + return messages.pop(0) + + mock_ws.receive_bytes = mock_receive_bytes + + # Collect yielded audio + audio_chunks = [] + async for chunk in aiter_websocket_audio(mock_ws): + audio_chunks.append(chunk) + + assert audio_chunks == [b"chunk1", b"chunk2", b"chunk3"] + + @pytest.mark.asyncio + async def test_stops_on_finish_stop_event(self): + """Test that iteration stops on finish stop event.""" + mock_ws = Mock() + + messages = [ + ormsgpack.packb({"event": "audio", "audio": b"chunk1"}), + ormsgpack.packb({"event": "finish", "reason": "stop"}), + ] + + async def mock_receive_bytes(): + return messages.pop(0) + + mock_ws.receive_bytes = mock_receive_bytes + + audio_chunks = [] + async for chunk in aiter_websocket_audio(mock_ws): + audio_chunks.append(chunk) + + assert audio_chunks == [b"chunk1"] + + @pytest.mark.asyncio + async def test_raises_on_finish_error_event(self): + """Test that WebSocketError is raised on error finish event.""" + mock_ws = Mock() + + messages = [ + ormsgpack.packb({"event": "audio", "audio": b"chunk1"}), + ormsgpack.packb({"event": "finish", "reason": "error"}), + ] + + async def mock_receive_bytes(): + return messages.pop(0) + + mock_ws.receive_bytes = mock_receive_bytes + + with pytest.raises(WebSocketError, match="WebSocket stream ended with error"): + async for _ in aiter_websocket_audio(mock_ws): + pass + + @pytest.mark.asyncio + async def test_raises_on_websocket_disconnect(self): + """Test that WebSocketError is raised on unexpected disconnect.""" + mock_ws = Mock() + + call_count = [0] + + async def mock_receive_bytes(): + call_count[0] += 1 + if call_count[0] == 1: + return ormsgpack.packb({"event": "audio", "audio": b"chunk1"}) + else: + raise WebSocketDisconnect() + + mock_ws.receive_bytes = mock_receive_bytes + + with pytest.raises(WebSocketError, match="WebSocket disconnected unexpectedly"): + async for _ in aiter_websocket_audio(mock_ws): + pass + + @pytest.mark.asyncio + async def test_ignores_unknown_events(self): + """Test that unknown events are ignored and iteration continues.""" + mock_ws = Mock() + + messages = [ + ormsgpack.packb({"event": "audio", "audio": b"chunk1"}), + ormsgpack.packb({"event": "unknown", "data": "ignored"}), + ormsgpack.packb({"event": "audio", "audio": b"chunk2"}), + ormsgpack.packb({"event": "finish", "reason": "stop"}), + ] + + async def mock_receive_bytes(): + return messages.pop(0) + + mock_ws.receive_bytes = mock_receive_bytes + + audio_chunks = [] + async for chunk in aiter_websocket_audio(mock_ws): + audio_chunks.append(chunk) + + # Unknown event should be ignored, iteration continues + assert audio_chunks == [b"chunk1", b"chunk2"] + + @pytest.mark.asyncio + async def test_empty_stream_with_immediate_finish(self): + """Test handling of stream that immediately finishes.""" + mock_ws = Mock() + + messages = [ + ormsgpack.packb({"event": "finish", "reason": "stop"}), + ] + + async def mock_receive_bytes(): + return messages.pop(0) + + mock_ws.receive_bytes = mock_receive_bytes + + audio_chunks = [] + async for chunk in aiter_websocket_audio(mock_ws): + audio_chunks.append(chunk) + + assert audio_chunks == [] + + @pytest.mark.asyncio + async def test_multiple_audio_events_in_sequence(self): + """Test handling of multiple consecutive audio events.""" + mock_ws = Mock() + + messages = [ + ormsgpack.packb({"event": "audio", "audio": b"a"}), + ormsgpack.packb({"event": "audio", "audio": b"b"}), + ormsgpack.packb({"event": "audio", "audio": b"c"}), + ormsgpack.packb({"event": "audio", "audio": b"d"}), + ormsgpack.packb({"event": "audio", "audio": b"e"}), + ormsgpack.packb({"event": "finish", "reason": "stop"}), + ] + + async def mock_receive_bytes(): + return messages.pop(0) + + mock_ws.receive_bytes = mock_receive_bytes + + audio_chunks = [] + async for chunk in aiter_websocket_audio(mock_ws): + audio_chunks.append(chunk) + + assert audio_chunks == [b"a", b"b", b"c", b"d", b"e"] diff --git a/tests/unit/test_tts_realtime.py b/tests/unit/test_tts_realtime.py index 87548f8..72141ec 100644 --- a/tests/unit/test_tts_realtime.py +++ b/tests/unit/test_tts_realtime.py @@ -13,6 +13,8 @@ def mock_client_wrapper(mock_api_key): """Mock client wrapper.""" wrapper = Mock(spec=ClientWrapper) wrapper.api_key = mock_api_key + # Mock the underlying httpx.Client + wrapper._client = Mock() return wrapper @@ -21,6 +23,8 @@ def async_mock_client_wrapper(mock_api_key): """Mock async client wrapper.""" wrapper = Mock(spec=AsyncClientWrapper) wrapper.api_key = mock_api_key + # Mock the underlying httpx.AsyncClient + wrapper._client = Mock() return wrapper @@ -58,10 +62,6 @@ def test_stream_websocket_basic( mock_executor_instance.submit.return_value = mock_future mock_executor.return_value = mock_executor_instance - # Mock the WebSocket client creation - mock_ws_client = Mock() - mock_client_wrapper.create_websocket_client.return_value = mock_ws_client - # Mock the audio receiver (iter_websocket_audio) with patch("fishaudio.resources.tts.iter_websocket_audio") as mock_receiver: mock_receiver.return_value = iter([b"audio1", b"audio2", b"audio3"]) @@ -79,9 +79,6 @@ def test_stream_websocket_basic( mock_connect_ws.assert_called_once() assert mock_connect_ws.call_args[0][0] == "/v1/tts/live" - # Verify WebSocket client was closed - mock_ws_client.close.assert_called_once() - @patch("fishaudio.resources.tts.connect_ws") @patch("fishaudio.resources.tts.ThreadPoolExecutor") def test_stream_websocket_with_config( @@ -100,9 +97,6 @@ def test_stream_websocket_with_config( mock_executor_instance.submit.return_value = mock_future mock_executor.return_value = mock_executor_instance - mock_ws_client = Mock() - mock_client_wrapper.create_websocket_client.return_value = mock_ws_client - with patch("fishaudio.resources.tts.iter_websocket_audio") as mock_receiver: mock_receiver.return_value = iter([b"audio"]) @@ -124,10 +118,6 @@ def test_stream_websocket_with_config( call_args = mock_connect_ws.call_args assert call_args[1]["headers"]["model"] == "speech-1.5" - # Verify config was sent in StartEvent (would be first send_bytes call) - # The sender runs in a thread, so we can't easily inspect the exact calls - mock_ws_client.close.assert_called_once() - @patch("fishaudio.resources.tts.connect_ws") @patch("fishaudio.resources.tts.ThreadPoolExecutor") def test_stream_websocket_with_text_events( @@ -146,9 +136,6 @@ def test_stream_websocket_with_text_events( mock_executor_instance.submit.return_value = mock_future mock_executor.return_value = mock_executor_instance - mock_ws_client = Mock() - mock_client_wrapper.create_websocket_client.return_value = mock_ws_client - with patch("fishaudio.resources.tts.iter_websocket_audio") as mock_receiver: mock_receiver.return_value = iter([b"audio1", b"audio2"]) @@ -185,9 +172,6 @@ def test_stream_websocket_max_workers( mock_executor_instance.submit.return_value = mock_future mock_executor.return_value = mock_executor_instance - mock_ws_client = Mock() - mock_client_wrapper.create_websocket_client.return_value = mock_ws_client - with patch("fishaudio.resources.tts.iter_websocket_audio") as mock_receiver: mock_receiver.return_value = iter([b"audio"]) @@ -214,11 +198,6 @@ async def test_stream_websocket_basic( mock_ws.send_bytes = AsyncMock() mock_aconnect_ws.return_value = mock_ws - # Mock the WebSocket client creation - mock_ws_client = Mock() - mock_ws_client.aclose = AsyncMock() - async_mock_client_wrapper.create_websocket_client.return_value = mock_ws_client - # Mock the audio receiver async def mock_audio_receiver(ws): for chunk in [b"audio1", b"audio2", b"audio3"]: @@ -245,9 +224,6 @@ async def text_stream(): mock_aconnect_ws.assert_called_once() assert mock_aconnect_ws.call_args[0][0] == "/v1/tts/live" - # Verify WebSocket client was closed - mock_ws_client.aclose.assert_called_once() - @pytest.mark.asyncio @patch("fishaudio.resources.tts.aconnect_ws") async def test_stream_websocket_with_config( @@ -261,10 +237,6 @@ async def test_stream_websocket_with_config( mock_ws.send_bytes = AsyncMock() mock_aconnect_ws.return_value = mock_ws - mock_ws_client = Mock() - mock_ws_client.aclose = AsyncMock() - async_mock_client_wrapper.create_websocket_client.return_value = mock_ws_client - async def mock_audio_receiver(ws): yield b"audio" @@ -292,9 +264,6 @@ async def text_stream(): call_args = mock_aconnect_ws.call_args assert call_args[1]["headers"]["model"] == "speech-1.6" - # Verify client was closed - mock_ws_client.aclose.assert_called_once() - @pytest.mark.asyncio @patch("fishaudio.resources.tts.aconnect_ws") async def test_stream_websocket_with_text_events( @@ -308,10 +277,6 @@ async def test_stream_websocket_with_text_events( mock_ws.send_bytes = AsyncMock() mock_aconnect_ws.return_value = mock_ws - mock_ws_client = Mock() - mock_ws_client.aclose = AsyncMock() - async_mock_client_wrapper.create_websocket_client.return_value = mock_ws_client - async def mock_audio_receiver(ws): yield b"audio1" yield b"audio2" @@ -347,10 +312,6 @@ async def test_stream_websocket_empty_stream( mock_ws.send_bytes = AsyncMock() mock_aconnect_ws.return_value = mock_ws - mock_ws_client = Mock() - mock_ws_client.aclose = AsyncMock() - async_mock_client_wrapper.create_websocket_client.return_value = mock_ws_client - async def mock_audio_receiver(ws): return yield # Make it a generator @@ -370,6 +331,3 @@ async def text_stream(): # Should have no audio assert audio_chunks == [] - - # Verify client was still closed - mock_ws_client.aclose.assert_called_once() diff --git a/uv.lock b/uv.lock index 02d8590..82bda1c 100644 --- a/uv.lock +++ b/uv.lock @@ -384,7 +384,7 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -392,7 +392,7 @@ wheels = [ ] [[package]] -name = "fish-audio" +name = "fish-audio-sdk" version = "1.0.0" source = { editable = "." } dependencies = [