diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..ed6ba72 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [thewebscraping] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4a8249..3aeede6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,69 +3,47 @@ name: CI on: push: branches: ['master', 'main'] - pull_request: - branches: ['master', 'main', 'dev', 'develop'] + branches: ['master', 'main', 'dev', 'develop', 'feat/*'] jobs: - build-on-ubuntu: - runs-on: ubuntu-latest + tests: + name: Tests on ${{ matrix.os }} / ${{ matrix.python-version }} + runs-on: ${{ matrix.os }} strategy: fail-fast: false - max-parallel: 3 matrix: - python-version: ['3.9', '3.10', '3.11'] - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + exclude: + - os: windows-latest + python-version: '3.10' + - os: windows-latest + python-version: '3.11' + - os: windows-latest + python-version: '3.12' + - os: macos-latest + python-version: '3.10' + - os: macos-latest + python-version: '3.11' + - os: macos-latest + python-version: '3.12' - - name: Install Dependencies, Lint and Tests - run: | - make init-actions - - build-on-mac: - runs-on: macos-latest - strategy: - fail-fast: false - max-parallel: 1 - matrix: - python-version: ['3.9'] steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: Install Dependencies, Lint and Tests - run: | - make init-actions - - - name: Pytest - run: | - make pytest - - build-on-windows: - runs-on: windows-latest - strategy: - fail-fast: false - max-parallel: 1 - matrix: - python-version: ['3.9'] - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: Install Dependencies, Lint - run: | - make init-actions - - - name: Pytest - run: | - make pytest + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + python-version: ${{ matrix.python-version }} + + - name: Run linting + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.9' + run: | + uv run ruff check . + uv run mypy src/tls_requests + + - name: Run tests + run: | + uv run pytest diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 34dea78..1b0a7ed 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -1,35 +1,30 @@ name: Build Documentation on: - release: - types: [created] + push: + branches: + - main + tags: + - 'v*' + workflow_dispatch: jobs: build: runs-on: ubuntu-latest - strategy: - max-parallel: 1 - matrix: - python-version: ['3.9'] + permissions: + contents: write steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install Dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements-dev.txt - - name: Configure Git Credentials - run: | - git config user.name github-actions[bot] - git config user.email 41898282+github-actions[bot]@users.noreply.github.com - - uses: actions/cache@v4 - with: - key: mkdocs-material-${{ env.cache_id }} - path: .cache - restore-keys: | - mkdocs-material- - - name: Publish Documentation - run: | - echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV - mkdocs gh-deploy --force + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + python-version: '3.11' + + - name: Configure Git Credentials + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + + - name: Publish Documentation + run: | + uv run mkdocs gh-deploy --force diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7a90847..ef1c767 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,26 +1,31 @@ -name: Upload Python Package +name: Publish to PyPI on: - release: - types: [created] + push: + tags: + - 'v*' jobs: - deploy: - name: Publish PyPI + build-n-publish: + name: Build and publish to PyPI runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/wrapper-tls-requests + permissions: + id-token: write # Required for Trusted Publishing + contents: read + steps: - - uses: actions/checkout@main - - name: Set up Python 3.9 - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 with: - python-version: '3.9' + enable-cache: true + - name: Build package - run: | - python -m pip install -U pip build setuptools twine - - name: Publish - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: | - python setup.py sdist bdist_wheel - twine upload --skip-existing dist/* + run: uv build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index 015c6bc..ba766f4 100644 --- a/.gitignore +++ b/.gitignore @@ -160,4 +160,8 @@ cython_debug/ # 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/ -tls_requests/bin/*xgo* +src/tls_requests/bin/*.dll +src/tls_requests/bin/*.so +src/tls_requests/bin/*.dylib +src/tls_requests/bin/*.json +!src/tls_requests/bin/__init__.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7a8b289..7738e6c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,33 +1,62 @@ -exclude: '^docs.sh/|scripts/' -default_stages: [pre-commit] - -default_language_version: - python: python3.10 +# Pre-commit hooks configuration +# See https://pre-commit.com for more information repos: + # General file checks - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + args: [--maxkb=1500] - id: check-json - id: check-toml - - id: check-xml - - id: check-yaml - - id: debug-statements - - id: check-builtin-literals + - id: check-merge-conflict - id: check-case-conflict - - id: check-docstring-first - - id: detect-private-key + - id: mixed-line-ending - # run the isort. - - repo: https://github.com/PyCQA/isort - rev: 5.13.2 + # Ruff - Fast Python linter and formatter + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.13 hooks: - - id: isort + # Run the linter + - id: ruff + args: [--extend-select, I, --fix, --exit-non-zero-on-fix] + # Run the formatter + - id: ruff-format + args: [--exit-non-zero-on-fix] - # run the flake8. - - repo: https://github.com/PyCQA/flake8 - rev: 7.0.0 + # Type checking with mypy + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.15.0 hooks: - - id: flake8 + - id: mypy + additional_dependencies: + - types-setuptools + - idna + - orjson + - charset-normalizer + - chardet + args: [--ignore-missing-imports, --config-file=pyproject.toml] + + # Markdown linting (disabled due to duplicate heading issues) + # - repo: https://github.com/igorshubovych/markdownlint-cli + # rev: v0.46.0 + # hooks: + # - id: markdownlint + # args: [--fix] + +# CI configuration +ci: + autofix_commit_msg: | + [pre-commit.ci] auto fixes from pre-commit.com hooks + + for more information, see https://pre-commit.ci + autofix_prs: true + autoupdate_branch: '' + autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate' + autoupdate_schedule: weekly + skip: [] + submodules: false diff --git a/CHANGELOG.md b/CHANGELOG.md index cfdf64e..20c8312 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,55 @@ Release History -=============== +================ + +1.2.3 (2026-01-24) +------------------ +**Improvements:** + +- **Enhanced IPv6 Support**: Implemented automatic repair for naked IPv6 addresses (e.g., `2001:db8::1` -> `[2001:db8::1]`) and strict bracket validation. +- **Robust URL Handling**: Added default `http://` scheme for schemeless URLs and improved component-based building logic to prevent crashes. +- **Type Safety**: Resolved Mypy strict type inconsistencies and added comprehensive test coverage for IPv6 edge cases. + +**Bugfixes:** + +- **Python 3.9 Compatibility**: Fixed `RuntimeError: There is no current event loop` during rotator initialization by implementing lazy asyncio lock loading. +- **Redirect Logic**: Fixed `Headers.__getitem__` to correctly raise `KeyError` for missing keys, ensuring compatibility with standard redirect handling. + +1.2.2 (2026-01-21) +------------------ +**Improvements:** + +- **Standardized API Parameters**: Renamed `tls_identifier` to `client_identifier` and `tls_debug` to `debug` for better consistency and professional API surface. +- **Src Layout Migration**: Refactored the project structure to use a modern `src/` layout, improving package isolation and build reliability. +- **Type Safety & Testing**: Integrated `mypy` for strict type checking and migrated to `uv run pytest` for comprehensive test verification (80+ tests passing). +- **Backward Compatibility**: Implemented a centralized fallback and logging warning system for legacy parameters (planned removal in v1.3.0). +- **Modernized CI/CD**: Fully migrated from `Makefile` to `uv`. Optimized GitHub Actions matrix to support Python 3.9 through 3.13 across Linux, macOS, and Windows. +- **Enhanced Platform Support**: Improved detection and loading logic for ARM64/aarch64 architectures (Apple Silicon, AWS Graviton). +- **Project Sustainability**: Added GitHub Sponsors support to the repository metadata and documentation. + +1.1.7 (2025-11-23) +------------------ +**Improvements:** + +- Optimized logging. ([#46](https://github.com/thewebscraping/tls-requests/issues/46)) +- Fixed cookie response handling. ([#47](https://github.com/thewebscraping/tls-requests/issues/47)) + +1.1.6 (2025-10-14) +------------------ +**Enhancements:** +This pull request introduces two major enhancements that significantly improve the library's anti‑detection capabilities and overall robustness: + +**A Smart Rotator System** +- Automatically rotates proxies, headers, and TLS identifiers to mimic authentic traffic. +- Introduced three new rotator classes: `ProxyRotator`, `HeaderRotator`, and `TLSIdentifierRotator`. +- Client and AsyncClient now enable header and TLS identifier rotation by default, using built‑in realistic templates. +- Unified parameters accept a single value, a list, or a pre‑configured Rotator instance. +- Proxy feedback loop (`mark_result`/`amark_result`) optimizes weighted rotation strategy. + +**Robust Library Management** +- Dependency‑free, self‑managing mechanism for the core `tls-client` C library lifecycle. +- Removed `requests` and `tqdm`; now uses built‑in `urllib` and [json](cci:1://file:///Users/twofarm/Desktop/works/tls_requests/tls_requests/models/response.py:204:4-205:43). +- TLSLibrary is version‑aware, automatically downloading the correct version from GitHub when needed. +- Automatic cleanup of old library files after successful updates. 1.0.7 (2024-12-14) ------------------- diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 87ee50a..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include LICENSE -include README.md -include CHANGELOG.md -recursive-include tls_requests docs Makefile *.md diff --git a/Makefile b/Makefile deleted file mode 100644 index 94544a0..0000000 --- a/Makefile +++ /dev/null @@ -1,37 +0,0 @@ -.PHONY: docs -init-actions: - python -m pip install --upgrade pip - python -m pip install -r requirements-dev.txt - python -m black tls_requests - python -m isort tls_requests - python -m flake8 tls_requests - -test: - tox -p - rm -rf *.egg-info - -test-readme: - python setup.py check --restructuredtext --strict && ([ $$? -eq 0 ] && echo "README.md and CHANGELOG.md ok") || echo "Invalid markup in README.md or CHANGELOG.md!" - -pytest: - python -m pytest tests - -coverage: - python -m pytest --cov-config .coveragerc --verbose --cov-report term --cov-report xml --cov=tls_requests tests - -docs: - mkdocs serve - -publish-test-pypi: - python -m pip install -r requirements-dev.txt - python -m pip install 'twine>=6.0.1' - python setup.py sdist bdist_wheel - twine upload --repository testpypi --skip-existing dist/* - rm -rf build dist .egg wrapper_tls_requests.egg-info - -publish-pypi: - python -m pip install -r requirements-dev.txt - python -m pip install 'twine>=6.0.1' - python setup.py sdist bdist_wheel - twine upload --skip-existing dist/* - rm -rf build dist .egg wrapper_tls_requests.egg-info diff --git a/README.md b/README.md index 3919d32..3665d74 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![GitHub License](https://img.shields.io/github/license/thewebscraping/tls-requests)](https://github.com/thewebscraping/tls-requests/blob/main/LICENSE) [![CI](https://github.com/thewebscraping/tls-requests/actions/workflows/ci.yml/badge.svg)](https://github.com/thewebscraping/tls-requests/actions/workflows/ci.yml) [![PyPI - Version](https://img.shields.io/pypi/v/wrapper-tls-requests)](https://pypi.org/project/wrapper-tls-requests/) +[![Sponsor](https://img.shields.io/badge/Sponsor-thewebscraping-pink?logo=github-sponsors&logoColor=white)](https://github.com/sponsors/thewebscraping) ![Python Version](https://img.shields.io/badge/Python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-blue?style=flat) ![Pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white) @@ -33,14 +34,35 @@ pip install git+https://github.com/thewebscraping/tls-requests.git **Quick Start** --------------- -Start using TLS Requests with just a few lines of code: +Start using TLS Requests with just a few lines of code. It automatically synchronizes headers based on your chosen browser identifier: -```pycon ->>> import tls_requests ->>> r = tls_requests.get("https://httpbin.org/get") ->>> r +```python +import tls_requests +# The library automatically injects matching User-Agent and Sec-CH-UA headers +r = tls_requests.get("https://httpbin.org/headers", client_identifier="chrome_133") +r.json()["headers"]["User-Agent"] +'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36' +``` + +Basic automatically rotates for proxies and TLS identifiers: + +```python +import tls_requests +proxy_rotator = tls_requests.ProxyRotator([ + "http://user1:pass1@proxy.example.com:8080", + "http://user2:pass2@proxy.example.com:8081", + "socks5://proxy.example.com:8082", + "proxy.example.com:8083", # defaults to http + "http://user:pass@proxy.example.com:8084|1.0|US", # weight and region support +]) +r = tls_requests.get( + "https://httpbin.org/get", + proxy=proxy_rotator, + client_identifier=tls_requests.TLSIdentifierRotator() +) +r ->>> r.status_code +r.status_code 200 ``` @@ -75,10 +97,10 @@ making it easy to scrape data or interact with websites that use sophisticated a **Example Code:** -```pycon ->>> import tls_requests ->>> r = tls_requests.get('https://www.coingecko.com/') ->>> r +```python +import tls_requests +r = tls_requests.get('https://www.coingecko.com/') +r ``` @@ -88,9 +110,10 @@ making it easy to scrape data or interact with websites that use sophisticated a ### **Enhanced Capabilities** * **Browser-like TLS Fingerprinting**: Enables secure and reliable browser-mimicking connections. -* **High-Performance Backend**: Built on a Go-based HTTP backend for speed and efficiency. +* **Dynamic Header Synchronization**: Automatically extracts browser versions from `client_identifier` and injects them into `User-Agent` and `sec-ch-ua` headers. +* **High-Performance Backend**: Built on a Go-based HTTP backend with **Protocol Racing** (Happy Eyeballs) enabled by default for faster connections. * **Synchronous & Asynchronous Support**: Seamlessly switch between synchronous and asynchronous requests. -* **Protocol Support**: Fully compatible with HTTP/1.1 and HTTP/2. +* **Protocol Support**: Fully compatible with HTTP/1.1, HTTP/2, and HTTP/3 (Alpha). * **Strict Timeouts**: Reliable timeout management for precise control over request durations. ### **Additional Features** @@ -101,6 +124,7 @@ making it easy to scrape data or interact with websites that use sophisticated a * **Content Decoding**: Automatic handling of gzip and brotli-encoded responses. * **Hooks**: Perfect for logging, monitoring, tracing, or pre/post-processing requests and responses. * **Unicode Support**: Effortlessly process Unicode response bodies. +* **Advanced TLS Options**: Support for `protocol_racing`, `allow_http` and `stream_id`. * **File Uploads**: Simplified multipart file upload support. * **Proxy Configuration**: Supports Socks5, HTTP, and HTTPS proxies for enhanced privacy. diff --git a/docs/advanced/async_client.md b/docs/advanced/async_client.md index e5a24d2..221490e 100644 --- a/docs/advanced/async_client.md +++ b/docs/advanced/async_client.md @@ -1,103 +1,91 @@ -Async Support in TLS Requests -============================= +# Asynchronous Support -TLS Requests provides support for asynchronous HTTP requests using the `AsyncClient`. This is especially useful when working in an asynchronous environment, such as with modern web frameworks, or when you need the performance benefits of asynchronous I/O. +`tls_requests` provides full support for asynchronous HTTP requests via the `AsyncClient`. This is essential for high-concurrency workloads, long-lived connections, and integration with modern async frameworks like FastAPI. * * * -Why Use Async? --------------- +## Why Use Async? -* **Improved Performance:** Async is more efficient than multi-threading for handling high concurrency workloads. -* **Long-lived Connections:** Useful for protocols like WebSockets or long polling. -* **Framework Compatibility:** Essential when integrating with async web frameworks (e.g., FastAPI, Starlette). - -Advanced usage with syntax similar to Client, refer to the [Client documentation](client). +* **Concurrency**: efficient handling of many simultaneous requests without the overhead of threads. +* **Performance**: Improved I/O throughput in data-intensive applications. +* **Compatibility**: Seamless integration with the Python `asyncio` ecosystem. * * * -Making Async Requests ---------------------- +## Making Async Requests + +To send asynchronous requests, use the `AsyncClient` within an `async` function. + +### Basic Example -To send asynchronous HTTP requests, use the `AsyncClient`: +```python +import asyncio +import tls_requests -```pycon ->>> import asyncio ->>> import random ->>> import time ->>> import tls_requests ->>> async def fetch(idx, url): +async def main(): async with tls_requests.AsyncClient() as client: - rand = random.uniform(0.1, 1.5) - start_time = time.perf_counter() - print("%s: Sleep for %.2f seconds." % (idx, rand)) - await asyncio.sleep(rand) - response = await client.get(url) - end_time = time.perf_counter() - print("%s: Took: %.2f" % (idx, (end_time - start_time))) - return response ->>> async def run(urls): - tasks = [asyncio.create_task(fetch(idx, url)) for idx, url in enumerate(urls)] - responses = await asyncio.gather(*tasks) - return responses - ->>> start_urls = [ - 'https://httpbin.org/absolute-redirect/1', - 'https://httpbin.org/absolute-redirect/2', - 'https://httpbin.org/absolute-redirect/3', - 'https://httpbin.org/absolute-redirect/4', - 'https://httpbin.org/absolute-redirect/5', -] - - ->>> r = asyncio.run(run(start_urls)) ->>> r -[, , , , ] + response = await client.get("https://httpbin.org/get") + print(f"Status: {response.status_code}") + print(f"Data: {response.json()}") +if __name__ == "__main__": + asyncio.run(main()) ``` -!!! tip - Use [IPython](https://ipython.readthedocs.io/en/stable/) or Python 3.8+ with `python -m asyncio` to try this code interactively, as they support executing `async`/`await` expressions in the console. - * * * -Key API Differences -------------------- +## Concurrent Requests -When using `AsyncClient`, the API methods are asynchronous and must be awaited. +You can use `asyncio.gather` to execute multiple requests in parallel efficiently. -### Making Requests +```python +import asyncio +import tls_requests -Use `await` for all request methods: +async def fetch_url(client, url): + response = await client.get(url) + return response.status_code -* `await client.get(url, ...)` -* `await client.post(url, ...)` -* `await client.put(url, ...)` -* `await client.patch(url, ...)` -* `await client.delete(url, ...)` -* `await client.options(url, ...)` -* `await client.head(url, ...)` -* `await client.request(method, url, ...)` -* `await client.send(request, ...)` +async def main(): + urls = [ + "https://httpbin.org/get", + "https://httpbin.org/ip", + "https://httpbin.org/user-agent" + ] + + async with tls_requests.AsyncClient() as client: + tasks = [fetch_url(client, url) for url in urls] + results = await asyncio.gather(*tasks) + print(f"Results: {results}") + +asyncio.run(main()) +``` * * * -### Managing Client Lifecycle +## Key Differences from Sync Client -#### Context Manager +The `AsyncClient` mirrors the `Client` API but requires the `await` keyword for all network operations. -For proper resource cleanup, use `async with`: +### Async Methods +All request methods are coroutines: -```python -import asyncio +- `await client.get(url, ...)` +- `await client.post(url, ...)` +- `await client.put(url, ...)` +- `await client.patch(url, ...)` +- `await client.delete(url, ...)` +- `await client.request(method, url, ...)` -async def fetch(url): - async with tls_requests.AsyncClient() as client: - response = await client.get(url) - return response +### Lifecycle Management +Always use the `async with` context manager to ensure that the underlying TLS sessions are automatically closed and resources are freed. -r = asyncio.run(fetch("https://httpbin.org/get")) -print(r) # +```python +async with tls_requests.AsyncClient() as client: + # do work + ... +# session is closed here +``` ``` #### Manual Closing diff --git a/docs/advanced/authentication.md b/docs/advanced/authentication.md index 54fc8d1..0ad5545 100644 --- a/docs/advanced/authentication.md +++ b/docs/advanced/authentication.md @@ -4,18 +4,17 @@ This section covers how to use authentication in your requests with `tls_request * * * -Basic Authentication --------------------- +## Basic Authentication ### Using a Tuple (Username and Password) -For basic HTTP authentication, pass a tuple `(username, password)` when initializing a `Client`. -This will automatically include the credentials in the `Authorization` header for all outgoing requests: +For basic HTTP authentication, pass a tuple `(username, password)` when initializing a `Client`. This will automatically include the credentials in the `Authorization` header for all outgoing requests: +```python +import tls_requests -```pycon ->>> client = tls_requests.Client(auth=("username", "secret")) ->>> response = client.get("https://www.example.com/") +client = tls_requests.Client(auth=("username", "secret")) +response = client.get("https://httpbin.org/basic-auth/username/secret") ``` * * * @@ -24,24 +23,20 @@ This will automatically include the credentials in the `Authorization` header fo To customize how authentication is handled, you can use a function that modifies the request directly: -```pycon ->>> def custom_auth(request): - request.headers["X-Authorization"] = "123456" - return request +```python +import tls_requests ->>> response = tls_requests.get("https://httpbin.org/headers", auth=custom_auth) ->>> response - ->>> response.request.headers["X-Authorization"] -'123456' ->>> response.json()["headers"]["X-Authorization"] -'123456' +def custom_auth(request): + request.headers["X-Authorization"] = "123456" + return request + +response = tls_requests.get("https://httpbin.org/headers", auth=custom_auth) +print(response.request.headers["X-Authorization"]) # Outputs: 123456 ``` * * * -Custom Authentication ---------------------- +## Custom Authentication For advanced use cases, you can define custom authentication schemes by subclassing `tls_requests.Auth` and overriding the `build_auth` method. @@ -49,13 +44,14 @@ For advanced use cases, you can define custom authentication schemes by subclass This example demonstrates how to implement Bearer token-based authentication by adding an `Authorization` header: - ```python +import tls_requests + class BearerAuth(tls_requests.Auth): def __init__(self, token): self.token = token - def build_auth(self, request: tls_requests.Request) -> tls_requests.Request | None: + def build_auth(self, request: tls_requests.Request) -> tls_requests.Request: request.headers["Authorization"] = f"Bearer {self.token}" return request ``` @@ -66,15 +62,14 @@ class BearerAuth(tls_requests.Auth): To use your custom `BearerAuth` implementation: -```pycon ->>> auth = BearerAuth(token="your_jwt_token") ->>> response = tls_requests.get("https://httpbin.org/headers", auth=auth) ->>> response - ->>> response.request.headers["Authorization"] -'Bearer your_jwt_token' ->>> response.json()["headers"]["Authorization"] -'Bearer your_jwt_token' +```python +import tls_requests + +# Define custom class as above +auth = BearerAuth(token="your_jwt_token") +response = tls_requests.get("https://httpbin.org/headers", auth=auth) + +print(response.request.headers["Authorization"]) # Outputs: Bearer your_jwt_token ``` With these approaches, you can integrate various authentication strategies into your `tls_requests` workflow, whether built-in or custom-designed for specific needs. diff --git a/docs/advanced/client.md b/docs/advanced/client.md index a498c69..89ed9b3 100644 --- a/docs/advanced/client.md +++ b/docs/advanced/client.md @@ -1,101 +1,95 @@ -Client Usage -================================ +# Client Usage -This guide details how to utilize the `tls_requests.Client` for efficient and advanced HTTP networking. -If you're transitioning from the popular `requests` library, the `Client` in `tls_requests` provides a powerful alternative with enhanced capabilities. +The `Client` class is the primary interface for making synchronous HTTP requests with `tls_requests`. It manages persistent sessions, handles cookie storage, and allows for shared configuration across multiple requests. -* * * - -Why Use a Client? ------------------ - -!!! hint - If you’re familiar with `requests`, think of `tls_requests.Client()` as the equivalent of `requests.Session()`. +If you are familiar with the `requests` library, `tls_requests.Client` is equivalent to `requests.Session`. -### TL;DR +* * * -Use a `Client` instance if you're doing more than one-off scripts or prototypes. It optimizes network resource usage by reusing connections, which is critical for performance when making multiple requests. +## Why Use the Client? -**Advantages:** +While you can use top-level functions like `tls_requests.get()`, using a `Client` is recommended for most applications because: -* Efficient connection reuse. -* Simplified configuration sharing across requests. -* Advanced control over request behavior and customization. +* **Performance**: Reuses underlying TLS sessions and network connections. +* **State Management**: Automatically manages cookies and authentication across multiple requests. +* **Consistency**: Shared headers, proxies, and timeouts are applied to every request made with the client. * * * -Recommended Usage ------------------ +## Usage Patterns -### Using a Context Manager +### Recommended: Context Manager -The best practice is to use a `Client` as a context manager. This ensures connections are properly cleaned up: +Using the `with` statement ensures that the client is automatically closed and its native resources are freed once you are finished. ```python +import tls_requests + with tls_requests.Client() as client: response = client.get("https://httpbin.org/get") - print(response) # + print(response.status_code) ``` -### Explicit Cleanup +### Manual Management -If not using a context manager, ensure to close the client explicitly: +If you cannot use a context manager, ensure you call `.close()` manually. ```python +import tls_requests + client = tls_requests.Client() -try: - response = client.get("https://httpbin.org/get") - print(response) # -finally: - client.close() +response = client.get("https://httpbin.org/get") +# ... do more work ... +client.close() ``` * * * -Making Requests ---------------- +## Persistent Configuration -A `Client` can send requests using methods like `.get()`, `.post()`, etc.: +You can set default values during client initialization that will apply to every subsequent request. ```python -with tls_requests.Client() as client: - response = client.get("https://httpbin.org/get") - print(response) # -``` - -### Custom Headers +import tls_requests -To include custom headers in a request: +# Set global headers and a proxy +client = tls_requests.Client( + headers={"User-Agent": "MyCustomBrowser/1.0"}, + proxy="http://127.0.0.1:8080" +) -```python -headers = {'X-Custom': 'value'} -with tls_requests.Client() as client: - response = client.get("https://httpbin.org/get", headers=headers) - print(response.request.headers['X-Custom']) # 'value' +# This request will use the custom User-Agent and Proxy +response = client.get("https://httpbin.org/headers") ``` -* * * - -Sharing Configuration Across Requests -------------------------------------- +### Merging Headers and Cookies -You can apply default configurations, such as headers, for all requests made with the `Client`: +If you provide headers or cookies both at the client level and in an individual request, they are merged. Request-level values will override client-level values if there is a conflict. ```python -headers = {'user-agent': 'my-app/1.0'} -with tls_requests.Client(headers=headers) as client: - response = client.get("https://httpbin.org/headers") - print(response.json()['headers']['User-Agent']) # 'my-app/1.0' +with tls_requests.Client(headers={"X-Client": "A"}) as client: + # This request has both 'X-Client: A' and 'X-Request: B' + resp = client.get("https://httpbin.org/headers", headers={"X-Request": "B"}) ``` * * * -Merging Configurations ----------------------- +## Request Methods + +The client supports all standard HTTP methods: -When client-level and request-level options overlap: +- `client.get(url, **kwargs)` +- `client.post(url, data=..., json=..., **kwargs)` +- `client.put(url, **kwargs)` +- `client.patch(url, **kwargs)` +- `client.delete(url, **kwargs)` +- `client.head(url, **kwargs)` +- `client.options(url, **kwargs)` -* **Headers, query parameters, cookies:** Combined. Example: +For more advanced scenarios like custom authentication or request hooks, refer to the dedicated guides in the [Advanced](../advanced/authentication.md) section. + +* **Merging Headers and Cookies:** + Request-level values will override client-level values if there is a conflict. ```python client_headers = {'X-Auth': 'client'} @@ -111,14 +105,12 @@ with tls_requests.Client(headers=client_headers) as client: ```python with tls_requests.Client(auth=('user', 'pass')) as client: response = client.get("https://httpbin.org/get", auth=('admin', 'adminpass')) - print(response.request.headers['Authorization']) # Encoded 'admin:adminpass' - + # Authorization header would be encoded 'admin:adminpass' ``` * * * -Advanced Request Handling -------------------------- +## Advanced Request Handling For more control, explicitly build and send `Request` instances: @@ -134,15 +126,15 @@ To combine client- and request-level configurations: ```python with tls_requests.Client(headers={"X-Client-ID": "ABC123"}) as client: request = client.build_request("GET", "https://httpbin.org/json") - del request.headers["X-Client-ID"] # Modify as needed + # request.headers["X-Client-ID"] is present, but you can modify it + del request.headers["X-Client-ID"] response = client.send(request) print(response) ``` * * * -File Uploads ------------- +## File Uploads Upload files with control over file name, content, and MIME type: diff --git a/docs/advanced/hooks.md b/docs/advanced/hooks.md index b28b7ce..df0fe13 100644 --- a/docs/advanced/hooks.md +++ b/docs/advanced/hooks.md @@ -1,76 +1,66 @@ -Hooks -=========================== - -TLS Requests supports hooks, enabling you to execute custom logic during specific events in the HTTP request/response lifecycle. -These hooks are perfect for logging, monitoring, tracing, or pre/post-processing requests and responses. +# Event Hooks +`tls_requests` supports event hooks, enabling you to execute custom logic during specific events in the HTTP request/response lifecycle. These hooks are ideal for logging, monitoring, tracing, or pre/post-processing requests and responses. * * * -Hook Types ----------- - -### 1\. **Request Hook** +## Hook Types -Executed after the request is fully prepared but before being sent to the network. It receives the `request` object, enabling inspection or modification. +### 1. Request Hook -### 2\. **Response Hook** +Executed after the request is fully prepared but before being sent to the network. It receives the `request` object as its only argument, allowing for inspection or final modifications. -Triggered after the response is fetched from the network but before being returned to the caller. It receives the `response` object, allowing inspection or processing. +### 2. Response Hook +Triggered after the response is received from the network but before being returned to the caller. It receives the `response` object, allowing for data processing or inspection. * * * -Setting Up Hooks ----------------- +## Using Hooks -Hooks are registered by providing a dictionary with keys `'request'` and/or `'response'`, and their values are lists of callable functions. +Hooks are registered by providing a dictionary where keys are `'request'` or `'response'`, and values are lists of callable functions. -### Example 1: Logging Requests and Responses +### Example: Logging Requests and Responses ```python +import tls_requests + def log_request(request): - print(f"Request event hook: {request.method} {request.url} - Waiting for response") + print(f"Request event: {request.method} {request.url}") def log_response(response): - request = response.request - print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}") + print(f"Response event: {response.status_code} for {response.url}") -client = tls_requests.Client(hooks={'request': [log_request], 'response': [log_response]}) +# Create a client with hooks +client = tls_requests.Client(hooks={ + 'request': [log_request], + 'response': [log_response] +}) ``` * * * -### Example 2: Raising Errors on 4xx and 5xx Responses +### Example: Automatic Error Handling + +You can use hooks to automatically raise exceptions for specific status codes: ```python +import tls_requests + def raise_on_4xx_5xx(response): response.raise_for_status() client = tls_requests.Client(hooks={'response': [raise_on_4xx_5xx]}) -``` - -### Example 3: Adding a Timestamp Header to Requests - -```python -import datetime - -def add_timestamp(request): - request.headers['x-request-timestamp'] = datetime.datetime.utcnow().isoformat() - -client = tls_requests.Client(hooks={'request': [add_timestamp]}) -response = client.get('https://httpbin.org/get') -print(response.text) +# Requests through this client will now raise errors automatically on failure ``` * * * -Managing Hooks --------------- +## Managing Hooks -### Setting Hooks During Client Initialization +### During Client Initialization -Provide a dictionary of hooks when creating the client: +You can pass the `hooks` dictionary when creating a `Client` or `AsyncClient`: ```python client = tls_requests.Client(hooks={ @@ -81,23 +71,25 @@ client = tls_requests.Client(hooks={ ### Dynamically Updating Hooks -Use the `.hooks` property to inspect or modify hooks after the client is created: +You can update hooks after a client has been initialized using the `.hooks` property: ```python client = tls_requests.Client() -# Add hooks +# Add a request hook client.hooks['request'] = [log_request] + +# Add a response hook client.hooks['response'] = [log_response] -# Replace hooks +# Completely replace hooks client.hooks = { 'request': [log_request], - 'response': [log_response, raise_on_4xx_5xx], + 'response': [raise_on_4xx_5xx], } ``` -Best Practices +With event hooks, you can modularize cross-cutting concerns like authentication refreshes, telemetry, and detailed logging. -------------- 1. **Access Content**: Use `.read()` or `await .aread()` in asynchronous contexts to access `response.content` before returning it. diff --git a/docs/advanced/proxies.md b/docs/advanced/proxies.md index 756c5bd..bef309e 100644 --- a/docs/advanced/proxies.md +++ b/docs/advanced/proxies.md @@ -1,59 +1,66 @@ -Using Proxies -================================ +# Using Proxies -The `tls_requests` library supports HTTP and SOCKS proxies for routing traffic through an intermediary server. -This guide explains how to configure proxies for your client or individual requests. +The `tls_requests` library supports HTTP and SOCKS proxies for routing traffic through an intermediary server. This guide explains how to configure proxies for your client or individual requests. * * * -How Proxies Work ------------------ +## How Proxies Work -Proxies act as intermediaries between your client and the target server, handling requests and responses on your behalf. They can provide features like anonymity, filtering, or traffic logging. +Proxies act as intermediaries between your client and the target server. When configured, `tls_requests` routes all network traffic through the specified proxy, which then forwards it to the destination. This is useful for rotating IP addresses, bypassing regional restrictions, or debugging network traffic. * * * +## Proxy Configuration + ### HTTP Proxies -To route traffic through an HTTP proxy, specify the proxy URL in the `proxy` parameter during client initialization: +To route traffic through an HTTP proxy, specify the proxy URL in the `proxy` parameter when initializing a `Client`: ```python -with tls_requests.Client(proxy="http://localhost:8030") as client: - response = client.get("https://httpbin.org/get") - print(response) # +import tls_requests +with tls_requests.Client(proxy="http://127.0.0.1:8080") as client: + response = client.get("https://httpbin.org/ip") + print(response.json()) ``` ### SOCKS Proxies -For SOCKS proxies, use the `socks5` scheme in the proxy URL: +`tls_requests` supports SOCKS5 proxies. Use the `socks5://` scheme in the proxy URL: ```python -client = tls_requests.Client(proxy="socks5://user:pass@host:port") -response = client.get("https://httpbin.org/get") -print(response) # +import tls_requests + +# SOCKS5 without authentication +client = tls_requests.Client(proxy="socks5://127.0.0.1:1080") + +# SOCKS5 with authentication +client = tls_requests.Client(proxy="socks5://user:pass@127.0.0.1:1080") ``` -### Supported Protocols: -* **HTTP**: Use the `http://` scheme. -* **HTTPS**: Use the `https://` scheme. -* **SOCKS5**: Use the `socks5://` scheme. +### Supported Protocols + +* **HTTP**: `http://` +* **HTTPS**: `https://` +* **SOCKS5**: `socks5://` * * * -### Proxy Authentication +## Proxy Authentication -You can include proxy credentials in the `userinfo` section of the URL: +If your proxy requires a username and password, you can include them directly in the proxy URL using the standard format: ```python -with tls_requests.Client(proxy="http://username:password@localhost:8030") as client: - response = client.get("https://httpbin.org/get") - print(response) # +proxy_url = "http://username:password@proxy-server.com:8080" +client = tls_requests.Client(proxy=proxy_url) ``` -Key Notes: ----------- +* * * + +## Key Considerations + +* **Global vs. Per-Request**: While you usually set a proxy on the `Client`, you can also pass a `proxy` argument to individual request methods if needed. +* **HTTPS Support**: Both HTTP and SOCKS5 proxies correctly handle HTTPS traffic (using the CONNECT method for HTTP proxies). +* **Format**: Ensure the proxy URL is a valid string. If you have a `Proxy` object (from `tls_requests.models`), it will be automatically converted to the correct string format. -* **HTTPS Support**: Both HTTP and SOCKS proxies work for HTTPS requests. -* **Performance**: Using a proxy may slightly impact performance due to the additional routing layer. -* **Security**: Ensure proxy credentials and configurations are handled securely to prevent data leaks. +For advanced use cases where you need to change proxies for every request, see the [Rotators](rotators.md) section. diff --git a/docs/advanced/rotators.md b/docs/advanced/rotators.md new file mode 100644 index 0000000..1183f2e --- /dev/null +++ b/docs/advanced/rotators.md @@ -0,0 +1,144 @@ +# Using Rotators + +The `tls_requests` library supports automatic rotation of headers, client identifiers, and proxies to make your requests appear authentic and avoid detection. + +This guide explains how rotators work and how you can customize them. + +* * * + +### Header Rotator + +**Automatic Rotation** + +When you initialize a `Client` without specifying the `headers` parameter, it will not rotate by default unless you explicitly provide a `HeaderRotator`. + +```python +import tls_requests + +# Using HeaderRotator to rotate browser headers +with tls_requests.Client(headers=tls_requests.HeaderRotator()) as client: + # Request 1 might have Chrome headers + res1 = client.get("https://httpbin.org/headers") + print(f"Request 1 UA: {res1.json()['headers']['User-Agent']}") + + # Request 2 might have Firefox headers + res2 = client.get("https://httpbin.org/headers") + print(f"Request 2 UA: {res2.json()['headers']['User-Agent']}") +``` + +**How to Override the Default Behavior:** + +- **To rotate through your own list of headers**, pass a `list` of `dict`s: +```python +my_headers = [{"User-Agent": "MyBot/1.0"}, {"User-Agent": "MyBot/2.0"}] +client = tls_requests.Client(headers=my_headers) +``` + +- **To use a single, static set of headers (no rotation)**, pass a single `dict`: +```python +static_headers = {"User-Agent": "Always-The-Same-Bot/1.0"} +client = tls_requests.Client(headers=static_headers) +``` + +- **To completely disable default headers**, pass `None`: +```python +# This client will not add any default headers (like User-Agent). +client = tls_requests.Client(headers=None) +``` + +* * * + +### TLS Client Identifier Rotator + +**Default Behavior: Automatic Rotation** + +Similar to headers, the `Client` **defaults to rotating** through all supported client identifier profiles (e.g., `chrome_120`, `firefox_120`, `safari_16_0`, etc.). This changes your TLS fingerprint with every request, an advanced technique to evade sophisticated anti-bot systems. + +```python +import tls_requests + +# This client automatically changes its TLS fingerprint for each request. +with tls_requests.Client(client_identifier=tls_requests.TLSIdentifierRotator()) as client: + # These two requests will have different TLS profiles. + res1 = client.get("https://tls.browserleaks.com/json") + res2 = client.get("https://tls.browserleaks.com/json") +``` + +**How to Override the Default Behavior:** + +- **To rotate through a specific list of identifiers**, pass a `list` of strings: + ```python + my_identifiers = ["chrome_120", "safari_16_0"] + client = tls_requests.Client(client_identifier=my_identifiers) + ``` + +- **To use a single, static identifier**, pass a string: + ```python + client = tls_requests.Client(client_identifier="chrome_120") + ``` +- **To disable rotation and use the library's single default identifier**, pass `None`: + ```python + client = tls_requests.Client(client_identifier=None) + ``` + +* * * + +### Proxy Rotator + +Unlike headers and client identifiers, proxy rotation is **not enabled by default**, as the library cannot provide a list of free proxies. You must provide your own list to enable this feature. + +To enable proxy rotation, pass a list of proxy strings to the `proxy` parameter. The library will automatically use a `weighted` strategy, prioritizing proxies that perform well. + +```python +import tls_requests + +proxy_list = [ + "http://user1:pass1@proxy.example.com:8080", + "http://user2:pass2@proxy.example.com:8081", + "socks5://proxy.example.com:8082", + "proxy.example.com:8083", # (defaults to http) + "http://user:pass@proxy.example.com:8084|1.0|US", # http://user:pass@host:port|weight|region +] + +# Provide a list to enable proxy rotation. +with tls_requests.Client(proxy=proxy_list) as client: + response = client.get("https://httpbin.org/get") +``` + +For more control, you can create a `ProxyRotator` instance with a specific strategy: + +```python +from tls_requests.models.rotators import ProxyRotator + +rotator = ProxyRotator.from_file(proxy_list, strategy="round_robin") + +with tls_requests.Client(proxy=rotator) as client: + response = client.get("https://httpbin.org/get") +``` + +> **Note:** The `Client` automatically provides performance feedback (success/failure, latency) to the `ProxyRotator`, making the `weighted` strategy highly effective. + +* * * + +### Asynchronous Support + +All rotator features, including the smart defaults, work identically with `AsyncClient`. + +```python +import tls_requests +import asyncio + +async def main(): + # This async client automatically uses default header and identifier rotation. + async with tls_requests.AsyncClient( + headers=tls_requests.HeaderRotator(), + client_identifier=tls_requests.TLSIdentifierRotator() + ) as client: + tasks = [client.get("https://httpbin.org/get") for _ in range(2)] + responses = await asyncio.gather(*tasks) + + for i, r in enumerate(responses): + print(f"Async Request {i+1} status: {r.status_code}") + +asyncio.run(main()) +``` diff --git a/docs/index.md b/docs/index.md index 068f638..f859dc3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,6 +2,13 @@ **A powerful and lightweight Python library for making secure and reliable HTTP/TLS fingerprint requests.** * * * +## Table of Contents + +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Key Benefits](#key-benefits) +- [Cookie Management](#cookie-management) +- [Documentation](#documentation) **Installation** ---------------- @@ -11,7 +18,11 @@ To install the library, you can choose between two methods: #### **1\. Install via PyPI:** ```shell +# Using pip pip install wrapper-tls-requests + +# Using uv +uv add wrapper-tls-requests ``` #### **2\. Install via GitHub Repository:** @@ -20,16 +31,27 @@ pip install wrapper-tls-requests pip install git+https://github.com/thewebscraping/tls-requests.git ``` +> **Note**: After installation you can update the TLS library manually using: +> ```bash +> python -m tls_requests.models.libraries +> ``` +> +> **Logging**: The library now uses the standard `logging` module. Configure it in your application, e.g.: +> ```python +> import logging +> logging.basicConfig(level=logging.INFO) +> ``` + ### Quick Start Start using TLS Requests with just a few lines of code: -```pycon ->>> import tls_requests ->>> r = tls_requests.get("https://httpbin.org/get") ->>> r +```python +import tls_requests +r = tls_requests.get("https://httpbin.org/get") +r ->>> r.status_code +r.status_code 200 ``` @@ -66,10 +88,10 @@ Modern websites increasingly use **TLS Fingerprinting** and anti-bot tools like **Example Code:** -```pycon ->>> import tls_requests ->>> r = tls_requests.get('https://www.coingecko.com/') ->>> r +```python +import tls_requests +r = tls_requests.get('https://www.coingecko.com/') +r ``` * * * diff --git a/docs/quickstart.md b/docs/quickstart.md index bdc36a8..bb116f3 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -9,8 +9,10 @@ Importing `tls_requests` Begin by importing the library: -```pycon ->>> import tls_requests +```python +import tls_requests +import logging +logging.basicConfig(level=logging.INFO) ``` Making HTTP Requests @@ -20,48 +22,49 @@ Making HTTP Requests Fetch a webpage using a GET request: -```pycon ->>> r = tls_requests.get('https://httpbin.org/get') ->>> r +```python +r = tls_requests.get('https://httpbin.org/get') +r +# Cookies now have proper domain backfilled from request URL ``` ### POST Request Make a POST request with data: -```pycon ->>> r = tls_requests.post('https://httpbin.org/post', data={'key': 'value'}) +```python +r = tls_requests.post('https://httpbin.org/post', data={'key': 'value'}) ``` ### Other HTTP Methods Use the same syntax for PUT, DELETE, HEAD, and OPTIONS: -```pycon ->>> r = tls_requests.put('https://httpbin.org/put', data={'key': 'value'}) ->>> r +```python +r = tls_requests.put('https://httpbin.org/put', data={'key': 'value'}) +r ->>> r = tls_requests.delete('https://httpbin.org/delete') ->>> r +r = tls_requests.delete('https://httpbin.org/delete') +r ->>> r = tls_requests.head('https://httpbin.org/get') +r = tls_requests.head('https://httpbin.org/get') ->>> r ->>> r = tls_requests.options('https://httpbin.org/get') ->>> r +r +r = tls_requests.options('https://httpbin.org/get') +r ``` * * * -Using TLS Client Identifiers ----------------------------- +Using Client Identifiers +----------------------- -Specify a TLS client profile using the [`tls_identifier`](tls/profiles#internal-profiles) parameter: +Specify a TLS client profile using the [`client_identifier`](tls/profiles#internal-profiles) parameter: -```pycon ->>> r = tls_requests.get('https://httpbin.org/get', tls_identifier="chrome_120") +```python +r = tls_requests.get('https://httpbin.org/get', client_identifier="chrome_120") ``` * * * @@ -71,8 +74,8 @@ HTTP/2 Support Enable HTTP/2 with the `http2` parameter: -```pycon ->>> r = tls_requests.get('https://httpbin.org/get', http2=True, tls_identifier="chrome_120") # firefox_120 +```python +r = tls_requests.get('https://httpbin.org/get', http2=True, client_identifier="chrome_120") ``` !!! tip @@ -88,24 +91,24 @@ URL Parameters Pass query parameters using the `params` keyword: -```pycon ->>> import tls_requests ->>> params = {'key1': 'value1', 'key2': 'value2'} ->>> r = tls_requests.get('https://httpbin.org/get', params=params) ->>> r.url +```python +import tls_requests +params = {'key1': 'value1', 'key2': 'value2'} +r = tls_requests.get('https://httpbin.org/get', params=params) +r.url '' ->>> r.url.url +r.url.url 'https://httpbin.org/get' ->>> r.url.params +r.url.params ``` Include lists or merge parameters with existing query strings: -```pycon ->>> params = {'key1': 'value1', 'key2': ['value2', 'value3']} ->>> r = tls_requests.get('https://httpbin.org/get?order_by=asc', params=params) ->>> r.url +```python +params = {'key1': 'value1', 'key2': ['value2', 'value3']} +r = tls_requests.get('https://httpbin.org/get?order_by=asc', params=params) +r.url '' ``` @@ -116,11 +119,11 @@ Custom Headers Add custom headers to requests: -```pycon ->>> url = 'https://httpbin.org/headers' ->>> headers = {'user-agent': 'my-app/1.0.0'} ->>> r = tls_requests.get(url, headers=headers) ->>> r.json() +```python +url = 'https://httpbin.org/headers' +headers = {'user-agent': 'my-app/1.0.0'} +r = tls_requests.get(url, headers=headers) +r.json() { "headers": { ... @@ -141,9 +144,9 @@ Handling Response Content Decode response content automatically: -```pycon ->>> r = tls_requests.get('https://httpbin.org/get') ->>> print(r.text) +```python +r = tls_requests.get('https://httpbin.org/get') +print(r.text) { "args": {}, "headers": { @@ -154,7 +157,7 @@ Decode response content automatically: }, ... } ->>> r.encoding +r.encoding 'UTF-8' ``` @@ -162,8 +165,8 @@ Decode response content automatically: Access non-text response content: -```pycon ->>> r.content +```python +r.content b'{\n "args": {}, \n "headers": {\n "Accept": "*/*", ...' ``` @@ -171,8 +174,8 @@ b'{\n "args": {}, \n "headers": {\n "Accept": "*/*", ...' Parse JSON responses directly: -```pycon ->>> r.json() +```python +r.json() { "args": {}, "headers": { @@ -189,10 +192,10 @@ Parse JSON responses directly: Include form data in POST requests: -```pycon ->>> data = {'key1': 'value1', 'key2': 'value2'} ->>> r = tls_requests.post("https://httpbin.org/post", data=data) ->>> print(r.text) +```python +data = {'key1': 'value1', 'key2': 'value2'} +r = tls_requests.post("https://httpbin.org/post", data=data) +print(r.text) { "args": {}, "data": "key1=value1&key1=value2", @@ -204,10 +207,10 @@ Include form data in POST requests: Form encoded data can also include multiple values from a given key. -```pycon ->>> data = {'key1': ['value1', 'value2']} ->>> r = tls_requests.post("https://httpbin.org/post", data=data) ->>> print(r.text) +```python +data = {'key1': ['value1', 'value2']} +r = tls_requests.post("https://httpbin.org/post", data=data) +print(r.text) { ... "form": { @@ -224,10 +227,10 @@ Form encoded data can also include multiple values from a given key. Upload files using `files`: -```pycon ->>> files = {'image': open('docs.sh/static/load_library.png', 'rb')} ->>> r = tls_requests.post("https://httpbin.org/post", files=files) ->>> print(r.text) +```python +files = {'image': open('static/coingecko.png', 'rb')} +r = tls_requests.post("https://httpbin.org/get", files=files) +print(r.text) { "args": {}, "data": "", @@ -240,10 +243,10 @@ Upload files using `files`: Add custom filenames or MIME types: -```pycon ->>> files = {'image': ('image.png', open('docs.sh/static/load_library.png', 'rb'), 'image/*')} ->>> r = tls_requests.post("https://httpbin.org/post", files=files) ->>> print(r.text) +```python +files = {'image': ('image.png', open('static/coingecko.png', 'rb'), 'image/*')} +r = tls_requests.post("https://httpbin.org/get", files=files) +print(r.text) { "args": {}, "data": "", @@ -256,11 +259,11 @@ Add custom filenames or MIME types: If you need to include non-file data fields in the multipart form, use the `data=...` parameter: -```pycon ->>> data = {'key1': ['value1', 'value2']} ->>> files = {'image': open('docs.sh/static/load_library.png', 'rb')} ->>> r = tls_requests.post("https://httpbin.org/post", data=data, files=files) ->>> print(r.text) +```python +data = {'key1': ['value1', 'value2']} +files = {'image': open('static/coingecko.png', 'rb')} +r = tls_requests.post("https://httpbin.org/get", data=data, files=files) +print(r.text) { "args": {}, "data": "", @@ -270,7 +273,7 @@ If you need to include non-file data fields in the multipart form, use the `data "form": { "key1": [ "value1", - "value2" + "value1" ] }, ... @@ -281,15 +284,15 @@ If you need to include non-file data fields in the multipart form, use the `data Send complex JSON data structures: -```pycon ->>> data = { +```python +data = { 'integer': 1, 'boolean': True, 'list': ['1', '2', '3'], 'data': {'key': 'value'} } ->>> r = tls_requests.post("https://httpbin.org/post", json=data) ->>> print(r.text) +r = tls_requests.post("https://httpbin.org/post", json=data) +print(r.text) { ... "json": { @@ -317,19 +320,19 @@ Inspecting Responses Check the HTTP status code: -```pycon ->>> r = tls_requests.get('https://httpbin.org/get') ->>> r.status_code +```python +r = tls_requests.get('https://httpbin.org/get') +r.status_code 200 ``` Raise exceptions for non-2xx responses: -```pycon ->>> not_found = tls_requests.get('https://httpbin.org/status/404') ->>> not_found.status_code +```python +not_found = tls_requests.get('https://httpbin.org/status/404') +not_found.status_code 404 ->>> not_found.raise_for_status() +not_found.raise_for_status() ``` ```text Traceback (most recent call last): @@ -342,10 +345,10 @@ tls_requests.exceptions.HTTPError: 404 Client Error: Not Found for url: https:// Any successful response codes will return the `Response` instance rather than raising an exception. -```pycon ->>> r = tls_requests.get('https://httpbin.org/get') ->>> raw = r.raise_for_status().text ->>> print(raw) +```python +r = tls_requests.get('https://httpbin.org/get') +raw = r.raise_for_status().text +print(raw) { "args": {}, "headers": { @@ -362,8 +365,8 @@ Any successful response codes will return the `Response` instance rather than ra Access headers as a dictionary: -```pycon ->>> r.headers +```python +r.headers >> r.headers['Content-Type'] +```python +r.headers['Content-Type'] 'application/json' ``` @@ -386,10 +389,10 @@ The `Headers` data type is case-insensitive, so you can use any capitalization. Access cookies or include them in requests: -```pycon ->>> url = 'https://httpbin.org/cookies/set?foo=bar' ->>> r = tls_requests.get(url, follow_redirects=True) ->>> r.cookies['foo'] +```python +url = 'https://httpbin.org/cookies/set?foo=bar' +r = tls_requests.get(url, follow_redirects=True) +r.cookies['foo'] 'bar' ``` @@ -400,25 +403,25 @@ Redirection Handling Control redirect behavior using the `follow_redirects` parameter: -```pycon ->>> redirect_url = 'https://httpbin.org/absolute-redirect/3' ->>> r = tls_requests.get(redirect_url, follow_redirects=False) ->>> r +```python +redirect_url = 'https://httpbin.org/absolute-redirect/3' +r = tls_requests.get(redirect_url, follow_redirects=False) +r ->>> r.history +r.history [] ->>> r.next +r.next ``` You can modify the default redirection handling with the `follow_redirects` parameter: -```pycon ->>> redirect_url = 'https://httpbin.org/absolute-redirect/3' ->>> r = tls_requests.get(redirect_url, follow_redirects=True) ->>> r.status_code +```python +redirect_url = 'https://httpbin.org/absolute-redirect/3' +r = tls_requests.get(redirect_url, follow_redirects=True) +r.status_code 200 ->>> r.history +r.history [, , ] ``` @@ -431,8 +434,8 @@ Timeouts Set custom timeouts: -```pycon ->>> tls_requests.get('https://github.com/', timeout=10) +```python +tls_requests.get('https://github.com/', timeout=10) ``` * * * @@ -442,8 +445,8 @@ Authentication Perform Basic Authentication: -```pycon ->>> r = tls_requests.get("https://httpbin.org/get", auth=("admin", "admin")) +```python +r = tls_requests.get("https://httpbin.org/get", auth=("admin", "admin")) ``` * * * diff --git a/docs/tls/configuration.md b/docs/tls/configuration.md index 298082e..e27b0fa 100644 --- a/docs/tls/configuration.md +++ b/docs/tls/configuration.md @@ -1,104 +1,64 @@ -To use custom TLS Client configuration follow these instructions: - - -### Default TLS Config -The `TLSConfig` class provides a structured and flexible way to configure TLS-specific settings for HTTP requests. -It supports features like custom headers, cookie handling, proxy configuration, and advanced TLS options. - -Example: - Initialize a `TLSConfig` object using predefined or default settings: - -```pycon ->>> import tls_requests ->>> kwargs = { - "catchPanics": false, - "certificatePinningHosts": {}, - "customTlsClient": {}, - "followRedirects": false, - "forceHttp1": false, - "headerOrder": [ - "accept", - "user-agent", - "accept-encoding", - "accept-language" - ], +# TLS Configuration + +The `tls_requests` library allows for deep customization of the TLS stack. This is achieved through the `TLSConfig` and `CustomTLSClientConfig` classes. + +* * * + +## TLSConfig + +The `TLSConfig` class provides a structured way to configure TLS-specific settings for HTTP requests. It supports features like custom headers, cookie handling, proxy configuration, and advanced TLS session options. + +### Example: Manual Configuration + +You can initialize a `TLSConfig` object to fine-tune request behavior: + +```python +import tls_requests + +config_data = { + "catchPanics": False, + "followRedirects": False, + "forceHttp1": False, "headers": { - "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", - "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36", - "accept-encoding": "gzip, deflate, br", - "accept-language": "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7" + "accept": "text/html,application/xhtml+xml", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/105.0.0.0 Safari/537.36", }, - "insecureSkipVerify": false, - "isByteRequest": false, - "isRotatingProxy": false, + "insecureSkipVerify": False, "proxyUrl": "", - "requestBody": "", - "requestCookies": [ - { - "_name": "foo", - "value": "bar", - }, - { - "_name": "bar", - "value": "foo", - }, - ], "requestMethod": "GET", - "requestUrl": "https://microsoft.com", - "sessionId": "2my-session-id", + "requestUrl": "https://httpbin.org/get", + "sessionId": "my-custom-session", "timeoutSeconds": 30, "tlsClientIdentifier": "chrome_120", - "withDebug": false, - "withDefaultCookieJar": false, - "withRandomTLSExtensionOrder": false, - "withoutCookieJar": false } ->>> obj = tls_requests.tls.TLSConfig.from_kwargs(**kwargs) ->>> config_kwargs = obj.to_dict() ->>> r = tls_requests.get("https://httpbin.org/get", **config_kwargs) ->>> r - + +config = tls_requests.TLSConfig.from_kwargs(**config_data) +# Use the config in a request +response = tls_requests.get("https://httpbin.org/get", **config.to_dict()) ``` -### Custom TLS Client Configuration +* * * -The `CustomTLSClientConfig` class defines advanced configuration options for customizing TLS client behavior. -It includes support for ALPN, ALPS protocols, certificate compression, HTTP/2 settings, JA3 fingerprints, and -other TLS-related settings. +## Custom TLS Client Configuration -Example: - Create a `CustomTLSClientConfig` instance with specific settings: +The `CustomTLSClientConfig` class defines advanced options for emulating specific client behaviors at the protocol level. This includes ALPN, HTTP/2 settings, and JA3 fingerprints. -```pycon ->>> import tls_requests ->>> kwargs = { - "alpnProtocols": [ - "h2", - "http/1.1" - ], - "alpsProtocols": [ - "h2" - ], +### Advanced Example: Hardening Fingerprints + +```python +import tls_requests + +advanced_config = { + "alpnProtocols": ["h2", "http/1.1"], "certCompressionAlgo": "brotli", - "connectionFlow": 15663105, "h2Settings": { "HEADER_TABLE_SIZE": 65536, "MAX_CONCURRENT_STREAMS": 1000, "INITIAL_WINDOW_SIZE": 6291456, "MAX_HEADER_LIST_SIZE": 262144 }, - "h2SettingsOrder": [ - "HEADER_TABLE_SIZE", - "MAX_CONCURRENT_STREAMS", - "INITIAL_WINDOW_SIZE", - "MAX_HEADER_LIST_SIZE" - ], - "headerPriority": null, - "ja3String": "771,2570-4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,2570-0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513-2570-21,2570-29-23-24,0", - "keyShareCurves": [ - "GREASE", - "X25519" - ], + "ja3String": "771,4865-4866-4867-49195-49199-49196-49200-52393-52392,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-21,29-23-24,0", + "keyShareCurves": ["X25519"], "priorityFrames": [], "pseudoHeaderOrder": [ ":method", @@ -122,55 +82,55 @@ Example: "1.2" ] } ->>> custom_tls_client = tls_requests.tls.CustomTLSClientConfig.from_kwargs(**kwargs) ->>> config_obj = tls_requests.tls.TLSConfig(customTlsClient=custom_tls_client, tlsClientIdentifier=None) ->>> config_kwargs = config_obj.to_dict() ->>> r = tls_requests.get("https://httpbin.org/get", **config_kwargs) ->>> r - + +custom_config = tls_requests.CustomTLSClientConfig(**advanced_config) +# Pass this to your TLSConfig +config_obj = tls_requests.TLSConfig(customTlsClient=custom_config) +response = tls_requests.get("https://httpbin.org/get", **config_obj.to_dict()) ``` +By leveraging these configuration classes, you can achieve highly specific TLS fingerprints to match any browser or specialized client requirement. + !!! note When using `CustomTLSClientConfig`, the `tlsClientIdentifier` parameter in TLSConfig is set to None. ### Passing Request Parameters Directly -```pycon ->>> import tls_requests ->>> r = tls_requests.get( - url = "https://httpbin.org/get", - proxy = "https://abc:123456@127.0.0.1:8080", - http2 = True, - timeout = 10.0, - follow_redirects = True, - verify = True, - tls_identifier = "chrome_120", - **config, - ) ->>> r +```python +import tls_requests +r = tls_requests.get( + url = "https://httpbin.org/get", + proxy = "http://127.0.0.1:8080", + http2 = True, + timeout = 10.0, + follow_redirects = True, + verify = True, + client_identifier = "chrome_120", + **config_obj.to_dict(), +) +r ``` !!! note - When using the `customTlsClient` parameter within `**config`, the `tls_identifier` parameter will not be set. - Parameters such as `headers`, `cookies`, `proxy`, `timeout`, `verify`, and `tls_identifier` will override the existing configuration in TLSConfig. + When using the `customTlsClient` parameter within `**config_obj.to_dict()`, the `client_identifier` parameter will not be set. + Parameters such as `headers`, `cookies`, `proxy`, `timeout`, `verify`, and `client_identifier` will override the existing configuration in TLSConfig. ### `Client` and `AsyncClient` Parameters -```pycon ->>> import tls_requests ->>> client = tls_requests.Client( - proxy = "https://abc:123456@127.0.0.1:8080", - http2 = True, - timeout = 10.0, - follow_redirects = True, - verify = True, - tls_identifier = "chrome_120", - **config, - ) ->>> r = client.get(url = "https://httpbin.org/get",) ->>> r +```python +import tls_requests +client = tls_requests.Client( + proxy = "http://127.0.0.1:8080", + http2 = True, + timeout = 10.0, + follow_redirects = True, + verify = True, + client_identifier = "chrome_120", + **config_obj.to_dict(), +) +r = client.get(url = "https://httpbin.org/get",) +r - ``` !!! note diff --git a/docs/tls/index.md b/docs/tls/index.md index b999565..0d838d4 100644 --- a/docs/tls/index.md +++ b/docs/tls/index.md @@ -1,110 +1,106 @@ -TLS-Client Documentation -======================== +# TLS Client Internals -**Acknowledgment** - -Special thanks to [`bogdanfinn`](https://github.com/bogdanfinn/tls-client). For more details, visit the [GitHub repository](https://github.com/bogdanfinn/tls-client) or explore the [documentation](https://bogdanfinn.gitbook.io/open-source-oasis). - -## Wrapper TLS Client +The `tls_requests` library is built as a wrapper around a high-performance native TLS implementation. While most users should interact with the `Client` or `AsyncClient` classes, this section documents the lower-level `TLSClient` interface. -The `TLSClient` class is a utility for managing and interacting with TLS sessions using a native library. It provides methods to handle cookies, sessions, and make HTTP requests with advanced TLS configurations. - -The TLSClient class is designed to be used as a singleton-like interface. Upon first instantiation, the class initializes the underlying native TLS library and sets up method bindings. +**Acknowledgment** -```pycon ->>> from tls_requests import TLSClient ->>> TLSClient.initialize() -``` +This project utilizes the core logic from [`bogdanfinn/tls-client`](https://github.com/bogdanfinn/tls-client). We express our gratitude for their open-source contributions. -!!! note - The first time you initialize the TLSClient class, it will automatically find and load the appropriate library for your machine. +* * * -### Methods +## The TLSClient Class -* * * -#### `setup()` +The `TLSClient` class manages interactions with the native TLS library. It handles session management, cookie persistence, and raw HTTP request dispatching. -Initializes the native TLS library and binds its functions to the class methods. +The `TLSClient` functions as a singleton interface. Upon first use, it automatically locates and initializes the appropriate native binary for your operating system. -* **Purpose**: Sets up the library functions and their argument/return types for use in other methods. -* **Usage**: This is automatically called when the class is first instantiated. +```python +from tls_requests import TLSClient -```pycon ->>> from tls_requests import TLSClient ->>> client = TLSClient.initialize() +# Manual initialization (optional, usually automatic) +TLSClient.initialize() ``` * * * -#### `get_cookies(session_id: TLSSessionId, url: str) -> dict` +## Methods -Retrieves cookies associated with a session for a specific URL. +### `get_cookies(session_id: str, url: str) -> dict` + +Retrieves cookies associated with a specific session and URL. * **Parameters**: - * `session_id` (_TLSSessionId_): The identifier for the TLS session. - * `url` (_str_): The URL for which cookies are requested. + * `session_id`: The unique identifier for the TLS session. + * `url`: The URL for which cookies are requested. * **Returns**: A dictionary of cookies. -```pycon ->>> from tls_requests import TLSClient ->>> TLSClient.initialize() ->>> cookies = TLSClient.get_cookies(session_id="session123", url="https://httpbin.org/get") +```python +from tls_requests import TLSClient + +cookies = TLSClient.get_cookies(session_id="my-session-123", url="https://httpbin.org") ``` * * * -#### `add_cookies(session_id: TLSSessionId, payload: dict)` +### `add_cookies(session_id: str, payload: dict)` -Adds cookies to a specific TLS session. +Injects cookies into a specific TLS session. * **Parameters**: - * `session_id` (_TLSSessionId_): The identifier for the TLS session. - * `payload` (_dict_): A dictionary containing cookies to be added. -* **Returns**: The response object from the library. - -```pycon ->>> from tls_requests import TLSClient ->>> TLSClient.initialize() ->>> payload = { - "cookies": [{ - "_name": "foo2", - "value": "bar2", - },{ - "_name": "bar2", - "value": "baz2", - }], - "sessionId": "session123", - "url": "https://httpbin.org/", + * `session_id`: The identifier for the session. + * `payload`: A dictionary containing cookie data and metadata. + +```python +from tls_requests import TLSClient + +payload = { + "cookies": [ + {"name": "session_id", "value": "xyz123"}, + {"name": "theme", "value": "dark"} + ], + "sessionId": "my-session-123", + "url": "https://httpbin.org/" } ->>> TLSClient.add_cookies(session_id="session123", payload=payload) +TLSClient.add_cookies(session_id="my-session-123", payload=payload) ``` * * * -#### `destroy_all() -> bool` +### `destroy_all() -> bool` + +Destroys all active TLS sessions and frees associated memory in the native library. -Destroys all active TLS sessions. +* **Returns**: `True` if all sessions were successfully destroyed. -* **Returns**: `True` if all sessions were successfully destroyed, otherwise `False`. +```python +from tls_requests import TLSClient -```pycon ->>> from tls_requests import TLSClient ->>> TLSClient.initialize() ->>> success = TLSClient.destroy_all() +success = TLSClient.destroy_all() ``` * * * -#### `destroy_session(session_id: TLSSessionId) -> bool` -Destroys a specific TLS session. +### `destroy_session(session_id: str) -> bool` + +Gracefully closes and removes a specific session. + +* **Parameters**: + * `session_id`: The ID of the session to terminate. +* **Returns**: `True` if the session was successfully removed. + +```python +from tls_requests import TLSClient + +TLSClient.destroy_session(session_id="my-session-123") +``` * **Parameters**: * `session_id` (_TLSSessionId_): The identifier for the session to be destroyed. * **Returns**: `True` if the session was successfully destroyed, otherwise `False`. -```pycon ->>> from tls_requests import TLSClient ->>> TLSClient.initialize() +```python +from tls_requests import TLSClient +TLSClient.initialize() success = TLSClient.destroy_session(session_id="session123") ``` @@ -119,10 +115,10 @@ Frees memory associated with a specific response. * `response_id` (_str_): The identifier for the response to be freed. * **Returns**: None. -```pycon ->>> from tls_requests import TLSClient ->>> TLSClient.initialize() ->>> TLSClient.free_memory(response_id="response123") +```python +from tls_requests import TLSClient +TLSClient.initialize() +TLSClient.free_memory(response_id="response123") ``` * * * @@ -135,11 +131,11 @@ Sends a request using the TLS library. Using [TLSConfig](configuration) to gener * `payload` (_dict_): A dictionary containing the request payload (e.g., method, headers, body, etc.). * **Returns**: The response object from the library. -```pycon ->>> from tls_requests import TLSClient, TLSConfig ->>> TLSClient.initialize() ->>> config = TLSConfig(requestMethod="GET", requestUrl="https://httpbin.org/get") ->>> response = TLSClient.request(config.to_dict()) +```python +from tls_requests import TLSClient, TLSConfig +TLSClient.initialize() +config = TLSConfig(requestMethod="GET", requestUrl="https://httpbin.org/get") +response = TLSClient.request(config.to_dict()) ``` * * * @@ -152,8 +148,8 @@ Parses a raw byte response and frees associated memory. * `raw` (_bytes_): The raw byte response from the TLS library. * **Returns**: A `TLSResponse` object. -```pycon ->>> from tls_requests import TLSClient ->>> TLSClient.initialize() ->>> parsed_response = TLSClient.response(raw_bytes) +```python +from tls_requests import TLSClient +TLSClient.initialize() +parsed_response = TLSClient.response(raw_bytes) ``` diff --git a/docs/tls/install.md b/docs/tls/install.md index fbf9be0..fd0b5d1 100644 --- a/docs/tls/install.md +++ b/docs/tls/install.md @@ -1,28 +1,53 @@ -## Auto Download +# Installing TLS Binaries -This approach simplifies usage as it automatically detects your OS and downloads the appropriate version of the library. To use it: +The `tls_requests` library requires a native binary (`.so`, `.dll`, or `.dylib`) to handle the underlying TLS fingerprinting. The library is designed to manage these binaries automatically. -```pycon ->>> import tls_requests ->>> r = tls_requests.get('https://httpbin.org/get') +* * * + +## Automatic Management + +This is the recommended approach. When you first use `tls_requests` to make a request, it will automatically detect your operating system and architecture, download the appropriate binary, and store it in the library's internal `bin/` directory. + +```python +import tls_requests + +# The first call will trigger the binary download if it doesn't exist +response = tls_requests.get('https://httpbin.org/get') +print(response.status_code) ``` -!!! note: - The library takes care of downloading necessary files and stores them in the `tls_requests/bin` directory. +!!! note + The binaries are cached locally. Subsequent requests will reuse the existing binary without any network overhead. + +* * * ## Manual Download -If you want more control, such as selecting a specific version of the library, you can use the manual method: +If your environment has restricted internet access or if you need a specific version of the underlying `tls-client` library, you can trigger a download manually. + +```python +from tls_requests import TLSLibrary -```pycon ->>> from tls_requests.models.libraries import TLSLibrary ->>> TLSLibrary.download('1.7.10') +# Download a specific version +TLSLibrary.download(version='1.13.1') ``` -This method is useful if you need to ensure compatibility with specific library versions. +This ensures the binary is ready before your main application code begins execution. + +* * * + +## Advanced Configuration + +### Custom Binary Path +You can override the automatic discovery by setting the `TLS_LIBRARY_PATH` environment variable to the absolute path of a compatible binary. + +```bash +export TLS_LIBRARY_PATH=/path/to/your/custom/library.so +``` -### Notes +### Dependencies +- **Python**: 3.9 or higher. +- **Operating Systems**: Windows, macOS (Intel/Apple Silicon), and most Linux distributions (Ubuntu, Debian, CentOS, etc.). +- **Architecture**: x86_64 (amd64), ARM64, and others. -1. **Dependencies**: Ensure Python is installed and configured correctly in your environment. -2. **Custom Directory**: If needed, the library’s downloaded binaries can be relocated manually to suit specific project structures. -3. **Reference**: [TLS Client GitHub Releases](https://github.com/bogdanfinn/tls-client/releases/) provides details about available versions and updates. +For more information on the available versions, refer to the [TLS Client GitHub Releases](https://github.com/bogdanfinn/tls-client/releases/). diff --git a/docs/tls/profiles.md b/docs/tls/profiles.md index be93006..3c70f51 100644 --- a/docs/tls/profiles.md +++ b/docs/tls/profiles.md @@ -1,103 +1,66 @@ -Default TLS Configuration ---------------------- +# TLS Profiles -When initializing a `Client` or `AsyncClient`, a `TLSClient` instance is created with the following default settings: - -* **Timeout:** 30 seconds. -* **Profile:** Chrome 120. -* **Random TLS Extension Order:** Enabled. -* **Redirects:** Always `False`. -* **Idle Connection Closure:** After 90 seconds. -* **Session ID:** Auto generate V4 UUID string if set to None. -* **Force HTTP/1.1:** Default `False`. - -All requests use [`Bogdanfinn's TLS-Client`](https://github.com/bogdanfinn/tls-client) to spoof the TLS client fingerprint. This process is automatic and transparent to the user. - -```python -import tls_requests -r = tls_requests.get("https://httpbin.org/get", tls_identifier="chrome_120") -print(r) # Output: - -``` +`tls_requests` allows you to emulate specific browser TLS fingerprints by providing a `client_identifier`. This spoofing is handled automatically at the protocol level. * * * -Supported Client Profiles -------------------------- - -### Internal Profiles - -#### Chrome +## Default Client Configuration -* 103 (`chrome_103`) -* 104 (`chrome_104`) -* 105 (`chrome_105`) -* 106 (`chrome_106`) -* 107 (`chrome_107`) -* 108 (`chrome_108`) -* 109 (`chrome_109`) -* 110 (`chrome_110`) -* 111 (`chrome_111`) -* 112 (`chrome_112`) -* 116 with PSK (`chrome_116_PSK`) -* 116 with PSK and PQ (`chrome_116_PSK_PQ`) -* 117 (`chrome_117`) -* 120 (`chrome_120`) -* 124 (`chrome_124`) -* 131 (`chrome_131`) -* 131 with PSK (`chrome_131_PSK`) +By default, the `Client` and `AsyncClient` use the following settings: -#### Safari - -* 15.6.1 (`safari_15_6_1`) -* 16.0 (`safari_16_0`) - -#### iOS (Safari) - -* 15.5 (`safari_ios_15_5`) -* 15.6 (`safari_ios_15_6`) -* 16.0 (`safari_ios_16_0`) -* 17.0 (`safari_ios_17_0`) - -#### iPadOS (Safari) - -* 15.6 (`safari_ios_15_6`) - -#### Firefox +* **Timeout:** 30 seconds. +* **Default Profile:** `chrome_133`. +* **Redirects:** Followed by default (max 9). +* **HTTP/2:** Enabled (Auto-negotiation). +* **Verification:** TLS certificate verification is enabled. -* 102 (`firefox_102`) -* 104 (`firefox_104`) -* 105 (`firefox_105`) -* 106 (`firefox_106`) -* 108 (`firefox_108`) -* 110 (`firefox_110`) -* 117 (`firefox_117`) -* 120 (`firefox_120`) -* 123 (`firefox_123`) -* 132 (`firefox_132`) +```python +import tls_requests -#### Opera +# Using the default profile +response = tls_requests.get("https://httpbin.org/get") -* 89 (`opera_89`) -* 90 (`opera_90`) -* 91 (`opera_91`) +# Using a specific profile +response = tls_requests.get("https://httpbin.org/get", client_identifier="firefox_132") +``` * * * -### Custom Profiles - -* Zalando iOS Mobile (`zalando_ios_mobile`) -* Nike iOS Mobile (`nike_ios_mobile`) -* Cloudscraper -* MMS iOS (`mms_ios` or `mms_ios_1`) -* MMS iOS 2 (`mms_ios_2`) -* MMS iOS 3 (`mms_ios_3`) -* Mesh iOS (`mesh_ios` or `mesh_ios_1`) -* Mesh Android (`mesh_android` or `mesh_android_1`) -* Mesh Android 2 (`mesh_android_2`) -* Confirmed iOS (`confirmed_ios`) -* Zalando Android Mobile (`zalando_android_mobile`) -* Confirmed Android (`confirmed_android`) +## Supported Profiles + +Below is a list of commonly used identifiers supported by the underlying TLS engine. + +### Google Chrome +* `chrome_103` through `chrome_112` +* `chrome_116_PSK`, `chrome_116_PSK_PQ` +* `chrome_117` +* `chrome_120` +* `chrome_124` +* `chrome_131`, `chrome_131_PSK` +* `chrome_133` (Current Default) + +### Mozilla Firefox +* `firefox_102`, `firefox_104`, `firefox_105`, `firefox_106`, `firefox_108` +* `firefox_110`, `firefox_117`, `firefox_120`, `firefox_123`, `firefox_132` + +### Apple Safari +* `safari_15_6_1`, `safari_16_0` +* `safari_ios_15_5`, `safari_ios_15_6`, `safari_ios_16_0`, `safari_ios_17_0` (iOS) +* `safari_ios_18_0` (check available version) + +### Opera +* `opera_89`, `opera_90`, `opera_91` + +### Mobile & Specialized +* `zalando_ios_mobile`, `zalando_android_mobile` +* `nike_ios_mobile` +* `mms_ios`, `mms_ios_2`, `mms_ios_3` +* `mesh_ios`, `mesh_android` +* `confirmed_ios`, `confirmed_android` +* `cloudscraper` + +!!! note + New profiles are added frequently. If an identifier is not listed here but exists in the latest `tls-client` release, it will likely work. * Confirmed Android 2 (`confirmed_android_2`) #### OkHttp4 diff --git a/mkdocs.yml b/mkdocs.yml index a07540f..90c0492 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,6 +1,6 @@ -site_name: TLS Requests +site_name: Wrapper TLS Requests site_description: A powerful and lightweight Python library for making secure and reliable HTTP/TLS Fingerprint requests. -site_url: https://github.com/thewebscraping/ +site_url: https://github.com/thewebscraping/tls-requests/ theme: name: 'material' @@ -30,6 +30,7 @@ nav: - Authentication: 'advanced/authentication.md' - Hooks: 'advanced/hooks.md' - Proxies: 'advanced/proxies.md' + - Rotators: 'advanced/rotators.md' - TLS Client: - Install: 'tls/install.md' - Wrapper TLS Client: 'tls/index.md' @@ -42,5 +43,5 @@ markdown_extensions: css_class: highlight - mkautodoc -extra_css: - - css/custom.css +# extra_css: # custom CSS removed because file is missing +# - css/custom.css diff --git a/pyproject.toml b/pyproject.toml index d2839f6..dccda17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,142 @@ [build-system] -requires = ['setuptools>=40.8.0'] -build-backend = 'setuptools.build_meta' +requires = ["hatchling", "uv-dynamic-versioning"] +build-backend = "hatchling.build" + +[project] +name = "wrapper-tls-requests" +dynamic = ["version"] +description = "A powerful and lightweight Python library for making secure and reliable HTTP/TLS fingerprint requests." +readme = "README.md" +requires-python = ">=3.8" +license = "MIT" +authors = [ + { name = "Tu Pham", email = "thetwofarm@gmail.com" }, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Environment :: Web Environment", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "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 :: Only", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Software Development :: Libraries", +] +dependencies = [ + "idna~=3.10", + "charset-normalizer", + "orjson", +] + +[dependency-groups] +dev = [ + "pytest", + "pytest-asyncio", + "pytest-httpserver", + "pytest-cov", + "werkzeug", + "tox", + "pre-commit", + "mkautodoc", + "ruff", + "mypy", + "mkdocs-material", + "mkdocs", + "mkdocstrings[python]", +] + +[project.urls] +Sponsor = "https://github.com/sponsors/thewebscraping" +Changelog = "https://github.com/thewebscraping/tls-requests/blob/main/CHANGELOG.md" +Documentation = "https://thewebscraping.github.io/tls-requests/" +Source = "https://github.com/thewebscraping/tls-requests" +Homepage = "https://github.com/thewebscraping/tls-requests" + +[tool.hatch.version] +source = "uv-dynamic-versioning" + +[tool.uv-dynamic-versioning] +fallback-version = "0.0.0" + +[tool.hatch.build.targets.sdist] +include = ["src/tls_requests"] + +[tool.hatch.build.targets.wheel] +packages = ["src/tls_requests"] [tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src"] asyncio_mode = "auto" + +[tool.mypy] +python_version = "3.9" +show_error_codes = true +ignore_missing_imports = true +warn_unused_configs = true +check_untyped_defs = true +disallow_untyped_defs = false + +[[tool.mypy.overrides]] +module = "tests.*" +ignore_errors = true + +[tool.ruff] +include = ["pyproject.toml", "src/**/*.py", "scripts/**/*.py"] +line-length = 120 +indent-width = 4 +target-version = "py311" + +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +[tool.ruff.lint] +select = ["E4", "E7", "E9", "F", "I"] +ignore = [] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401", "F403", "F405"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +[tool.ruff.lint.isort] +required-imports = ["from __future__ import annotations"] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 0c95c2a..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,24 +0,0 @@ --r requirements.txt - -# Documentation -mkdocs==1.6.1 -mkautodoc==0.2.0 -mkdocs-material==9.5.39 - -# Packaging -setuptools~=75.3.0 -twine~=6.0.1 - -# Tests & Linting -pre-commit -pytest-cov -Werkzeug -black==24.3.0 -coverage[toml]==7.6.1 -isort==5.13.2 -flake8==7.1.1 -mypy==1.11.2 -pytest==8.3.3 -pytest-asyncio==0.24.0 -pytest_httpserver==1.1.0 -tox==4.23.2 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 7a91425..0000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Base -chardet~=5.2.0 -requests~=2.32.3 -tqdm~=4.67.1 -idna~=3.10 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 4128f96..0000000 --- a/setup.cfg +++ /dev/null @@ -1,69 +0,0 @@ -[metadata] -long_description = file: README.md -long_description_content_type = text/markdown -license = MIT -license_file = LICENSE -python_requires = >=3.8 -install_requires = - chardet ~= 5.2.0 - requests ~= 2.32.3 - tqdm ~= 4.67.1 - idna ~= 3.10 -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - Environment :: Web Environment - License :: OSI Approved :: MIT License - Natural Language :: English - Operating System :: OS Independent - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.8 - 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 :: Only - Programming Language :: Python :: Implementation :: CPython - Programming Language :: Python :: Implementation :: PyPy - Topic :: Internet :: WWW/HTTP - Topic :: Software Development :: Libraries - -project_urls = - Changelog = https://github.com/thewebscraping/tls-requests/blob/main/CHANGELOG.md - Documentation = https://thewebscraping.github.io/tls-requests/ - Source = https://github.com/thewebscraping/tls-requests - Homepage = https://github.com/thewebscraping/tls-requests - -[options] -zip_safe = False -include_package_data = True -packages = find: - -[options.packages.find] -exclude = - examples* - tools* - docs* - -[flake8] -max-line-length = 120 -ignore = F821, E203, W503, E501, E231 -per-file-ignores = - __init__.py: F405, F403, E402, F401 - -[tool.black] -line-length = 120 -include = '\.pyi?$' -unstable = true - - -[tool.isort] -atomic = true -profile = "black" -line_length = 120 -skip_gitignore = true -skip_glob = ["tests/data", "profiling"] -known_first_party = ["black", "blib2to3", "blackd", "_black_version"] -add_imports = "from __future__ import annotations" diff --git a/setup.py b/setup.py deleted file mode 100644 index 90b0610..0000000 --- a/setup.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python - -import os -import re -import sys -from codecs import open - -from setuptools import setup - -BASE_DIR = os.path.dirname(__file__) -CURRENT_PYTHON = sys.version_info[:2] -REQUIRED_PYTHON = (3, 8) - -if CURRENT_PYTHON < REQUIRED_PYTHON: - sys.stderr.write( - """Python version not supported, you need to use Python version >= {}.{}""".format( - *REQUIRED_PYTHON - ) - ) - sys.exit(1) - - -def normalize(name) -> str: - name = re.sub(r"\s+", "-", name) - return re.sub(r"[-_.]+", "-", name).lower() - - -version = {} -with open(os.path.join(BASE_DIR, "tls_requests", "__version__.py"), "r", "utf-8") as f: - exec(f.read(), version) - -if __name__ == "__main__": - setup( - name=version["__title__"], - version=version["__version__"], - description=version["__description__"], - long_description_content_type="text/markdown", - author=version["__author__"], - author_email=version["__author_email__"], - url=version["__url__"], - ) diff --git a/tls_requests/__init__.py b/src/tls_requests/__init__.py similarity index 60% rename from tls_requests/__init__.py rename to src/tls_requests/__init__.py index 277d5b4..0e2c589 100644 --- a/tls_requests/__init__.py +++ b/src/tls_requests/__init__.py @@ -1,4 +1,19 @@ -from .__version__ import __description__, __title__, __version__ +from __future__ import annotations + +import importlib.metadata + +try: + __version__ = importlib.metadata.version("wrapper-tls-requests") +except importlib.metadata.PackageNotFoundError: + __version__ = "0.0.0" + +__title__ = "wrapper-tls-requests" +__description__ = ( + "A powerful and lightweight Python library for making secure and reliable HTTP/TLS fingerprint requests." +) +__author__ = "Tu Pham" +__license__ = "MIT" + from .api import * from .client import * from .exceptions import * @@ -33,9 +48,11 @@ "patch", "post", "put", - "request", ] +from .api import request + +__all__ += ["request"] __locals = locals() for __name in __all__: diff --git a/tls_requests/api.py b/src/tls_requests/api.py similarity index 64% rename from tls_requests/api.py rename to src/tls_requests/api.py index 11c625c..183ce14 100644 --- a/tls_requests/api.py +++ b/src/tls_requests/api.py @@ -1,16 +1,35 @@ from __future__ import annotations -import typing +from typing import Any, Optional from .client import Client from .models import Response -from .settings import (DEFAULT_FOLLOW_REDIRECTS, DEFAULT_TIMEOUT, - DEFAULT_TLS_HTTP2, DEFAULT_TLS_IDENTIFIER) -from .types import (AuthTypes, CookieTypes, HeaderTypes, MethodTypes, - ProtocolTypes, ProxyTypes, RequestData, RequestFiles, - TimeoutTypes, TLSIdentifierTypes, URLParamTypes, URLTypes) - -__all__ = [ +from .settings import ( + DEFAULT_ALLOW_HTTP, + DEFAULT_CLIENT_IDENTIFIER, + DEFAULT_DEBUG, + DEFAULT_FOLLOW_REDIRECTS, + DEFAULT_HTTP2, + DEFAULT_PROTOCOL_RACING, + DEFAULT_TIMEOUT, +) +from .types import ( + AuthTypes, + CookieTypes, + HeaderTypes, + IdentifierArgTypes, + IdentifierTypes, + MethodTypes, + ProtocolTypes, + ProxyTypes, + RequestData, + RequestFiles, + TimeoutTypes, + URLParamTypes, + URLTypes, +) + +__all__ = ( "delete", "get", "head", @@ -19,7 +38,7 @@ "post", "put", "request", -] +) def request( @@ -27,18 +46,22 @@ def request( url: URLTypes, *, params: URLParamTypes = None, - data: RequestData = None, - files: RequestFiles = None, - json: typing.Any = None, + data: Optional[RequestData] = None, + files: Optional[RequestFiles] = None, + json: Any = None, headers: HeaderTypes = None, cookies: CookieTypes = None, auth: AuthTypes = None, proxy: ProxyTypes = None, - http2: ProtocolTypes = DEFAULT_TLS_HTTP2, + http2: ProtocolTypes = DEFAULT_HTTP2, timeout: TimeoutTypes = DEFAULT_TIMEOUT, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, verify: bool = True, - tls_identifier: TLSIdentifierTypes = DEFAULT_TLS_IDENTIFIER, + client_identifier: IdentifierArgTypes = DEFAULT_CLIENT_IDENTIFIER, + debug: bool = DEFAULT_DEBUG, + protocol_racing: bool = DEFAULT_PROTOCOL_RACING, + allow_http: bool = DEFAULT_ALLOW_HTTP, + stream_id: Optional[int] = None, **config, ) -> Response: """ @@ -80,7 +103,11 @@ def request( http2=http2, timeout=timeout, verify=verify, - client_identifier=tls_identifier, + client_identifier=client_identifier, + debug=debug, + protocol_racing=protocol_racing, + allow_http=allow_http, + stream_id=stream_id, **config, ) as client: return client.request( @@ -94,6 +121,9 @@ def request( auth=auth, follow_redirects=follow_redirects, timeout=timeout, + protocol_racing=protocol_racing, + allow_http=allow_http, + stream_id=stream_id, ) @@ -105,11 +135,15 @@ def get( cookies: CookieTypes = None, auth: AuthTypes = None, proxy: ProxyTypes = None, - http2: ProtocolTypes = DEFAULT_TLS_HTTP2, + http2: ProtocolTypes = DEFAULT_HTTP2, timeout: TimeoutTypes = DEFAULT_TIMEOUT, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, verify: bool = True, - tls_identifier: TLSIdentifierTypes = DEFAULT_TLS_IDENTIFIER, + client_identifier: IdentifierTypes = DEFAULT_CLIENT_IDENTIFIER, + debug: bool = DEFAULT_DEBUG, + protocol_racing: bool = DEFAULT_PROTOCOL_RACING, + allow_http: bool = DEFAULT_ALLOW_HTTP, + stream_id: Optional[int] = None, **config, ) -> Response: """ @@ -132,7 +166,11 @@ def get( follow_redirects=follow_redirects, timeout=timeout, verify=verify, - tls_identifier=tls_identifier, + client_identifier=client_identifier, + debug=debug, + protocol_racing=protocol_racing, + allow_http=allow_http, + stream_id=stream_id, **config, ) @@ -145,11 +183,15 @@ def options( cookies: CookieTypes = None, auth: AuthTypes = None, proxy: ProxyTypes = None, - http2: ProtocolTypes = DEFAULT_TLS_HTTP2, + http2: ProtocolTypes = DEFAULT_HTTP2, timeout: TimeoutTypes = DEFAULT_TIMEOUT, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, verify: bool = True, - tls_identifier: TLSIdentifierTypes = DEFAULT_TLS_IDENTIFIER, + client_identifier: IdentifierTypes = DEFAULT_CLIENT_IDENTIFIER, + debug: bool = DEFAULT_DEBUG, + protocol_racing: bool = DEFAULT_PROTOCOL_RACING, + allow_http: bool = DEFAULT_ALLOW_HTTP, + stream_id: Optional[int] = None, **config, ) -> Response: """ @@ -172,7 +214,11 @@ def options( follow_redirects=follow_redirects, timeout=timeout, verify=verify, - tls_identifier=tls_identifier, + client_identifier=client_identifier, + debug=debug, + protocol_racing=protocol_racing, + allow_http=allow_http, + stream_id=stream_id, **config, ) @@ -185,11 +231,15 @@ def head( cookies: CookieTypes = None, auth: AuthTypes = None, proxy: ProxyTypes = None, - http2: ProtocolTypes = DEFAULT_TLS_HTTP2, + http2: ProtocolTypes = DEFAULT_HTTP2, timeout: TimeoutTypes = DEFAULT_TIMEOUT, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, verify: bool = True, - tls_identifier: TLSIdentifierTypes = DEFAULT_TLS_IDENTIFIER, + client_identifier: IdentifierTypes = DEFAULT_CLIENT_IDENTIFIER, + debug: bool = DEFAULT_DEBUG, + protocol_racing: bool = DEFAULT_PROTOCOL_RACING, + allow_http: bool = DEFAULT_ALLOW_HTTP, + stream_id: Optional[int] = None, **config, ) -> Response: """ @@ -212,7 +262,11 @@ def head( timeout=timeout, follow_redirects=follow_redirects, verify=verify, - tls_identifier=tls_identifier, + client_identifier=client_identifier, + debug=debug, + protocol_racing=protocol_racing, + allow_http=allow_http, + stream_id=stream_id, **config, ) @@ -220,19 +274,23 @@ def head( def post( url: URLTypes, *, - data: RequestData = None, - files: RequestFiles = None, - json: typing.Any = None, + data: Optional[RequestData] = None, + files: Optional[RequestFiles] = None, + json: Optional[Any] = None, params: URLParamTypes = None, headers: HeaderTypes = None, cookies: CookieTypes = None, auth: AuthTypes = None, proxy: ProxyTypes = None, - http2: ProtocolTypes = DEFAULT_TLS_HTTP2, + http2: ProtocolTypes = DEFAULT_HTTP2, timeout: TimeoutTypes = DEFAULT_TIMEOUT, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, verify: bool = True, - tls_identifier: TLSIdentifierTypes = DEFAULT_TLS_IDENTIFIER, + client_identifier: IdentifierTypes = DEFAULT_CLIENT_IDENTIFIER, + debug: bool = DEFAULT_DEBUG, + protocol_racing: bool = DEFAULT_PROTOCOL_RACING, + allow_http: bool = DEFAULT_ALLOW_HTTP, + stream_id: Optional[int] = None, **config, ) -> Response: """ @@ -255,7 +313,11 @@ def post( timeout=timeout, follow_redirects=follow_redirects, verify=verify, - tls_identifier=tls_identifier, + client_identifier=client_identifier, + debug=debug, + protocol_racing=protocol_racing, + allow_http=allow_http, + stream_id=stream_id, **config, ) @@ -263,19 +325,23 @@ def post( def put( url: URLTypes, *, - data: RequestData = None, - files: RequestFiles = None, - json: typing.Any = None, + data: Optional[RequestData] = None, + files: Optional[RequestFiles] = None, + json: Optional[Any] = None, params: URLParamTypes = None, headers: HeaderTypes = None, cookies: CookieTypes = None, auth: AuthTypes = None, proxy: ProxyTypes = None, - http2: ProtocolTypes = DEFAULT_TLS_HTTP2, + http2: ProtocolTypes = DEFAULT_HTTP2, timeout: TimeoutTypes = DEFAULT_TIMEOUT, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, verify: bool = True, - tls_identifier: TLSIdentifierTypes = DEFAULT_TLS_IDENTIFIER, + client_identifier: IdentifierTypes = DEFAULT_CLIENT_IDENTIFIER, + debug: bool = DEFAULT_DEBUG, + protocol_racing: bool = DEFAULT_PROTOCOL_RACING, + allow_http: bool = DEFAULT_ALLOW_HTTP, + stream_id: Optional[int] = None, **config, ) -> Response: """ @@ -298,7 +364,11 @@ def put( timeout=timeout, follow_redirects=follow_redirects, verify=verify, - tls_identifier=tls_identifier, + client_identifier=client_identifier, + debug=debug, + protocol_racing=protocol_racing, + allow_http=allow_http, + stream_id=stream_id, **config, ) @@ -306,19 +376,23 @@ def put( def patch( url: URLTypes, *, - data: RequestData = None, - files: RequestFiles = None, - json: typing.Any = None, + data: Optional[RequestData] = None, + files: Optional[RequestFiles] = None, + json: Optional[Any] = None, params: URLParamTypes = None, headers: HeaderTypes = None, cookies: CookieTypes = None, auth: AuthTypes = None, proxy: ProxyTypes = None, - http2: ProtocolTypes = DEFAULT_TLS_HTTP2, + http2: ProtocolTypes = DEFAULT_HTTP2, timeout: TimeoutTypes = DEFAULT_TIMEOUT, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, verify: bool = True, - tls_identifier: TLSIdentifierTypes = DEFAULT_TLS_IDENTIFIER, + client_identifier: IdentifierTypes = DEFAULT_CLIENT_IDENTIFIER, + debug: bool = DEFAULT_DEBUG, + protocol_racing: bool = DEFAULT_PROTOCOL_RACING, + allow_http: bool = DEFAULT_ALLOW_HTTP, + stream_id: Optional[int] = None, **config, ) -> Response: """ @@ -341,7 +415,11 @@ def patch( timeout=timeout, follow_redirects=follow_redirects, verify=verify, - tls_identifier=tls_identifier, + client_identifier=client_identifier, + debug=debug, + protocol_racing=protocol_racing, + allow_http=allow_http, + stream_id=stream_id, **config, ) @@ -349,16 +427,20 @@ def patch( def delete( url: URLTypes, *, - params: URLParamTypes | None = None, - headers: HeaderTypes | None = None, - cookies: CookieTypes | None = None, - auth: AuthTypes | None = None, + params: URLParamTypes = None, + headers: HeaderTypes = None, + cookies: CookieTypes = None, + auth: AuthTypes = None, proxy: ProxyTypes = None, - http2: ProtocolTypes = DEFAULT_TLS_HTTP2, + http2: ProtocolTypes = DEFAULT_HTTP2, timeout: TimeoutTypes = DEFAULT_TIMEOUT, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, verify: bool = True, - tls_identifier: TLSIdentifierTypes = DEFAULT_TLS_IDENTIFIER, + client_identifier: IdentifierTypes = DEFAULT_CLIENT_IDENTIFIER, + debug: bool = DEFAULT_DEBUG, + protocol_racing: bool = DEFAULT_PROTOCOL_RACING, + allow_http: bool = DEFAULT_ALLOW_HTTP, + stream_id: Optional[int] = None, **config, ) -> Response: """ @@ -381,6 +463,10 @@ def delete( timeout=timeout, follow_redirects=follow_redirects, verify=verify, - tls_identifier=tls_identifier, + client_identifier=client_identifier, + debug=debug, + protocol_racing=protocol_racing, + allow_http=allow_http, + stream_id=stream_id, **config, ) diff --git a/tls_requests/bin/__init__.py b/src/tls_requests/bin/__init__.py similarity index 100% rename from tls_requests/bin/__init__.py rename to src/tls_requests/bin/__init__.py diff --git a/tls_requests/client.py b/src/tls_requests/client.py similarity index 67% rename from tls_requests/client.py rename to src/tls_requests/client.py index 579b948..74444b6 100644 --- a/tls_requests/client.py +++ b/src/tls_requests/client.py @@ -2,24 +2,59 @@ import datetime import time -import typing import uuid from enum import Enum -from typing import (Any, Callable, Literal, Mapping, Optional, Sequence, - TypeVar, Union) +from typing import Any, Callable, List, Literal, Mapping, Optional, Sequence, TypeVar, Union from .exceptions import ProxyError, RemoteProtocolError, TooManyRedirects -from .models import (URL, Auth, BasicAuth, Cookies, Headers, Proxy, Request, - Response, StatusCodes, TLSClient, TLSConfig, URLParams) -from .settings import (DEFAULT_FOLLOW_REDIRECTS, DEFAULT_HEADERS, - DEFAULT_MAX_REDIRECTS, DEFAULT_TIMEOUT, - DEFAULT_TLS_HTTP2, DEFAULT_TLS_IDENTIFIER) -from .types import (AuthTypes, CookieTypes, HeaderTypes, HookTypes, - ProtocolTypes, ProxyTypes, RequestData, RequestFiles, - TimeoutTypes, TLSIdentifierTypes, URLParamTypes, URLTypes) +from .models import ( + URL, + Auth, + BasicAuth, + Cookies, + HeaderRotator, + Headers, + Proxy, + ProxyRotator, + Request, + Response, + StatusCodes, + TLSClient, + TLSConfig, + TLSIdentifierRotator, + URLParams, +) +from .settings import ( + DEFAULT_ALLOW_HTTP, + DEFAULT_CLIENT_IDENTIFIER, + DEFAULT_DEBUG, + DEFAULT_FOLLOW_REDIRECTS, + DEFAULT_HTTP2, + DEFAULT_MAX_REDIRECTS, + DEFAULT_PROTOCOL_RACING, + DEFAULT_TIMEOUT, +) +from .types import ( + AuthTypes, + CookieTypes, + HeaderTypes, + HookTypes, + IdentifierArgTypes, + ProtocolTypes, + ProxyTypes, + RequestData, + RequestFiles, + TimeoutTypes, + URLParamTypes, + URLTypes, +) from .utils import get_logger -__all__ = ["AsyncClient", "Client"] +__all__ = ( + "BaseClient", + "AsyncClient", + "Client", +) T = TypeVar("T", bound="Client") A = TypeVar("A", bound="AsyncClient") @@ -28,6 +63,9 @@ logger = get_logger("TLSRequests") +BC = TypeVar("BC", bound="BaseClient") + + class ProtocolType(str, Enum): AUTO = "auto" HTTP1 = "http1" @@ -82,36 +120,70 @@ class BaseClient: def __init__( self, *, - auth: AuthTypes = None, - params: URLParamTypes = None, - headers: HeaderTypes = None, - cookies: CookieTypes = None, - proxy: ProxyTypes = None, + auth: Optional[AuthTypes] = None, + params: Optional[URLParamTypes] = None, + headers: Optional[HeaderTypes] = None, + cookies: Optional[CookieTypes] = None, + proxy: Optional[ProxyTypes] = None, timeout: TimeoutTypes = DEFAULT_TIMEOUT, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, max_redirects: int = DEFAULT_MAX_REDIRECTS, - http2: ProtocolTypes = DEFAULT_TLS_HTTP2, + http2: ProtocolTypes = DEFAULT_HTTP2, verify: bool = True, - client_identifier: Optional[TLSIdentifierTypes] = DEFAULT_TLS_IDENTIFIER, - hooks: HookTypes = None, + client_identifier: Optional[IdentifierArgTypes] = DEFAULT_CLIENT_IDENTIFIER, + debug: bool = DEFAULT_DEBUG, + protocol_racing: bool = DEFAULT_PROTOCOL_RACING, + allow_http: bool = DEFAULT_ALLOW_HTTP, + stream_id: Optional[int] = None, + hooks: Optional[HookTypes] = None, encoding: str = "utf-8", **config, ) -> None: + if "tls_identifier" in config: + logger.warning( + "The 'client_identifier' parameter is deprecated and will be removed in version 1.3.0. " + "Please use 'tls_identifier' instead." + ) + client_identifier = config.pop("tls_identifier") + self._session = TLSClient.initialize() - self._config = TLSConfig.from_kwargs(**config) + self._config = TLSConfig.from_kwargs( + http2=http2, + verify=verify, + client_identifier=self.prepare_client_identifier(client_identifier), + debug=debug, + protocol_racing=protocol_racing, + allow_http=allow_http, + stream_id=stream_id, + **config, + ) self._params = URLParams(params) self._cookies = Cookies(cookies) self._state = ClientState.UNOPENED - self._headers = Headers(headers) + self._header_rotator: Optional[HeaderRotator] = None + self._headers: Headers = Headers() + if isinstance(headers, HeaderRotator): + self._header_rotator = headers + elif isinstance(headers, list): + self._header_rotator = HeaderRotator.from_file(headers) # type: ignore + elif headers is not None: + self._headers = Headers(headers) self._hooks = hooks if isinstance(hooks, dict) else {} self.auth = auth - self.proxy = self.prepare_proxy(proxy) + self.proxy = ProxyRotator.from_file(proxy) if isinstance(proxy, list) else proxy # type: ignore self.timeout = timeout self.follow_redirects = follow_redirects self.max_redirects = max_redirects self.http2 = http2 self.verify = verify - self.client_identifier = client_identifier + self.protocol_racing = protocol_racing + self.allow_http = allow_http + self.stream_id = stream_id + self.client_identifier: Optional[IdentifierArgTypes] = ( + TLSIdentifierRotator.from_file(client_identifier) # type: ignore + if isinstance(client_identifier, list) + else client_identifier + ) self.encoding = encoding @property @@ -128,14 +200,16 @@ def closed(self) -> bool: @property def headers(self) -> Headers: - for k, v in DEFAULT_HEADERS.items(): - if k not in self._headers: - self._headers[k] = v return self._headers @headers.setter def headers(self, headers: HeaderTypes) -> None: - self._headers = Headers(headers) + if isinstance(headers, HeaderRotator): + self._header_rotator = headers # type: ignore[assignment] + elif isinstance(headers, list): + self._header_rotator = HeaderRotator.from_file(headers) + elif headers is not None: + self._headers = Headers(headers) @property def cookies(self) -> Cookies: @@ -161,9 +235,7 @@ def hooks(self) -> Mapping[Literal["request", "response"], list[Callable]]: def hooks(self, hooks: HookTypes) -> None: self._hooks = self._rebuild_hooks(hooks) - def prepare_auth( - self, request: Request, auth: AuthTypes, *args, **kwargs - ) -> Union[Request, Any]: + def prepare_auth(self, request: Request, auth: AuthTypes, *args, **kwargs) -> Union[Request, Any]: """Build Auth Request instance""" if isinstance(auth, tuple) and len(auth) == 2: @@ -176,32 +248,54 @@ def prepare_auth( if isinstance(auth, Auth): return auth.build_auth(request) - def prepare_headers(self, headers: HeaderTypes = None) -> Headers: - """Prepare Headers""" + return auth - merged_headers = self.headers.copy() - return merged_headers.update(headers) + def prepare_headers(self, headers: HeaderTypes = None, user_agent: Optional[str] = None) -> Headers: + """Prepare Headers. Gets base headers from rotator if available.""" + if headers is None: + if self._header_rotator is not None: + return self._header_rotator.next(user_agent=user_agent) + return self.headers.copy() + if isinstance(headers, HeaderRotator): + return headers.next(user_agent=user_agent) + return Headers(headers) def prepare_cookies(self, cookies: CookieTypes = None) -> Cookies: """Prepare Cookies""" merged_cookies = self.cookies.copy() - return merged_cookies.update(cookies) + merged_cookies.update(cookies) + return merged_cookies def prepare_params(self, params: URLParamTypes = None) -> URLParams: """Prepare URL Params""" merged_params = self.params.copy() - return merged_params.update(params) - - def prepare_proxy(self, proxy: ProxyTypes = None) -> Optional[Proxy]: - if proxy is not None: - if isinstance(proxy, (bytes, str, URL, Proxy)): - return Proxy(proxy) - - raise ProxyError("Invalid proxy.") - - def prepare_config(self, request: Request): + merged_params.update(params) + return merged_params + + def prepare_proxy(self, proxy: Optional[ProxyTypes]) -> Optional[Proxy]: + if proxy is None: + return None + if isinstance(proxy, ProxyRotator): + res = proxy.next() + return Proxy(res) if isinstance(res, (str, bytes)) else res + if isinstance(proxy, (str, bytes)): + return Proxy(proxy) + if isinstance(proxy, Proxy): + return proxy + if isinstance(proxy, URL): + return Proxy(str(proxy)) + raise ProxyError(f"Unsupported proxy type: {type(proxy)}") + + def prepare_client_identifier(self, identifier: Optional[IdentifierArgTypes]) -> str: + if isinstance(identifier, str): + return identifier + if isinstance(identifier, TLSIdentifierRotator): + return str(identifier.next()) + return str(DEFAULT_CLIENT_IDENTIFIER) + + def prepare_config(self, request: Request, client_identifier: str = DEFAULT_CLIENT_IDENTIFIER): """Prepare TLS Config""" config = self.config.copy_with( @@ -214,7 +308,11 @@ def prepare_config(self, request: Request): timeout=request.timeout, http2=True if self.http2 in ["auto", "http2", True, None] else False, verify=self.verify, - tls_identifier=self.client_identifier, + client_identifier=client_identifier, + protocol_racing=request.protocol_racing, + allow_http=request.allow_http, + stream_id=request.stream_id, + **getattr(request, "_extra_config", {}), ) # Set Request SessionId. @@ -226,13 +324,17 @@ def build_request( method: str, url: URLTypes, *, - data: RequestData = None, - files: RequestFiles = None, - json: typing.Any = None, + data: Optional[RequestData] = None, + files: Optional[RequestFiles] = None, + json: Optional[Any] = None, params: URLParamTypes = None, headers: HeaderTypes = None, cookies: CookieTypes = None, timeout: TimeoutTypes = None, + protocol_racing: Optional[bool] = None, + allow_http: Optional[bool] = None, + stream_id: Optional[int] = None, + **kwargs, ) -> Request: """Build Request instance""" @@ -245,27 +347,29 @@ def build_request( params=self.prepare_params(params), headers=self.prepare_headers(headers), cookies=self.prepare_cookies(cookies), - proxy=self.proxy, + proxy=self.prepare_proxy(self.proxy), timeout=timeout or self.timeout, + protocol_racing=protocol_racing if protocol_racing is not None else self.protocol_racing, + allow_http=allow_http if allow_http is not None else self.allow_http, + stream_id=stream_id if stream_id is not None else self.stream_id, + **kwargs, ) - def build_hook_request( - self, request: Request, *args, **kwargs - ) -> Union[Request, Any]: + def build_hook_request(self, request: Request, *args, **kwargs) -> Union[Request, Any]: request_hooks = self._rebuild_hooks(self.hooks).get("request") if isinstance(request_hooks, Sequence): for hook in request_hooks: if callable(hook): return hook(request) + return None - def build_hook_response( - self, response: Response, *args, **kwargs - ) -> Union[Response, Any]: + def build_hook_response(self, response: Response, *args, **kwargs) -> Union[Response, Any]: request_hooks = self._rebuild_hooks(self.hooks).get("response") if isinstance(request_hooks, Sequence): for hook in request_hooks: if callable(hook): return hook(response) + return None def _rebuild_hooks(self, hooks: HookTypes): if isinstance(hooks, dict): @@ -274,10 +378,9 @@ def _rebuild_hooks(self, hooks: HookTypes): for k, items in hooks.items() if str(k) in ["request", "response"] and isinstance(items, Sequence) } + return None - def _rebuild_redirect_request( - self, request: Request, response: Response - ) -> Request: + def _rebuild_redirect_request(self, request: Request, response: Response) -> Request: """Rebuild Redirect Request""" return Request( @@ -308,7 +411,9 @@ def _rebuild_redirect_url(self, request: Request, response: Response) -> URL: try: url = URL(response.headers["Location"]) except KeyError: - raise RemoteProtocolError("Invalid URL in Location headers: %s" % e) + raise RemoteProtocolError("Redirect response without 'Location' header.") + except Exception as e: + raise RemoteProtocolError("Invalid URL in 'Location' header: %s" % e) from e if not url.netloc: for missing_field in ["scheme", "host", "port"]: @@ -324,20 +429,27 @@ def _rebuild_redirect_url(self, request: Request, response: Response) -> URL: self.config.sessionId = str(uuid.uuid4()) else: raise RemoteProtocolError( - "Switching remote scheme from HTTP/2 to HTTP/1 is not supported. Please initialize Client with parameter `http2` to `auto`." + "Switching remote scheme from HTTP/2 to HTTP/1 is not supported. Please initialize Client with" + " parameter `http2` to `auto`." ) setattr(url, "_url", None) # reset url if not url.url: - raise RemoteProtocolError("Invalid URL in Location headers: %s" % e) + raise RemoteProtocolError("Invalid URL in Location headers.") return url def _send( - self, request: Request, *, history: list = None, start: float = None + self, + request: Request, + *, + history: Optional[List[Response]] = None, + start: Optional[float] = None, ) -> Response: + history = [] if history is None else history start = start or time.perf_counter() - config = self.prepare_config(request) + client_identifier = self.prepare_client_identifier(self.client_identifier) + config = self.prepare_config(request, client_identifier=client_identifier) response = Response.from_tls_response( self.session.request(config.to_dict()), is_byte_response=config.isByteResponse, @@ -365,21 +477,17 @@ def close(self) -> None: self.session.destroy_session(self.config.sessionId) self._state = ClientState.CLOSED - def __enter__(self: T) -> T: + def __enter__(self: BC) -> BC: if self._state == ClientState.OPENED: - raise RuntimeError( - "It is not possible to open a client instance more than once." - ) + raise RuntimeError("It is not possible to open a client instance more than once.") if self._state == ClientState.CLOSED: - raise RuntimeError( - "The client instance cannot be reopened after it has been closed." - ) + raise RuntimeError("The client instance cannot be reopened after it has been closed.") self._state = ClientState.OPENED return self - def __exit__(self, *args, **kwargs) -> None: + def __exit__(self, *args: Any, **kwargs: Any) -> None: self.close() @@ -409,16 +517,20 @@ def request( method: str, url: URLTypes, *, - data: RequestData = None, - files: RequestFiles = None, - json: typing.Any = None, + data: Optional[RequestData] = None, + files: Optional[RequestFiles] = None, + json: Optional[Any] = None, params: URLParamTypes = None, headers: HeaderTypes = None, cookies: CookieTypes = None, auth: AuthTypes = None, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, timeout: TimeoutTypes = DEFAULT_TIMEOUT, - ): + protocol_racing: Optional[bool] = None, + allow_http: Optional[bool] = None, + stream_id: Optional[int] = None, + **kwargs, + ) -> Response: """ Constructs and sends an HTTP request. @@ -459,6 +571,10 @@ def request( headers=headers, cookies=cookies, timeout=timeout, + protocol_racing=protocol_racing, + allow_http=allow_http, + stream_id=stream_id, + **kwargs, ) return self.send(request, auth=auth, follow_redirects=follow_redirects) @@ -473,14 +589,25 @@ def send( raise RuntimeError("Cannot send a request, as the client has been closed.") self._state = ClientState.OPENED - for fn in [self.prepare_auth, self.build_hook_request]: - request_ = fn(request, auth or self.auth, follow_redirects) - if isinstance(request_, Request): - request = request_ + auth_request = self.prepare_auth(request, auth or self.auth) + if isinstance(auth_request, Request): + request = auth_request + + hook_request = self.build_hook_request(request) + if isinstance(hook_request, Request): + request = hook_request self.follow_redirects = follow_redirects response = self._send(request, start=time.perf_counter(), history=[]) + if isinstance(self.proxy, ProxyRotator) and response.request.proxy: + proxy_success = 200 <= response.status_code < 500 and response.status_code not in [407] + self.proxy.mark_result( + proxy=response.request.proxy, + success=proxy_success, + latency=response.elapsed.total_seconds(), + ) + if self.hooks.get("response"): response_ = self.build_hook_response(response) if isinstance(response_, Response): @@ -501,6 +628,7 @@ def get( auth: AuthTypes = None, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, timeout: TimeoutTypes = DEFAULT_TIMEOUT, + **kwargs, ): """ Send a `GET` request. @@ -516,6 +644,7 @@ def get( auth=auth, follow_redirects=follow_redirects, timeout=timeout, + **kwargs, ) def options( @@ -528,6 +657,7 @@ def options( auth: AuthTypes = None, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, timeout: TimeoutTypes = DEFAULT_TIMEOUT, + **kwargs, ) -> Response: """ Send an `OPTIONS` request. @@ -543,6 +673,7 @@ def options( auth=auth, follow_redirects=follow_redirects, timeout=timeout, + **kwargs, ) def head( @@ -555,6 +686,7 @@ def head( auth: AuthTypes = None, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, timeout: TimeoutTypes = DEFAULT_TIMEOUT, + **kwargs, ) -> Response: """ Send a `HEAD` request. @@ -570,21 +702,23 @@ def head( auth=auth, follow_redirects=follow_redirects, timeout=timeout, + **kwargs, ) def post( self, url: URLTypes, *, - data: RequestData = None, - files: RequestFiles = None, - json: typing.Any = None, + data: Optional[RequestData] = None, + files: Optional[RequestFiles] = None, + json: Optional[Any] = None, params: URLParamTypes = None, headers: HeaderTypes = None, cookies: CookieTypes = None, auth: AuthTypes = None, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, timeout: TimeoutTypes = DEFAULT_TIMEOUT, + **kwargs, ) -> Response: """ Send a `POST` request. @@ -603,21 +737,23 @@ def post( auth=auth, follow_redirects=follow_redirects, timeout=timeout, + **kwargs, ) def put( self, url: URLTypes, *, - data: RequestData = None, - files: RequestFiles = None, - json: typing.Any = None, + data: Optional[RequestData] = None, + files: Optional[RequestFiles] = None, + json: Optional[Any] = None, params: URLParamTypes = None, headers: HeaderTypes = None, cookies: CookieTypes = None, auth: AuthTypes = None, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, timeout: TimeoutTypes = DEFAULT_TIMEOUT, + **kwargs, ) -> Response: """ Send a `PUT` request. @@ -636,21 +772,23 @@ def put( auth=auth, follow_redirects=follow_redirects, timeout=timeout, + **kwargs, ) def patch( self, url: URLTypes, *, - data: RequestData = None, - files: RequestFiles = None, - json: typing.Any = None, + data: Optional[RequestData] = None, + files: Optional[RequestFiles] = None, + json: Optional[Any] = None, params: URLParamTypes = None, headers: HeaderTypes = None, cookies: CookieTypes = None, auth: AuthTypes = None, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, timeout: TimeoutTypes = DEFAULT_TIMEOUT, + **kwargs, ) -> Response: """ Send a `PATCH` request. @@ -669,6 +807,7 @@ def patch( auth=auth, follow_redirects=follow_redirects, timeout=timeout, + **kwargs, ) def delete( @@ -681,6 +820,7 @@ def delete( auth: AuthTypes = None, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, timeout: TimeoutTypes = DEFAULT_TIMEOUT, + **kwargs, ) -> Response: """ Send a `DELETE` request. @@ -696,6 +836,7 @@ def delete( auth=auth, follow_redirects=follow_redirects, timeout=timeout, + **kwargs, ) @@ -721,24 +862,94 @@ class AsyncClient(BaseClient): **Parameters:** See `tls_requests.BaseClient`. """ + async def aprepare_headers(self, headers: HeaderTypes = None, user_agent: Optional[str] = None) -> Headers: + """Prepare Headers. Gets base headers from rotator if available.""" + if headers is None: + if self._header_rotator is not None: + return await self._header_rotator.anext(user_agent=user_agent) + return self.headers.copy() + if isinstance(headers, HeaderRotator): + return await headers.anext(user_agent=user_agent) + return Headers(headers) + + async def aprepare_proxy(self, proxy: Optional[ProxyTypes]) -> Optional[Proxy]: + if proxy is None: + return None + if isinstance(proxy, ProxyRotator): + return await proxy.anext() + if isinstance(proxy, (str, bytes)): + return Proxy(proxy) + if isinstance(proxy, Proxy): + return proxy + if isinstance(proxy, URL): + return Proxy(str(proxy)) + raise ProxyError(f"Unsupported proxy type: {type(proxy)}") + + async def aprepare_client_identifier(self, identifier) -> str: + if isinstance(identifier, str): + return identifier + if isinstance(identifier, TLSIdentifierRotator): + return await identifier.anext() + return DEFAULT_CLIENT_IDENTIFIER + + async def abuild_request( + self, + method: str, + url: URLTypes, + *, + data: Optional[RequestData] = None, + files: Optional[RequestFiles] = None, + json: Optional[Any] = None, + params: URLParamTypes = None, + headers: HeaderTypes = None, + cookies: CookieTypes = None, + timeout: TimeoutTypes = None, + protocol_racing: Optional[bool] = None, + allow_http: Optional[bool] = None, + stream_id: Optional[int] = None, + **kwargs, + ) -> Request: + headers = await self.aprepare_headers(headers) + proxy = await self.aprepare_proxy(self.proxy) + return Request( + method, + url, + data=data, + files=files, + json=json, + params=self.prepare_params(params), + headers=headers, + cookies=self.prepare_cookies(cookies), + proxy=proxy, + timeout=timeout or self.timeout, + protocol_racing=protocol_racing if protocol_racing is not None else self.protocol_racing, + allow_http=allow_http if allow_http is not None else self.allow_http, + stream_id=stream_id if stream_id is not None else self.stream_id, + **kwargs, + ) + async def request( self, method: str, url: URLTypes, *, - data: RequestData = None, - files: RequestFiles = None, - json: typing.Any = None, + data: Optional[RequestData] = None, + files: Optional[RequestFiles] = None, + json: Optional[Any] = None, params: URLParamTypes = None, headers: HeaderTypes = None, cookies: CookieTypes = None, auth: AuthTypes = None, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, timeout: TimeoutTypes = DEFAULT_TIMEOUT, + protocol_racing: Optional[bool] = None, + allow_http: Optional[bool] = None, + stream_id: Optional[int] = None, + **kwargs, ) -> Response: """Async Request""" - request = self.build_request( + request = await self.abuild_request( method=method, url=url, data=data, @@ -748,6 +959,10 @@ async def request( headers=headers, cookies=cookies, timeout=timeout, + protocol_racing=protocol_racing, + allow_http=allow_http, + stream_id=stream_id, + **kwargs, ) return await self.send(request, auth=auth, follow_redirects=follow_redirects) @@ -761,6 +976,7 @@ async def get( auth: AuthTypes = None, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, timeout: TimeoutTypes = DEFAULT_TIMEOUT, + **kwargs, ) -> Response: """ Send a `GET` request. @@ -776,6 +992,7 @@ async def get( auth=auth, follow_redirects=follow_redirects, timeout=timeout, + **kwargs, ) async def options( @@ -788,6 +1005,7 @@ async def options( auth: AuthTypes = None, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, timeout: TimeoutTypes = DEFAULT_TIMEOUT, + **kwargs, ) -> Response: """ Send an `OPTIONS` request. @@ -803,6 +1021,7 @@ async def options( auth=auth, follow_redirects=follow_redirects, timeout=timeout, + **kwargs, ) async def head( @@ -815,6 +1034,7 @@ async def head( auth: AuthTypes = None, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, timeout: TimeoutTypes = DEFAULT_TIMEOUT, + **kwargs, ) -> Response: """ Send a `HEAD` request. @@ -830,21 +1050,23 @@ async def head( auth=auth, follow_redirects=follow_redirects, timeout=timeout, + **kwargs, ) async def post( self, url: URLTypes, *, - data: RequestData = None, - files: RequestFiles = None, - json: typing.Any = None, + data: Optional[RequestData] = None, + files: Optional[RequestFiles] = None, + json: Optional[Any] = None, params: URLParamTypes = None, headers: HeaderTypes = None, cookies: CookieTypes = None, auth: AuthTypes = None, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, timeout: TimeoutTypes = DEFAULT_TIMEOUT, + **kwargs, ) -> Response: """ Send a `POST` request. @@ -863,21 +1085,23 @@ async def post( auth=auth, follow_redirects=follow_redirects, timeout=timeout, + **kwargs, ) async def put( self, url: URLTypes, *, - data: RequestData = None, - files: RequestFiles = None, - json: typing.Any = None, + data: Optional[RequestData] = None, + files: Optional[RequestFiles] = None, + json: Optional[Any] = None, params: URLParamTypes = None, headers: HeaderTypes = None, cookies: CookieTypes = None, auth: AuthTypes = None, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, timeout: TimeoutTypes = DEFAULT_TIMEOUT, + **kwargs, ) -> Response: """ Send a `PUT` request. @@ -896,21 +1120,23 @@ async def put( auth=auth, follow_redirects=follow_redirects, timeout=timeout, + **kwargs, ) async def patch( self, url: URLTypes, *, - data: RequestData = None, - files: RequestFiles = None, - json: typing.Any = None, + data: Optional[RequestData] = None, + files: Optional[RequestFiles] = None, + json: Optional[Any] = None, params: URLParamTypes = None, headers: HeaderTypes = None, cookies: CookieTypes = None, auth: AuthTypes = None, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, timeout: TimeoutTypes = DEFAULT_TIMEOUT, + **kwargs, ) -> Response: """ Send a `PATCH` request. @@ -929,6 +1155,7 @@ async def patch( auth=auth, follow_redirects=follow_redirects, timeout=timeout, + **kwargs, ) async def delete( @@ -941,6 +1168,7 @@ async def delete( auth: AuthTypes = None, follow_redirects: bool = DEFAULT_FOLLOW_REDIRECTS, timeout: TimeoutTypes = DEFAULT_TIMEOUT, + **kwargs, ) -> Response: """ Send a `DELETE` request. @@ -956,6 +1184,7 @@ async def delete( auth=auth, follow_redirects=follow_redirects, timeout=timeout, + **kwargs, ) async def send( @@ -970,14 +1199,25 @@ async def send( raise RuntimeError("Cannot send a request, as the client has been closed.") self._state = ClientState.OPENED - for fn in [self.prepare_auth, self.build_hook_request]: - request_ = fn(request, auth or self.auth, follow_redirects) - if isinstance(request_, Request): - request = request_ + auth_request = self.prepare_auth(request, auth or self.auth) + if isinstance(auth_request, Request): + request = auth_request + + hook_request = self.build_hook_request(request) + if isinstance(hook_request, Request): + request = hook_request self.follow_redirects = follow_redirects response = await self._send(request, start=time.perf_counter(), history=[]) + if isinstance(self.proxy, ProxyRotator) and response.request.proxy: + proxy_success = 200 <= response.status_code < 500 and response.status_code not in [407] + await self.proxy.amark_result( + proxy=response.request.proxy, + success=proxy_success, + latency=response.elapsed.total_seconds(), + ) + if self.hooks.get("response"): response_ = self.build_hook_response(response) if isinstance(response_, Response): @@ -988,11 +1228,17 @@ async def send( await response.aclose() return response - async def _send( - self, request: Request, *, history: list = None, start: float = None + async def _send( # type: ignore[override] + self, + request: Request, + *, + history: Optional[List[Response]] = None, + start: Optional[float] = None, ) -> Response: + history = [] if history is None else history start = start or time.perf_counter() - config = self.prepare_config(request) + client_identifier = await self.aprepare_client_identifier(self.client_identifier) + config = self.prepare_config(request, client_identifier=client_identifier) response = Response.from_tls_response( await self.session.arequest(config.to_dict()), is_byte_response=config.isByteResponse, @@ -1019,14 +1265,10 @@ async def aclose(self) -> None: async def __aenter__(self: A) -> A: if self._state == ClientState.OPENED: - raise RuntimeError( - "It is not possible to open a client instance more than once." - ) + raise RuntimeError("It is not possible to open a client instance more than once.") if self._state == ClientState.CLOSED: - raise RuntimeError( - "The client instance cannot be reopened after it has been closed." - ) + raise RuntimeError("The client instance cannot be reopened after it has been closed.") self._state = ClientState.OPENED return self diff --git a/tls_requests/exceptions.py b/src/tls_requests/exceptions.py similarity index 62% rename from tls_requests/exceptions.py rename to src/tls_requests/exceptions.py index 8402a91..50cae40 100644 --- a/tls_requests/exceptions.py +++ b/src/tls_requests/exceptions.py @@ -5,24 +5,41 @@ if TYPE_CHECKING: pass -__all__ = [ +__all__ = ( + "AuthenticationError", "CookieConflictError", "HTTPError", - "URLError", - "RemoteProtocolError", + "HeaderError", "ProtocolError", + "RemoteProtocolError", "StreamConsumed", "StreamError", - "TooManyRedirects", "TLSError", -] + "RotatorError", + "TooManyRedirects", + "URLError", +) class HTTPError(Exception): """HTTP Error""" - def __init__(self, message: str) -> None: + def __init__(self, message: str, **kwargs) -> None: self.message = message + response = kwargs.pop("response", None) + self.response = response + self.request = kwargs.pop("request", None) + if response is not None and not self.request and hasattr(response, "request"): + self.request = self.response.request + super().__init__(message) + + +class AuthenticationError(HTTPError): + """Authentication Error""" + + +class HeaderError(HTTPError): + """Header Error""" class ProtocolError(HTTPError): @@ -75,3 +92,7 @@ class StreamConsumed(StreamError): class StreamClosed(StreamError): pass + + +class RotatorError(HTTPError): + pass diff --git a/src/tls_requests/models/__init__.py b/src/tls_requests/models/__init__.py new file mode 100644 index 0000000..31848a0 --- /dev/null +++ b/src/tls_requests/models/__init__.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from .auth import Auth, BasicAuth +from .cookies import Cookies +from .encoders import JsonEncoder, MultipartEncoder, StreamEncoder, UrlencodedEncoder +from .headers import Headers +from .libraries import TLSLibrary +from .request import Request +from .response import Response +from .rotators import BaseRotator, HeaderRotator, ProxyRotator, TLSIdentifierRotator +from .status_codes import StatusCodes +from .tls import CustomTLSClientConfig, TLSClient, TLSConfig, TLSResponse +from .urls import URL, Proxy, URLParams + +__all__ = [ + "Auth", + "BasicAuth", + "Cookies", + "JsonEncoder", + "MultipartEncoder", + "StreamEncoder", + "UrlencodedEncoder", + "Headers", + "TLSLibrary", + "Request", + "Response", + "BaseRotator", + "HeaderRotator", + "ProxyRotator", + "TLSIdentifierRotator", + "StatusCodes", + "CustomTLSClientConfig", + "TLSClient", + "TLSConfig", + "TLSResponse", + "URL", + "Proxy", + "URLParams", +] diff --git a/tls_requests/models/auth.py b/src/tls_requests/models/auth.py similarity index 62% rename from tls_requests/models/auth.py rename to src/tls_requests/models/auth.py index 63089e1..9c6205d 100644 --- a/tls_requests/models/auth.py +++ b/src/tls_requests/models/auth.py @@ -1,7 +1,15 @@ +from __future__ import annotations + from base64 import b64encode from typing import Any, Union -from tls_requests.models.request import Request +from ..exceptions import AuthenticationError +from .request import Request + +__all__ = ( + "Auth", + "BasicAuth", +) class Auth: @@ -22,16 +30,14 @@ def build_auth(self, request: Request): return self._build_auth_headers(request) def _build_auth_headers(self, request: Request): - auth_token = b64encode( - b":".join([self._encode(self.username), self._encode(self.password)]) - ).decode() + auth_token = b64encode(b":".join([self._encode(self.username), self._encode(self.password)])).decode() request.headers["Authorization"] = "Basic %s" % auth_token def _encode(self, value: Union[str, bytes]) -> bytes: - if isinstance(self.username, str): - value = value.encode("latin1") + if isinstance(value, str): + return value.encode("latin1") if not isinstance(value, bytes): - raise TypeError("`username` or `password` parameter must be str or byte.") + raise AuthenticationError("`username` or `password` parameter must be str or byte.") return value diff --git a/tls_requests/models/cookies.py b/src/tls_requests/models/cookies.py similarity index 86% rename from tls_requests/models/cookies.py rename to src/tls_requests/models/cookies.py index 5a9b7a0..6aaa672 100644 --- a/tls_requests/models/cookies.py +++ b/src/tls_requests/models/cookies.py @@ -2,24 +2,25 @@ from __future__ import annotations +import calendar import copy -from abc import ABC +import time from email.message import Message from http import cookiejar as cookielib from http.cookiejar import Cookie from http.cookies import Morsel -from typing import TYPE_CHECKING, Iterator, MutableMapping, Optional +from typing import TYPE_CHECKING, Any, Dict, Iterator, MutableMapping, Optional from urllib.parse import urlparse, urlunparse -from tls_requests.exceptions import CookieConflictError -from tls_requests.types import CookieTypes +from ..exceptions import CookieConflictError, CookieError +from ..types import CookieTypes if TYPE_CHECKING: from .request import Request from .response import Response -__all__ = ["Cookies"] +__all__ = ("Cookies",) class MockResponse: @@ -34,9 +35,8 @@ def info(self): class MockRequest: - def __init__(self, request: Request): - self._new_headers = {} + self._new_headers: Dict[str, str] = {} self._request = request self._headers = request.headers self.type = urlparse(str(request.url)).scheme @@ -58,7 +58,7 @@ def get_full_url(self): # If they did set it, retrieve it and reconstruct the expected domain host = self._headers["Host"] - parsed = urlparse(self.request_url) + parsed = urlparse(str(self._request.url)) # Reconstruct the URL as we expect it return urlunparse( [ @@ -137,10 +137,8 @@ def set(self, name, value, **kwargs): """ # support client code that unsets cookies by assignment of a None value: if value is None: - remove_cookie_by_name( - self, name, domain=kwargs.get("domain"), path=kwargs.get("path") - ) - return + remove_cookie_by_name(self, name, domain=kwargs.get("domain"), path=kwargs.get("path")) + return None if isinstance(value, Morsel): c = morsel_to_cookie(value) @@ -239,9 +237,7 @@ def get_dict(self, domain=None, path=None): """ dictionary = {} for cookie in iter(self): - if (domain is None or cookie.domain == domain) and ( - path is None or cookie.path == path - ): + if (domain is None or cookie.domain == domain) and (path is None or cookie.path == path): dictionary[cookie.name] = cookie.value return dictionary @@ -274,21 +270,20 @@ def __delitem__(self, name): remove_cookie_by_name(self, name) def set_cookie(self, cookie, *args, **kwargs): - if ( - hasattr(cookie.value, "startswith") - and cookie.value.startswith('"') - and cookie.value.endswith('"') - ): + if hasattr(cookie.value, "startswith") and cookie.value.startswith('"') and cookie.value.endswith('"'): cookie.value = cookie.value.replace('\\"', "") - return super().set_cookie(cookie, *args, **kwargs) + return super().set_cookie(cookie, *args, **kwargs) # type: ignore - def update(self, other): # noqa + def update(self, other: Any = None, **kwargs: Any) -> None: # type: ignore[override] """Updates this jar with cookies from another CookieJar or dict-like""" - if isinstance(other, cookielib.CookieJar): - for cookie in other: - self.set_cookie(copy.copy(cookie)) - else: - super().update(other) + if other is not None: + if isinstance(other, cookielib.CookieJar): + for cookie in other: + self.set_cookie(copy.copy(cookie)) + else: + super().update(other) + if kwargs: + super().update(**kwargs) def _find(self, name, domain=None, path=None): """Requests uses this method internally to get cookie values. @@ -329,9 +324,7 @@ def _find_no_duplicates(self, name, domain=None, path=None): if path is None or cookie.path == path: if toReturn is not None: # if there are multiple cookies that meet passed in criteria - raise CookieConflictError( - f"There are multiple cookies with name, {name!r}" - ) + raise CookieConflictError(f"There are multiple cookies with name, {name!r}") # we will eventually return this as long as no cookie conflict toReturn = cookie.value @@ -350,6 +343,8 @@ def __setstate__(self, state): """Unlike a normal CookieJar, this class is pickleable.""" self.__dict__.update(state) if "_cookies_lock" not in self.__dict__: + import threading + self._cookies_lock = threading.RLock() def copy(self): @@ -361,7 +356,7 @@ def copy(self): def get_policy(self): """Return the CookiePolicy instance used.""" - return self._policy + return getattr(self, "_policy", None) def extract_cookies_to_jar(cookiejar, response, request): @@ -446,9 +441,7 @@ def create_cookie(name, value, **kwargs): badargs = set(kwargs) - set(result) if badargs: - raise TypeError( - f"create_cookie() got unexpected keyword arguments: {list(badargs)}" - ) + raise CookieError(f"create_cookie() got unexpected keyword arguments: {list(badargs)}") result.update(kwargs) result["port_specified"] = bool(result["port"]) @@ -467,7 +460,7 @@ def morsel_to_cookie(morsel): try: expires = int(time.time() + int(morsel["max-age"])) except ValueError: - raise TypeError(f"max-age: {morsel['max-age']} must be integer") + raise CookieError(f"max-age: {morsel['max-age']} must be integer") elif morsel["expires"]: time_template = "%a, %d-%b-%Y %H:%M:%S GMT" expires = calendar.timegm(time.strptime(morsel["expires"], time_template)) @@ -523,7 +516,7 @@ def merge_cookies(cookiejar, cookies): cookiejar = cookiejar_from_dict(cookies, cookiejar=cookiejar, overwrite=False) elif isinstance(cookies, cookielib.CookieJar): try: - cookiejar.update(cookies) + cookiejar.update(cookies) # type: ignore except AttributeError: for cookie_in_jar in cookies: cookiejar.set_cookie(cookie_in_jar) @@ -531,24 +524,24 @@ def merge_cookies(cookiejar, cookies): return cookiejar -class Cookies(MutableMapping[str, str], ABC): - def __init__(self, cookies: CookieTypes = None) -> None: +class Cookies(MutableMapping[str, str]): + def __init__(self, cookies: Optional[CookieTypes] = None) -> None: self.cookiejar = self._prepare_cookiejar(cookies) - def _prepare_cookiejar(self, cookies: CookieTypes = None) -> RequestsCookieJar: + def _prepare_cookiejar(self, cookies: Optional[CookieTypes] = None) -> RequestsCookieJar: if isinstance(cookies, self.__class__): return cookies.cookiejar if isinstance(cookies, (dict, tuple, list, set)): cookiejar = RequestsCookieJar() - if isinstance(cookies, dict): - cookies = cookies.items() - - for k, v in cookies: - if isinstance(v, (float, int)): - v = str(v) - - cookiejar.set(k, v) + cookie_items = cookies.items() if isinstance(cookies, dict) else cookies + + for item in cookie_items: + if isinstance(item, (tuple, list)) and len(item) >= 2: + k, v = item[0], item[1] + if isinstance(v, (float, int)): + v = str(v) + cookiejar.set(str(k), str(v)) return cookiejar return RequestsCookieJar() @@ -561,10 +554,8 @@ def get_cookie_header(self, request: Request) -> str: def set(self, name, value, **kwargs) -> Optional[Cookie]: if value is None: - remove_cookie_by_name( - self, name, domain=kwargs.get("domain"), path=kwargs.get("path") - ) - return + remove_cookie_by_name(self.cookiejar, name, domain=kwargs.get("domain"), path=kwargs.get("path")) + return None if isinstance(value, Morsel): cookie = morsel_to_cookie(value) @@ -574,13 +565,13 @@ def set(self, name, value, **kwargs) -> Optional[Cookie]: self.cookiejar.set_cookie(cookie) return cookie - def get(self, name, default=None, domain="", path="/") -> str: + def get(self, name, default=None, domain=None, path=None) -> Any: return self.cookiejar.get(name, default, domain, path) - def delete(self, name: str, domain: str = None, path: str = None) -> None: - return remove_cookie_by_name(self.cookiejar, name) + def delete(self, name: str, domain: Optional[str] = None, path: Optional[str] = None) -> None: + return remove_cookie_by_name(self.cookiejar, name, domain=domain, path=path) - def clear(self, domain: str = None, path: str = None): + def clear(self, domain: Optional[str] = None, path: Optional[str] = None): args = [] if domain: args.append(domain) @@ -589,23 +580,22 @@ def clear(self, domain: str = None, path: str = None): self.cookiejar.clear(*args) - def update(self, cookies: CookieTypes = None) -> "Cookies": # noqa + def update(self, cookies: Optional[CookieTypes] = None) -> None: # type: ignore[override] self.cookiejar.update(self._prepare_cookiejar(cookies)) - return self def copy(self) -> "Cookies": ret = self.__class__() ret.cookiejar = _copy_cookie_jar(self.cookiejar) return ret - def __setitem__(self, name: str, value: str) -> Optional[Cookie]: - return self.set(name, value) + def __setitem__(self, name: str, value: str) -> None: # type: ignore[override] + self.set(name, value) def __getitem__(self, name: str) -> str: - return self.cookiejar.get(name) + return self.cookiejar.get(name) or "" def __delitem__(self, name: str) -> None: - return self.delete(name) + self.delete(name) def __len__(self) -> int: return len(self.cookiejar) diff --git a/tls_requests/models/encoders.py b/src/tls_requests/models/encoders.py similarity index 75% rename from tls_requests/models/encoders.py rename to src/tls_requests/models/encoders.py index 3bca4c6..b805325 100644 --- a/tls_requests/models/encoders.py +++ b/src/tls_requests/models/encoders.py @@ -1,20 +1,21 @@ +from __future__ import annotations + import binascii import os from io import BufferedReader, BytesIO, TextIOWrapper from mimetypes import guess_type -from typing import Any, AsyncIterator, Dict, Iterator, Mapping, Tuple, TypeVar +from typing import Any, AsyncIterator, Dict, Iterator, List, Mapping, Optional, Tuple, TypeVar, cast from urllib.parse import urlencode -from tls_requests.types import (BufferTypes, ByteOrStr, RequestData, - RequestFiles, RequestFileValue, RequestJson) -from tls_requests.utils import to_bytes, to_str +from ..types import BufferTypes, ByteOrStr, RequestData, RequestFiles, RequestFileValue, RequestJson +from ..utils import to_bytes, to_str -__all__ = [ +__all__ = ( "JsonEncoder", "UrlencodedEncoder", "MultipartEncoder", "StreamEncoder", -] +) T = TypeVar("T", bound="BaseEncoder") @@ -45,7 +46,7 @@ def iter_buffer(buffer: BufferTypes, chunk_size: int = 65_536): class BaseField: def __init__(self, name: str, value: Any): self._name = name - self._headers = {} + self._headers: Dict[bytes, bytes] = {} @property def headers(self): @@ -64,9 +65,7 @@ def render_parts(self) -> bytes: def render_headers(self) -> bytes: headers = self.get_headers() - return ( - b"\r\n".join(b"%s: %s" % (k, v) for k, v in headers.items()) + b"\r\n\r\n" - ) + return b"\r\n".join(b"%s: %s" % (k, v) for k, v in headers.items()) + b"\r\n\r\n" def render_data(self, chunk_size: int = 65_536) -> Iterator[bytes]: yield b"" @@ -80,9 +79,7 @@ def get_headers(self) -> Dict[bytes, bytes]: content_type = getattr(self, "content_type", None) if content_type: self._headers[b"Content-Type"] = ( - self.content_type.encode("ascii") - if isinstance(content_type, str) - else content_type + content_type.encode("ascii") if isinstance(content_type, str) else content_type ) return self._headers @@ -109,7 +106,7 @@ def unpack(self, value: RequestFileValue) -> Tuple[str, BufferTypes, str]: if args: content_type = args[0] else: - buffer = value + buffer = value[0] elif isinstance(value, str): buffer = value.encode("utf-8") @@ -118,21 +115,21 @@ def unpack(self, value: RequestFileValue) -> Tuple[str, BufferTypes, str]: if isinstance(buffer, (TextIOWrapper, BufferedReader)): if not filename: - _, filename = os.path.split(buffer.name) + _, filename = os.path.split(str(buffer.name)) if not content_type: - content_type = guess_content_type(buffer.name) + content_type = guess_content_type(str(buffer.name)) if buffer.mode != "rb": buffer.close() buffer = open(buffer.name, "rb") - elif not isinstance(buffer, bytes): - raise ValueError - else: + elif isinstance(buffer, bytes): buffer = BytesIO(buffer) + elif not hasattr(buffer, "read"): + raise ValueError - return filename or "upload", buffer, content_type or "application/octet-stream" + return str(filename or "upload"), cast(BufferTypes, buffer), str(content_type or "application/octet-stream") def render_data(self, chunk_size: int = 65_536) -> Iterator[bytes]: yield from iter_buffer(self._buffer, chunk_size) @@ -174,7 +171,7 @@ async def __aiter__(self) -> AsyncIterator[bytes]: for chunk in self.render(): yield chunk - def __enter__(self) -> T: + def __enter__(self: T) -> T: return self def __exit__(self, *args, **kwargs): @@ -184,9 +181,9 @@ def __exit__(self, *args, **kwargs): class MultipartEncoder(BaseEncoder): def __init__( self, - data: RequestData = None, - files: RequestFiles = None, - boundary: bytes = None, + data: Optional[RequestData] = None, + files: Optional[RequestFiles] = None, + boundary: Optional[bytes] = None, *, chunk_size: int = 65_536, **kwargs, @@ -194,9 +191,7 @@ def __init__( self._chunk_size = chunk_size self._is_closed = False self.fields = self._prepare_fields(data, files) - self.boundary = ( - boundary if boundary and isinstance(boundary, bytes) else get_boundary() - ) + self.boundary = boundary if boundary and isinstance(boundary, bytes) else get_boundary() @property def headers(self) -> dict: @@ -216,24 +211,24 @@ def render(self) -> Iterator[bytes]: yield b"--%s--\r\n" % self.boundary yield b"" - def _prepare_fields(self, data: RequestData, files: RequestFiles): - fields = [] + def _prepare_fields(self, data: Optional[RequestData], files: Optional[RequestFiles]): + fields: List[BaseField] = [] if isinstance(data, Mapping): for name, value in data.items(): if isinstance(value, (bytes, str, int, float, bool)): - fields.append(DataField(name=name, value=value)) + fields.append(DataField(name=to_str(name), value=value)) else: for item in value: - fields.append(DataField(name=name, value=item)) + fields.append(DataField(name=to_str(name), value=item)) if isinstance(files, Mapping): - for name, value in files.items(): - fields.append(FileField(name=name, value=value)) + for file_name, file_value in files.items(): + fields.append(FileField(name=to_str(file_name), value=file_value)) return fields class JsonEncoder(BaseEncoder): - def __init__(self, data: RequestData, *, chunk_size: int = 65_536, **kwargs): + def __init__(self, data: Optional[RequestData], *, chunk_size: int = 65_536, **kwargs): self._buffer = self._prepare_fields(data) self._chunk_size = chunk_size self._is_closed = False @@ -241,13 +236,14 @@ def __init__(self, data: RequestData, *, chunk_size: int = 65_536, **kwargs): def get_headers(self): return {b"Content-Type": b"application/json"} - def _prepare_fields(self, data: RequestData): - if isinstance(data, Mapping): + def _prepare_fields(self, data: Optional[RequestData]): + if data is not None: return BytesIO(to_bytes(data)) + return None class UrlencodedEncoder(BaseEncoder): - def __init__(self, data: RequestData, *, chunk_size: int = 65_536, **kwargs): + def __init__(self, data: Optional[RequestData], *, chunk_size: int = 65_536, **kwargs): self._buffer = self._prepare_fields(data) self._chunk_size = chunk_size self._is_closed = False @@ -255,7 +251,7 @@ def __init__(self, data: RequestData, *, chunk_size: int = 65_536, **kwargs): def get_headers(self): return {b"Content-Type": b"application/x-www-form-urlencoded"} - def _prepare_fields(self, data: RequestData): + def _prepare_fields(self, data: Optional[RequestData]): fields = [] if isinstance(data, Mapping): for name, value in data.items(): @@ -266,22 +262,23 @@ def _prepare_fields(self, data: RequestData): fields.append((name, to_str(item))) return BytesIO(urlencode(fields, doseq=True).encode("utf-8")) + return None class StreamEncoder(BaseEncoder): def __init__( self, - data: RequestData = None, - files: RequestFiles = None, - json: RequestJson = None, + data: Optional[RequestData] = None, + files: Optional[RequestFiles] = None, + json: Optional[RequestJson] = None, *, - chunk_size: int = 65_536, + chunk_size: Optional[int] = 65_536, **kwargs, ): self._chunk_size = chunk_size if isinstance(chunk_size, int) else 65_536 self._is_closed = False if files is not None: - self._stream = MultipartEncoder(data, files) + self._stream: BaseEncoder = MultipartEncoder(data, files) elif data is not None: self._stream = UrlencodedEncoder(data) elif json is not None: @@ -297,9 +294,9 @@ def get_headers(self) -> dict: return self._stream.get_headers() @classmethod - def from_bytes(cls, raw: bytes, *, chunk_size: int = None) -> "StreamEncoder": + def from_bytes(cls, raw: bytes, *, chunk_size: Optional[int] = None) -> "StreamEncoder": ret = cls(chunk_size=chunk_size) - ret._stream._buffer = BytesIO(raw) + setattr(ret._stream, "_buffer", BytesIO(raw)) return ret def close(self): diff --git a/tls_requests/models/headers.py b/src/tls_requests/models/headers.py similarity index 62% rename from tls_requests/models/headers.py rename to src/tls_requests/models/headers.py index c10acf6..f5838b9 100644 --- a/tls_requests/models/headers.py +++ b/src/tls_requests/models/headers.py @@ -1,12 +1,17 @@ -from abc import ABC +from __future__ import annotations + from collections.abc import Mapping, MutableMapping from enum import Enum -from typing import Any, ItemsView, KeysView, List, Literal, Tuple, ValuesView +from typing import Any, ItemsView, KeysView, List, Literal, Optional, Tuple, ValuesView -from tls_requests.types import ByteOrStr, HeaderTypes -from tls_requests.utils import to_str +from ..exceptions import HeaderError +from ..types import ByteOrStr, HeaderTypes +from ..utils import to_str -__all__ = ["Headers"] +__all__ = ( + "Headers", + "HeaderAlias", +) HeaderAliasTypes = Literal["*", "lower", "capitalize"] @@ -16,23 +21,14 @@ class HeaderAlias(str, Enum): CAPITALIZE = "capitalize" ALL = "*" - def __contains__(self, key: str) -> bool: - for item in self: - if item == key: - return True - return False + @classmethod + def contains(cls, key: Any) -> bool: + return any(item.value == key for item in cls) -class Headers(MutableMapping, ABC): - def __init__( - self, - headers: HeaderTypes = None, - *, - alias: HeaderAliasTypes = HeaderAlias.LOWER - ): - self.alias = ( - alias if alias in HeaderAlias._value2member_map_ else HeaderAlias.LOWER - ) +class Headers(MutableMapping): + def __init__(self, headers: Optional[HeaderTypes] = None, *, alias: HeaderAliasTypes = "lower"): + self.alias: HeaderAliasTypes = alias if HeaderAlias.contains(alias) else "lower" self._items = self._prepare_items(headers) def get(self, key: str, default: Any = None) -> Any: @@ -51,19 +47,20 @@ def keys(self) -> KeysView: def values(self) -> ValuesView: return {k: v for k, v in self.items()}.values() - def update(self, headers: HeaderTypes) -> "Headers": # noqa - headers = self.__class__(headers, alias=self.alias) # noqa - for idx, (key, _) in enumerate(headers._items): + def update(self, headers: Optional[HeaderTypes]) -> None: # type: ignore[override] + if headers is None: + return + new_headers = self.__class__(headers, alias=self.alias) + for key, _ in new_headers._items: if key in self: self.pop(key) - self._items.extend(headers._items) - return self + self._items.extend(new_headers._items) def copy(self) -> "Headers": - return self.__class__(self._items.copy(), alias=self.alias) # noqa + return self.__class__(self._items.copy(), alias=self.alias) # type: ignore[arg-type] - def _prepare_items(self, headers: HeaderTypes) -> List[Tuple[str, Any]]: + def _prepare_items(self, headers: Optional[HeaderTypes]) -> List[Tuple[str, List[str]]]: if headers is None: return [] if isinstance(headers, self.__class__): @@ -74,9 +71,9 @@ def _prepare_items(self, headers: HeaderTypes) -> List[Tuple[str, Any]]: try: items = [self._normalize(k, args[0]) for k, *args in headers] return items - except IndexError: + except (IndexError, ValueError): pass - raise TypeError + raise HeaderError("Invalid headers format") def _normalize_key(self, key: ByteOrStr) -> str: key = to_str(key, encoding="ascii") @@ -90,13 +87,13 @@ def _normalize_key(self, key: ByteOrStr) -> str: def _normalize_value(self, value) -> List[str]: if isinstance(value, dict): - raise TypeError + raise HeaderError("Header value cannot be a dictionary.") if isinstance(value, (list, tuple, set)): items = [] for item in value: if isinstance(item, dict): - raise TypeError + raise HeaderError("Header value items cannot be a dictionary.") items.append(to_str(item)) return items @@ -110,8 +107,7 @@ def __setitem__(self, key, value) -> None: key, value = self._normalize(key, value) for idx, (k, _) in enumerate(self._items): if k == key: - values = [v for v in value if v not in self._items[idx][1]] - self._items[idx][1].extend(values) + self._items[idx] = (k, value) found = True break @@ -119,7 +115,10 @@ def __setitem__(self, key, value) -> None: self._items.append((key, value)) def __getitem__(self, key): - return self.get(key) + val = self.get(key) + if val is None: + raise KeyError(key) + return val def __delitem__(self, key): key = self._normalize_key(key) @@ -143,17 +142,21 @@ def __iter__(self): return (k for k, _ in self._items) def __len__(self): - return len(self._headers) + return len(self._items) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, (Mapping, list, tuple, set, self.__class__)): + return False - def __eq__(self, other: HeaderTypes): - items = sorted(self._items) - other = sorted(self._prepare_items(other)) - return items == other + try: + items = sorted(self._items) + other_prepared = sorted(self._prepare_items(other)) # type: ignore + return items == other_prepared + except (HeaderError, TypeError): + return False def __repr__(self): - SECURE = [ - self._normalize_key(key) for key in ["Authorization", "Proxy-Authorization"] - ] + SECURE = [self._normalize_key(key) for key in ["Authorization", "Proxy-Authorization"]] return "<%s: %s>" % ( self.__class__.__name__, {k: "[secure]" if k in SECURE else ",".join(v) for k, v in self._items}, diff --git a/src/tls_requests/models/libraries.py b/src/tls_requests/models/libraries.py new file mode 100644 index 0000000..9319430 --- /dev/null +++ b/src/tls_requests/models/libraries.py @@ -0,0 +1,463 @@ +from __future__ import annotations + +import ctypes +import glob +import json +import os +import platform +import re +import sys +import urllib.error +import urllib.request +from dataclasses import dataclass, field, fields +from pathlib import Path +from platform import machine +from typing import List, Optional, Tuple + +from tls_requests.utils import get_logger + +logger = get_logger("TLSLibrary") + +__all__ = ("TLSLibrary",) + +LATEST_VERSION_TAG_NAME = "v1.13.1" +BIN_DIR = os.path.join(Path(__file__).resolve(strict=True).parent.parent / "bin") +RELEASE_CONFIG_PATH = os.path.join(BIN_DIR, "release.json") +GITHUB_API_URL = "https://api.github.com/repos/bogdanfinn/tls-client/releases" +PLATFORM = sys.platform +IS_UBUNTU = False +ARCH_MAPPING = { + "amd64": "amd64", + "x86_64": "amd64", + "x86": "386", + "i686": "386", + "i386": "386", + "arm64": "arm64", + "aarch64": "arm64", + "armv5l": "arm-5", + "armv6l": "arm-6", + "armv7l": "arm-7", + "ppc64le": "ppc64le", + "riscv64": "riscv64", + "s390x": "s390x", +} + +FILE_EXT = ".unk" +MACHINE_RAW = machine().lower() +MACHINE = ARCH_MAPPING.get(MACHINE_RAW) or MACHINE_RAW +if PLATFORM == "linux": + FILE_EXT = "so" + try: + if hasattr(platform, "freedesktop_os_release"): + platform_data = platform.freedesktop_os_release() + curr_system: Optional[str] = None + if "ID" in platform_data: + curr_system = platform_data["ID"] + else: + curr_system = platform_data.get("id") + + if curr_system and "ubuntu" in curr_system.lower(): + IS_UBUNTU = True + except Exception: + pass + +elif PLATFORM in ("win32", "cygwin"): + PLATFORM = "windows" + FILE_EXT = "dll" +elif PLATFORM == "darwin": + FILE_EXT = "dylib" + +PATTERN_RE = re.compile(r"%s-%s.*%s" % (PLATFORM, MACHINE, FILE_EXT), re.I) +PATTERN_UBUNTU_RE = re.compile(r"%s-%s.*%s" % ("ubuntu", MACHINE, FILE_EXT), re.I) +TLS_LIBRARY_PATH = os.getenv("TLS_LIBRARY_PATH") + + +@dataclass +class BaseRelease: + @classmethod + def model_fields_set(cls) -> set: + return {model_field.name for model_field in fields(cls)} + + @classmethod + def from_kwargs(cls, **kwargs): + model_fields_set = cls.model_fields_set() + return cls(**{k: v for k, v in kwargs.items() if k in model_fields_set}) # noqa + + +@dataclass +class ReleaseAsset(BaseRelease): + browser_download_url: str + name: Optional[str] = None + + +@dataclass +class Release(BaseRelease): + name: Optional[str] = None + tag_name: Optional[str] = None + assets: List[ReleaseAsset] = field(default_factory=list) + + @classmethod + def from_kwargs(cls, **kwargs): + model_fields_set = cls.model_fields_set() + assets = kwargs.pop("assets", []) or [] + kwargs["assets"] = [ReleaseAsset.from_kwargs(**asset_kwargs) for asset_kwargs in assets] + return cls(**{k: v for k, v in kwargs.items() if k in model_fields_set}) + + +class TLSLibrary: + """TLS Library + + A utility class for managing the TLS library, including discovery, validation, + downloading, and loading. This class facilitates interaction with system-specific + binaries, ensuring compatibility with the platform and machine architecture. + + Class Attributes: + _PATH (str): The current path to the loaded TLS library. + + Methods: + fetch_api(version: Optional[str] = None, retries: int = 3) -> Generator[str, None, None]: + Fetches library download URLs from the GitHub API for the specified version. + + is_valid(fp: str) -> bool: + Validates a file path against platform-specific patterns. + + find() -> str: + Finds the first valid library binary in the binary directory. + + find_all() -> list[str]: + Lists all library binaries in the binary directory. + + download(version: Optional[str] = None) -> str: + Downloads the library binary for the specified version. + + set_path(fp: str): + Sets the path to the currently loaded library. + + load() -> ctypes.CDLL: + Loads the library, either from an existing path or by discovering and downloading it. + """ + + _PATH: Optional[str] = None + _LIBRARY: Optional[ctypes.CDLL] = None + + @staticmethod + def _parse_version(version_string: str) -> Tuple[int, ...]: + """Converts a version string (e.g., "v1.11.2") to a comparable tuple (1, 11, 2).""" + try: + parts = str(version_string).lstrip("v").split(".") + return tuple(map(int, parts)) + except (ValueError, AttributeError): + return 0, 0, 0 + + @staticmethod + def _parse_version_from_filename(filename: str) -> Tuple[int, ...]: + """Extracts and parses the version from a library filename.""" + match = re.search(r"v?(\d+\.\d+\.\d+)", Path(filename).name) + if match: + return TLSLibrary._parse_version(match.group(1)) + return 0, 0, 0 + + @classmethod + def cleanup_files(cls, keep_file: Optional[str] = None): + """Removes all library files in the BIN_DIR except for the one to keep.""" + for file_path in cls.find_all(): + is_remove = True + if keep_file and Path(file_path).name == Path(keep_file).name: + is_remove = False + + if is_remove: + try: + os.remove(file_path) + logger.info(f"Removed old library file: {file_path}") + except OSError as e: + logger.error(f"Error removing old library file {file_path}: {e}") + + @classmethod + def import_config(cls) -> Optional[dict]: + """Loads release data from local disk.""" + if os.path.exists(RELEASE_CONFIG_PATH): + try: + with open(RELEASE_CONFIG_PATH, "r", encoding="utf-8") as f: + return json.load(f) + except Exception as e: + logger.error(f"Error loading local release config: {e}") + return None + + @classmethod + def export_config(cls, data: dict): + """Saves release data to local disk.""" + try: + os.makedirs(BIN_DIR, exist_ok=True) + with open(RELEASE_CONFIG_PATH, "w", encoding="utf-8") as f: + json.dump(data, f, indent=4) + logger.info(f"Saved release config to {RELEASE_CONFIG_PATH}") + except Exception as e: + logger.error(f"Error saving local release config: {e}") + + @classmethod + def fetch_api(cls, version: Optional[str] = None, retries: int = 3): + def _process_data(data): + releases_data = data if isinstance(data, list) else [data] + + releases = [Release.from_kwargs(**kwargs) for kwargs in releases_data] + + if version is not None: + version_ = "v%s" % version if not str(version).startswith("v") else str(version) + releases = [ + release for release in releases if re.search(version_, release.name or release.tag_name, re.I) + ] + + found_urls = False + for release in releases: + for asset in release.assets: + if asset.name: + if IS_UBUNTU and PATTERN_UBUNTU_RE.search(asset.name): + ubuntu_urls.append(asset.browser_download_url) + found_urls = True + if PATTERN_RE.search(asset.name): + asset_urls.append(asset.browser_download_url) + found_urls = True + return found_urls + + asset_urls: List[str] = [] + ubuntu_urls: List[str] = [] + api_data = None + + for _ in range(retries): + try: + with urllib.request.urlopen(GITHUB_API_URL, timeout=10) as response: + if response.status == 200: + content = response.read().decode("utf-8") + api_data = json.loads(content) + # Save the first element (latest release) to local config + if isinstance(api_data, list) and api_data: + cls.export_config(api_data[0]) + elif isinstance(api_data, dict): + cls.export_config(api_data) + + if _process_data(api_data): + logger.info("Fetched release data from GitHub API.") + break + except Exception as ex: + logger.debug(f"GitHub API fetch failed (Attempt {_ + 1}): {ex}") + + if not asset_urls and not ubuntu_urls: + local_data = cls.import_config() + if local_data: + _process_data(local_data) + logger.info(f"Loaded release data from local config: {RELEASE_CONFIG_PATH}") + else: + # Last resort: construct a direct download URL based on naming patterns + # This works if API is rate-limited and no local config exists + v_tag = version or LATEST_VERSION_TAG_NAME + if not v_tag.startswith("v"): + v_tag = f"v{v_tag}" + + # Mapping for Windows architecture naming convention in bogdanfinn/tls-client + win_arch_map = {"amd64": "64", "386": "32"} + + target_arches = [MACHINE] + if PLATFORM == "windows" and MACHINE in win_arch_map: + target_arches.insert(0, win_arch_map[MACHINE]) + + # Generate a few potential candidates for the fallback URL + platforms = [PLATFORM] + if IS_UBUNTU: + platforms.insert(0, "ubuntu") + + for plat in platforms: + for arch in target_arches: + # Try with 'v' and without 'v' in filename as naming patterns vary + for v_str in [v_tag, v_tag.lstrip("v")]: + direct_filename = f"tls-client-{plat}-{arch}-{v_str}.{FILE_EXT}" + direct_url = ( + f"https://github.com/bogdanfinn/tls-client/releases/download/{v_tag}/{direct_filename}" + ) + asset_urls.append(direct_url) + + logger.info(f"Fallback: generated direct download URLs: {', '.join(asset_urls)}") + + for url in ubuntu_urls: + yield url + + for url in asset_urls: + yield url + + @classmethod + def find(cls) -> Optional[str]: + for fp in cls.find_all(): + if PATTERN_RE.search(fp): + return fp + return None + + @classmethod + def find_all(cls) -> List[str]: + return [src for src in glob.glob(os.path.join(BIN_DIR, r"*")) if src.lower().endswith(("so", "dll", "dylib"))] + + @classmethod + def update(cls): + """Forces a download of the latest library version.""" + logger.info(f"Updating TLS library to version {LATEST_VERSION_TAG_NAME}...") + downloaded_fp = cls.download(version=LATEST_VERSION_TAG_NAME) + if downloaded_fp: + cls.cleanup_files(keep_file=downloaded_fp) + logger.info("Update complete.") + return downloaded_fp + logger.error("Update failed.") + return None + + upgrade = update + + @classmethod + def download(cls, version: Optional[str] = None) -> Optional[str]: + try: + logger.info( + "System Info - Platform: %s, Machine: %s, File Ext : %s." + % ( + PLATFORM, + "%s (Ubuntu)" % MACHINE if IS_UBUNTU else MACHINE, + FILE_EXT, + ) + ) + download_url = None + for url in cls.fetch_api(version): + if not url: + continue + + download_url = url + logger.info("Trying to download library from: %s" % download_url) + + try: + destination_name = download_url.split("/")[-1] + destination = os.path.join(BIN_DIR, destination_name) + + # Use standard library's urllib to download the file + with urllib.request.urlopen(download_url, timeout=15) as response: + if response.status != 200: + logger.debug(f"Skipping {download_url}: HTTP {response.status}") + continue + + os.makedirs(BIN_DIR, exist_ok=True) + total_size = int(response.headers.get("content-length", 0)) + chunk_size = 8192 # 8KB + + with open(destination, "wb") as file: + downloaded = 0 + while True: + chunk = response.read(chunk_size) + if not chunk: + break + + file.write(chunk) + downloaded += len(chunk) + + # Simple text-based progress bar + if total_size > 0: + percent = (downloaded / total_size) * 100 + bar_length = 50 + filled_length = int(bar_length * downloaded // total_size) + bar = "=" * filled_length + "-" * (bar_length - filled_length) + sys.stdout.write(f"\rDownloading {destination_name}: [{bar}] {percent:.1f}%") + sys.stdout.flush() + + sys.stdout.write("\n") + return destination + except (urllib.error.URLError, urllib.error.HTTPError) as ex: + logger.debug(f"Failed to download from {download_url}: {ex}") + continue + + logger.error("All download attempts failed.") + + except Exception as e: + logger.error("An unexpected error occurred during download: %s" % e) + return None + + @classmethod + def set_path(cls, fp: str): + cls._PATH = fp + + @classmethod + def load(cls): + """ + Loads the TLS library. It checks for the correct version, downloads it if + the local version is outdated or missing, and then loads it into memory. + """ + target_version = cls._parse_version(LATEST_VERSION_TAG_NAME) + + if cls._LIBRARY and cls._PATH: + cached_version = cls._parse_version_from_filename(cls._PATH) + if cached_version == target_version: + return cls._LIBRARY + + def _load_library(fp_): + try: + lib = ctypes.cdll.LoadLibrary(fp_) + cls.set_path(fp_) + cls._LIBRARY = lib + logger.info(f"Successfully loaded TLS library: {fp_}") + return lib + except Exception as ex: + logger.error(f"Unable to load TLS library '{fp_}', details: {ex}") + try: + os.remove(fp_) + except (FileNotFoundError, PermissionError): + pass + + if TLS_LIBRARY_PATH: + logger.info(f"Loading TLS library from environment variable: {TLS_LIBRARY_PATH}") + return _load_library(TLS_LIBRARY_PATH) + + logger.debug(f"Required library version: {LATEST_VERSION_TAG_NAME}") + local_files = cls.find_all() + newest_local_version: tuple[int, ...] = (0, 0, 0) + newest_local_file = None + + if local_files: + for file_path in local_files: + file_version = cls._parse_version_from_filename(file_path) + if file_version > newest_local_version: + newest_local_version = file_version + newest_local_file = file_path + logger.debug( + f"Found newest local library: {newest_local_file} (version {'.'.join(map(str, newest_local_version))})" + ) + else: + logger.debug("No local library found.") + + if newest_local_version < target_version: + if newest_local_file: + logger.warning( + f"Local library is outdated (Found: {'.'.join(map(str, newest_local_version))}, " + f"Required: {LATEST_VERSION_TAG_NAME}). " + f"Auto-downloading... To manually upgrade, run: `python -m tls_requests.models.libraries`" + ) + else: + logger.info(f"Downloading required library version {LATEST_VERSION_TAG_NAME}...") + + downloaded_fp = cls.download(version=LATEST_VERSION_TAG_NAME) + if downloaded_fp: + cls.cleanup_files(keep_file=downloaded_fp) + library = _load_library(downloaded_fp) + if library: + return library + + logger.error( + f"Failed to download the required TLS library {LATEST_VERSION_TAG_NAME}. " + "Please check your connection or download it manually from GitHub." + ) + raise OSError("Failed to download the required TLS library.") + + if newest_local_file: + library = _load_library(newest_local_file) + if library: + cls.cleanup_files(keep_file=newest_local_file) + return library + + raise OSError("Could not find or load a compatible TLS library.") + + +if __name__ == "__main__": + try: + TLSLibrary.load() + except Exception as ex: + logger.error(f"Manual load test failed: {ex}") diff --git a/src/tls_requests/models/request.py b/src/tls_requests/models/request.py new file mode 100644 index 0000000..7961077 --- /dev/null +++ b/src/tls_requests/models/request.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from typing import Any, Optional, Union + +from ..settings import DEFAULT_TIMEOUT +from ..types import ( + CookieTypes, + HeaderTypes, + MethodTypes, + RequestData, + RequestFiles, + TimeoutTypes, + URLParamTypes, + URLTypes, +) +from .cookies import Cookies +from .encoders import StreamEncoder +from .headers import Headers +from .urls import URL, Proxy + +__all__ = ("Request",) + + +class Request: + def __init__( + self, + method: MethodTypes, + url: URLTypes, + *, + data: Optional[RequestData] = None, + files: Optional[RequestFiles] = None, + json: Optional[Any] = None, + params: URLParamTypes = None, + headers: Optional[HeaderTypes] = None, + cookies: CookieTypes = None, + proxy: Optional[Union[Proxy, URL, str, bytes]] = None, + timeout: Optional[TimeoutTypes] = None, + protocol_racing: Optional[bool] = None, + allow_http: Optional[bool] = None, + stream_id: Optional[int] = None, + **kwargs: Any, + ) -> None: + self._content: bytes = b"" + self._session_id: str = "" + self._extra_config = kwargs + self.url = URL(url, params=params) + self.method = method.upper() + self.cookies = Cookies(cookies) + self.proxy = Proxy(proxy) if proxy else None + self.timeout = timeout if isinstance(timeout, (float, int)) else DEFAULT_TIMEOUT + self.protocol_racing = protocol_racing + self.allow_http = allow_http + self.stream_id = stream_id + self.stream = StreamEncoder(data, files, json) + self.headers = self._prepare_headers(headers) + + def _prepare_headers(self, headers) -> Headers: + headers = Headers(headers) + headers.update(self.stream.headers) + if self.url.host and "Host" not in headers: + headers.setdefault(b"Host", self.url.host) + + return headers + + @property + def id(self): + return self._session_id + + @property + def content(self) -> bytes: + return self._content + + def read(self) -> bytes: + if not self._content: + self._content = b"".join(self.stream.render()) + return self._content + + async def aread(self) -> bytes: + if not self._content: + self._content = b"".join([chunk async for chunk in self.stream]) + return self._content + + def __repr__(self) -> str: + return "<%s: (%s, %s)>" % (self.__class__.__name__, self.method, self.url) diff --git a/tls_requests/models/response.py b/src/tls_requests/models/response.py similarity index 61% rename from tls_requests/models/response.py rename to src/tls_requests/models/response.py index 8aa8127..22378c5 100644 --- a/tls_requests/models/response.py +++ b/src/tls_requests/models/response.py @@ -1,21 +1,24 @@ +from __future__ import annotations + import binascii import codecs import datetime +import typing from email.message import Message from typing import Any, Callable, Optional, TypeVar, Union -from tls_requests.exceptions import Base64DecodeError, HTTPError -from tls_requests.models.cookies import Cookies -from tls_requests.models.encoders import StreamEncoder -from tls_requests.models.headers import Headers -from tls_requests.models.request import Request -from tls_requests.models.status_codes import StatusCodes -from tls_requests.models.tls import TLSResponse -from tls_requests.settings import CHUNK_SIZE -from tls_requests.types import CookieTypes, HeaderTypes, ResponseHistory -from tls_requests.utils import b64decode, chardet, to_json +from ..exceptions import Base64DecodeError, HTTPError +from ..settings import CHUNK_SIZE +from ..types import CookieTypes, HeaderTypes, ResponseHistory +from ..utils import b64decode, chardet, to_json +from .cookies import Cookies +from .encoders import StreamEncoder +from .headers import Headers +from .request import Request +from .status_codes import StatusCodes +from .tls import TLSResponse -__all__ = ["Response"] +__all__ = ("Response",) T = TypeVar("T", bound="Response") @@ -33,30 +36,30 @@ def __init__( self, status_code: int, *, - headers: HeaderTypes = None, + headers: Optional[HeaderTypes] = None, cookies: CookieTypes = None, - request: Union[Request] = None, - history: ResponseHistory = None, - body: bytes = None, - stream: StreamEncoder = None, - default_encoding: Union[str, Callable] = "utf-8", + request: Optional[Request] = None, + history: Optional[ResponseHistory] = None, + body: Optional[bytes] = None, + stream: Optional[StreamEncoder] = None, + default_encoding: Union[str, Callable[..., Any]] = "utf-8", ) -> None: - self._content = None - self._elapsed = None - self._encoding = None - self._text = None - self._response_id = None - self._http_version = None + self._content: bytes = b"" + self._elapsed: Optional[datetime.timedelta] = None + self._encoding: Optional[str] = None + self._text: Optional[str] = None + self._response_id: str = "" + self._http_version: str = "HTTP/1.1" self._request: Optional[Request] = request self._cookies = Cookies(cookies) self._is_stream_consumed = False self._is_closed = False self._next: Optional[Request] = None self.headers = Headers(headers) - self.stream = None self.status_code = status_code self.history = history if isinstance(history, list) else [] self.default_encoding = default_encoding + self.stream: Optional[StreamEncoder] = None if isinstance(stream, StreamEncoder): self.stream = stream else: @@ -68,7 +71,7 @@ def id(self) -> str: @property def elapsed(self) -> datetime.timedelta: - return self._elapsed + return self._elapsed or datetime.timedelta(0) @elapsed.setter def elapsed(self, elapsed: datetime.timedelta) -> None: @@ -77,9 +80,7 @@ def elapsed(self, elapsed: datetime.timedelta) -> None: @property def request(self) -> Request: if self._request is None: - raise RuntimeError( - "The request instance has not been set on this response." - ) + raise RuntimeError("The request instance has not been set on this response.") return self._request @request.setter @@ -101,9 +102,14 @@ def http_version(self) -> str: @property def cookies(self) -> Cookies: - if self._cookies is None: - self._cookies = Cookies() - self._cookies.extract_cookies(self, self.request) + if self._request: + # Fix missing domain in cookies by backfilling from request URL + # Ref: https://github.com/thewebscraping/tls-requests/issues/47 + for cookie in self._cookies.cookiejar: + if not cookie.domain: + cookie.domain = self._request.url.host + cookie.domain_specified = False + cookie.domain_initial_dot = False return self._cookies @property @@ -135,23 +141,33 @@ def charset(self) -> Optional[str]: msg = Message() msg["content-type"] = self.headers["Content-Type"] return msg.get_content_charset(failobj=None) + return None @property def encoding(self) -> str: if self._encoding is None: - encoding = self.charset - if encoding is None: - if isinstance(self.default_encoding, str): - try: - codecs.lookup(self.default_encoding) - encoding = self.default_encoding - except LookupError: - pass - - if not encoding and chardet and self.content: - encoding = chardet.detect(self.content)["encoding"] + encoding = self.charset or self.default_encoding + if not encoding and chardet and self.content: + encoding = chardet.detect(self.content)["encoding"] + try: + if encoding: + # fix: charset=utf-8,gbk + if callable(encoding): + encoding = typing.cast(str, encoding(self)) + + if isinstance(encoding, str): + encoding = encoding.split(",")[0].strip() + codecs.lookup(encoding) + self._encoding = encoding + else: + raise LookupError + except LookupError: + self._encoding = "utf-8" # fallback to utf-8 + + # Always ensure self._encoding is not None at this point + if self._encoding is None: + self._encoding = "utf-8" - self._encoding = encoding or "utf-8" return self._encoding @property @@ -191,13 +207,10 @@ def raise_for_status(self) -> "Response": raise HTTPError( http_error_msg.format( self.status_code, - ( - self.reason - if self.status_code < 100 - else StatusCodes.get_reason(self.status_code) - ), + (self.reason if self.status_code < 100 else StatusCodes.get_reason(self.status_code)), self.url, - ) + ), + response=self, ) return self @@ -209,11 +222,15 @@ def __repr__(self) -> str: return f"" def read(self) -> bytes: + if self.stream is None: + return b"" with self.stream as stream: self._content = b"".join(stream.render()) return self._content async def aread(self) -> bytes: + if self.stream is None: + return b"" with self.stream as stream: self._content = b"".join([chunk async for chunk in stream]) return self._content @@ -226,25 +243,26 @@ def close(self) -> None: if not self._is_closed: self._is_closed = True self._is_stream_consumed = True - self.stream.close() + if self.stream: + self.stream.close() + + # Fix pickle dump + # Ref: https://github.com/thewebscraping/tls-requests/issues/35 + self.stream = None async def aclose(self) -> None: return self.close() @classmethod - def from_tls_response( - cls, response: TLSResponse, is_byte_response: bool = False - ) -> "Response": + def from_tls_response(cls, response: TLSResponse, is_byte_response: bool = False) -> "Response": def _parse_response_body(value: Optional[str]) -> bytes: if value: if is_byte_response and response.status > 0: try: - value = b64decode(value.split(",")[-1]) - return value + content = b64decode(value.split(",")[-1]) + return content except (binascii.Error, AssertionError): - raise Base64DecodeError( - "Couldn't decode the base64 string into bytes." - ) + raise Base64DecodeError("Couldn't decode the base64 string into bytes.") return value.encode("utf-8") return b"" @@ -254,6 +272,6 @@ def _parse_response_body(value: Optional[str]) -> bytes: headers=response.headers, cookies=response.cookies, ) - ret._response_id = response.id - ret._http_version = response.usedProtocol + ret._response_id = response.id or "" + ret._http_version = response.usedProtocol or "HTTP/1.1" return ret diff --git a/src/tls_requests/models/rotators.py b/src/tls_requests/models/rotators.py new file mode 100644 index 0000000..6b689e0 --- /dev/null +++ b/src/tls_requests/models/rotators.py @@ -0,0 +1,569 @@ +from __future__ import annotations + +import asyncio +import itertools +import json +import random +import threading +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Any, Generic, Iterable, Iterator, List, Literal, Optional, TypeVar, Union + +from ..exceptions import RotatorError +from ..types import HeaderTypes, IdentifierTypes +from .headers import Headers +from .urls import Proxy + +__all__ = [ + "BaseRotator", + "ProxyRotator", + "TLSIdentifierRotator", + "HeaderRotator", +] + +T = TypeVar("T") +R = TypeVar("R", bound="BaseRotator") + +CLIENT_IDENTIFIER_TEMPLATES = [ + "chrome_120", + "chrome_124", + "chrome_131", + "chrome_133", + "firefox_120", + "firefox_123", + "firefox_132", + "firefox_133", + "safari_16_0", + "safari_ios_16_0", + "safari_ios_17_0", + "safari_ios_18_0", + "safari_ios_18_5", +] + +USER_AGENTS = [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + ( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0" + " Safari/537.36" + ), + ( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0" + " Safari/537.36" + ), + ( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0" + " Safari/537.36" + ), + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0", + "Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0", + ( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2" + " Safari/605.1.15" + ), + ( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2" + " Safari/605.1.15" + ), + ( + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2" + " Mobile/15E148 Safari/604.1" + ), + ( + "Mozilla/5.0 (iPad; CPU OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2" + " Mobile/15E148 Safari/604.1" + ), + ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0" + " Safari/537.36 Edg/120.0.0.0" + ), + ( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0" + " Safari/537.36 Edg/120.0.0.0" + ), + ( + "Mozilla/5.0 (Linux; Android 14; SM-G998B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile" + " Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 14; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile" + " Safari/537.36" + ), + ( + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1" + " Mobile/15E148 Safari/604.1" + ), + ( + "Mozilla/5.0 (Linux; Android 15; SM-S931B Build/AP3A.240905.015.A2; wv) AppleWebKit/537.36 (KHTML, like Gecko)" + " Version/4.0 Chrome/127.0.6533.103 Mobile Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 14; SM-S928B/DS) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.230" + " Mobile Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 14; SM-F956U) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0" + " Chrome/80.0.3987.119 Mobile Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 13; SM-S911U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Mobile" + " Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 13; SM-S901B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile" + " Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 13; SM-S908U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Mobile" + " Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile" + " Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 13; SM-G998U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile" + " Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 14; Pixel 9 Pro Build/AD1A.240418.003; wv) AppleWebKit/537.36 (KHTML, like Gecko)" + " Version/4.0 Chrome/124.0.6367.54 Mobile Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 14; Pixel 9 Build/AD1A.240411.003.A5; wv) AppleWebKit/537.36 (KHTML, like Gecko)" + " Version/4.0 Chrome/124.0.6367.54 Mobile Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 15; Pixel 8 Pro Build/AP4A.250105.002; wv) AppleWebKit/537.36 (KHTML, like Gecko)" + " Version/4.0 Chrome/132.0.6834.163 Mobile Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 15; Pixel 8 Build/AP4A.250105.002; wv) AppleWebKit/537.36 (KHTML, like Gecko)" + " Version/4.0 Chrome/132.0.6834.163 Mobile Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 13; Pixel 7 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile" + " Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile" + " Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 15; moto g - 2025 Build/V1VK35.22-13-2; wv) AppleWebKit/537.36 (KHTML, like Gecko)" + " Version/4.0 Chrome/132.0.6834.163 Mobile Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 13; 23129RAA4G Build/TKQ1.221114.001; wv) AppleWebKit/537.36 (KHTML, like Gecko)" + " Version/4.0 Chrome/116.0.0.0 Mobile Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 15; 24129RT7CC Build/AP3A.240905.015.A2; wv) AppleWebKit/537.36 (KHTML, like" + " Gecko) Version/4.0 Chrome/130.0.6723.86 Mobile Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 12; HBP-LX9 Build/HUAWEIHBP-L29; wv) AppleWebKit/537.36 (KHTML, like Gecko)" + " Version/4.0 Chrome/99.0.4844.88 Mobile Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; U; Android 12; zh-Hans-CN; ADA-AL00 Build/HUAWEIADA-AL00) AppleWebKit/537.36 (KHTML, like" + " Gecko) Version/4.0 Chrome/100.0.4896.58 Quark/6.11.2.531 Mobile Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 12; PSD-AL00 Build/HUAWEIPSD-AL00; wv) AppleWebKit/537.36 (KHTML, like Gecko)" + " Version/4.0 Chrome/99.0.4844.88 Mobile Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 14; 24030PN60G Build/UKQ1.231003.002; wv) AppleWebKit/537.36 (KHTML, like Gecko)" + " Version/4.0 Chrome/122.0.6261.119 Mobile Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 10; VOG-L29) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile" + " Safari/537.36" + ), + ( + "Mozilla/5.0 (Linux; Android 10; MAR-LX1A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile" + " Safari/537.36" + ), +] + +HEADER_TEMPLATES = [ + { + "accept": "*/*", + "connection": "keep-alive", + "accept-encoding": "gzip, deflate, br, zstd", + "User-Agent": ua, + } + for ua in USER_AGENTS +] + + +class BaseRotator(ABC, Generic[T]): + """ + A unified, thread-safe and coroutine-safe abstract base class for a + generic rotating data source. + + This class provides a dual API for both synchronous and asynchronous contexts. + - For synchronous, thread-safe operations, use methods like `next()`, `add()`. + - For asynchronous, coroutine-safe operations, use methods prefixed with 'a', + like `anext()`, `aadd()`. + + It uses a `threading.Lock` for thread safety and an `asyncio.Lock` for + coroutine safety. + + Attributes: + items (List[T]): The list of items to rotate through. + strategy (str): The rotation strategy in use. + """ + + def __init__( + self, + items: Optional[Iterable[T]] = None, + strategy: Literal["round_robin", "random", "weighted"] = "random", + ) -> None: + """ + Initializes the BaseRotator. + + Args: + items: An iterable of initial items. + strategy: The rotation strategy to use. + """ + self.items: List[T] = list(items or []) + self.strategy = strategy + self._iterator: Optional[Iterator[T]] = None + self._lock = threading.Lock() + self._async_lock: Optional[asyncio.Lock] = None + self._rebuild_iterator() + + @property + def async_lock(self) -> asyncio.Lock: + """ + Lazily creates and returns an `asyncio.Lock`. This ensures that the lock + is created within the correct event loop and avoids issues in + synchronous contexts or older Python versions (like 3.9). + """ + if self._async_lock is None: + self._async_lock = asyncio.Lock() + return self._async_lock + + @classmethod + def from_file( + cls: type[R], + source: Union[str, Path, list], + strategy: Literal["round_robin", "random", "weighted"] = "random", + ) -> R: + """ + Factory method to create a rotator from a file or a list. This method + is synchronous as it's typically used during setup. + """ + + items = [] + if isinstance(source, (str, Path)): + path = Path(source) + if not path.exists(): + raise FileNotFoundError(f"Source file not found: {path}") + + if path.suffix == ".json": + data = json.loads(path.read_text()) + items = [cls.rebuild_item(item) for item in data] + else: + lines = path.read_text().splitlines() + for line in lines: + line_content = line.split("#", 1)[0].strip() + if not line_content: + continue + items.append(cls.rebuild_item(line_content)) + elif isinstance(source, list): + items = [cls.rebuild_item(item) for item in source] + else: + raise RotatorError(f"Unsupported source type: {type(source)}") + + valid_items = [item for item in items if item is not None] + return cls(valid_items, strategy) + + @classmethod + @abstractmethod + def rebuild_item(cls, item: Any) -> Optional[T]: + """ + Abstract method to convert a raw item into a typed object. Must be + implemented by subclasses. + """ + raise NotImplementedError + + def _rebuild_iterator(self) -> None: + """ + Reconstructs the internal iterator. This is a core logic method and + should be called only after acquiring a lock. + """ + if not self.items: + self._iterator = None + return + + if self.strategy == "round_robin": + self._iterator = itertools.cycle(self.items) + elif self.strategy == "random": + self._iterator = None + elif self.strategy == "weighted": + weights = [getattr(item, "weight", 1.0) for item in self.items] + self._iterator = self._weighted_cycle(self.items, weights) + else: + raise ValueError(f"Unsupported strategy: {self.strategy}") + + def _weighted_cycle(self, items: List[T], weights: List[float]) -> Iterator[T]: + """Creates an infinite iterator that yields items based on weights.""" + while True: + yield random.choices(items, weights=weights, k=1)[0] + + def next(self, *args, **kwargs) -> T: + """ + Retrieves the next item using a thread-safe mechanism. + + Returns: + The next item from the collection. + + Raises: + ValueError: If the rotator contains no items. + """ + with self._lock: + if not self.items: + raise ValueError("Rotator is empty.") + if self.strategy == "random": + return random.choice(self.items) + if self._iterator is None: + self._rebuild_iterator() + assert self._iterator is not None + return next(self._iterator) + + def add(self, item: T) -> None: + """ + Adds a new item to the rotator in a thread-safe manner. + """ + with self._lock: + self.items.append(item) + self._rebuild_iterator() + + def remove(self, item: T) -> None: + """ + Removes an item from the rotator in a thread-safe manner. + """ + with self._lock: + self.items = [i for i in self.items if i != item] + self._rebuild_iterator() + + async def anext(self, *args, **kwargs) -> T: + """ + Retrieves the next item using a coroutine-safe mechanism. + + Returns: + The next item from the collection. + + Raises: + ValueError: If the rotator contains no items. + """ + async with self.async_lock: + if not self.items: + raise ValueError("Rotator is empty.") + if self.strategy == "random": + return random.choice(self.items) + if self._iterator is None: + self._rebuild_iterator() + assert self._iterator is not None + return next(self._iterator) + + async def aadd(self, item: T) -> None: + """ + Adds a new item to the rotator in a coroutine-safe manner. + """ + async with self.async_lock: + self.items.append(item) + self._rebuild_iterator() + + async def aremove(self, item: T) -> None: + """ + Removes an item from the rotator in a coroutine-safe manner. + """ + async with self.async_lock: + self.items = [i for i in self.items if i != item] + self._rebuild_iterator() + + def __len__(self) -> int: + return len(self.items) + + def __iter__(self) -> Iterator[T]: + return iter(self.items) + + +class ProxyRotator(BaseRotator[Proxy]): + """ + A unified rotator for managing `Proxy` objects, supporting both sync and + async operations. + """ + + @classmethod + def rebuild_item(cls, item: Any) -> Optional[Proxy]: + """Constructs a `Proxy` object from various input types.""" + try: + if isinstance(item, Proxy): + return item + if isinstance(item, dict): + return Proxy.from_dict(item) + if isinstance(item, str): + return Proxy.from_string(item) + except Exception: + return None + return None + + def mark_result(self, proxy: Proxy, success: bool, latency: Optional[float] = None) -> None: + """ + Thread-safely updates a proxy's performance statistics. + """ + with self._lock: + self._update_proxy_stats(proxy, success, latency) + + async def amark_result(self, proxy: Proxy, success: bool, latency: Optional[float] = None) -> None: + """ + Coroutine-safely updates a proxy's performance statistics. + """ + async with self.async_lock: + self._update_proxy_stats(proxy, success, latency) + + def _update_proxy_stats(self, proxy: Proxy, success: bool, latency: Optional[float] = None): + """Internal logic for updating proxy stats. Must be called from a locked context.""" + if success: + proxy.mark_success(latency) + else: + proxy.mark_failed() + + if self.strategy == "weighted": + self._rebuild_iterator() + + +class TLSIdentifierRotator(BaseRotator[IdentifierTypes]): + """ + A unified rotator for TLS Identifiers, supporting both sync and async operations. + """ + + def __init__( + self, + items: Optional[Iterable[IdentifierTypes]] = None, + strategy: Literal["round_robin", "random", "weighted"] = "round_robin", + ) -> None: + super().__init__(items or CLIENT_IDENTIFIER_TEMPLATES, strategy) # type: ignore[arg-type] + + @classmethod + def rebuild_item(cls, item: Any) -> Optional[IdentifierTypes]: + """Processes a raw item to be used as a TLS identifier.""" + if isinstance(item, str): + return item + return None + + +class HeaderRotator(BaseRotator[Headers]): + """ + A unified rotator for managing `Headers` objects, supporting both sync and + async operations. + + This rotator can dynamically update the 'User-Agent' header for each request + without modifying the original header templates. + + Examples: + >>> common_headers = { + ... "Accept": "application/json", + ... "Accept-Language": "en-US,en;q=0.9", + ... "User-Agent": "Default-Bot/1.0" + ... } + >>> mobile_headers = { + ... "Accept": "*/*", + ... "User-Agent": "Default-Mobile/1.0", + ... "X-Custom-Header": "mobile" + ... } + >>> + >>> rotator = HeaderRotator.from_file([common_headers, mobile_headers]) + >>> + >>> # Get headers without modification + >>> h1 = rotator.next() + >>> print(h1['User-Agent']) + Default-Bot/1.0 + >>> + >>> # Get headers with a new, dynamic User-Agent + >>> h2 = rotator.next(user_agent="My-Custom-UA/2.0") + >>> print(h2['User-Agent']) + My-Custom-UA/2.0 + >>> + >>> # The original header set remains unchanged + >>> h3 = rotator.next() + >>> print(h3['User-Agent']) + Default-Mobile/1.0 + """ + + def __init__( + self, + items: Optional[Iterable[HeaderTypes]] = None, + strategy: Literal["round_robin", "random", "weighted"] = "random", + ) -> None: + super().__init__(items or HEADER_TEMPLATES, strategy) # type: ignore[arg-type] + + @classmethod + def rebuild_item(cls, item: HeaderTypes) -> Optional[Headers]: + """ + Constructs a `Headers` object from various input types. + + It can process existing `Headers` objects, dictionaries, or lists of tuples. + + Args: + item: The raw data to convert into a `Headers` object. + + Returns: + A `Headers` instance, or None if the input is invalid. + """ + try: + if isinstance(item, Headers): + return item + return Headers(item) + except Exception: + return None + + def next(self, user_agent: Optional[str] = None, **kwargs) -> Headers: + """ + Retrieves the next `Headers` object in a thread-safe manner and + optionally updates its User-Agent. + + Args: + user_agent: If provided, this string will replace the 'User-Agent' + header in the returned object. + + Returns: + A copy of the next `Headers` object, potentially with a modified User-Agent. + """ + headers = super().next() + headers_copy = headers.copy() + if not isinstance(headers_copy, Headers): + headers_copy = Headers(headers_copy) + if user_agent: + headers_copy["User-Agent"] = user_agent + return headers_copy + + async def anext(self, user_agent: Optional[str] = None, **kwargs) -> Headers: + """ + Retrieves the next `Headers` object in a coroutine-safe manner and + optionally updates its User-Agent. + + Args: + user_agent: If provided, this string will replace the 'User-Agent' + header in the returned object. + + Returns: + A copy of the next `Headers` object, potentially with a modified User-Agent. + """ + headers = await super().anext() + headers_copy = headers.copy() + if not isinstance(headers_copy, Headers): + headers_copy = Headers(headers_copy) + if user_agent: + headers_copy["User-Agent"] = user_agent + return headers_copy diff --git a/tls_requests/models/status_codes.py b/src/tls_requests/models/status_codes.py similarity index 93% rename from tls_requests/models/status_codes.py rename to src/tls_requests/models/status_codes.py index 4fdac33..9bd016a 100644 --- a/tls_requests/models/status_codes.py +++ b/src/tls_requests/models/status_codes.py @@ -1,9 +1,13 @@ +from __future__ import annotations + from enum import Enum -__all__ = ["StatusCodes"] +__all__ = ("StatusCodes",) class StatusCodes(int, Enum): + reason: str + def __new__(cls, value: int, reason: str = ""): obj = int.__new__(cls, value) obj._value_ = value @@ -14,9 +18,11 @@ def __str__(self) -> str: return str(self.value) @classmethod - def get_reason(cls, value: int): + def get_reason(cls, value: int) -> str: if value in cls._value2member_map_: - return cls._value2member_map_[value].reason + member = cls._value2member_map_[value] + if isinstance(member, StatusCodes): + return member.reason return "Unknown Error" CONTINUE = 100, "Continue" diff --git a/tls_requests/models/tls.py b/src/tls_requests/models/tls.py similarity index 57% rename from tls_requests/models/tls.py rename to src/tls_requests/models/tls.py index 7b6271e..17692ce 100644 --- a/tls_requests/models/tls.py +++ b/src/tls_requests/models/tls.py @@ -1,25 +1,35 @@ +from __future__ import annotations + import ctypes +import re import uuid from dataclasses import asdict, dataclass, field from dataclasses import fields as get_fields -from typing import Any, List, Mapping, Optional, Set, TypeVar, Union - -from tls_requests.models.encoders import StreamEncoder -from tls_requests.models.libraries import TLSLibrary -from tls_requests.models.status_codes import StatusCodes -from tls_requests.settings import (DEFAULT_HEADERS, DEFAULT_TIMEOUT, - DEFAULT_TLS_DEBUG, DEFAULT_TLS_HTTP2, - DEFAULT_TLS_IDENTIFIER) -from tls_requests.types import (MethodTypes, TLSCookiesTypes, - TLSIdentifierTypes, TLSSessionId, URLTypes) -from tls_requests.utils import to_base64, to_bytes, to_json - -__all__ = [ +from typing import Any, Callable, Dict, List, Mapping, Optional, Set, TypeVar, Union + +from ..settings import ( + BROWSER_HEADERS, + DEFAULT_ALLOW_HTTP, + DEFAULT_CLIENT_IDENTIFIER, + DEFAULT_DEBUG, + DEFAULT_HTTP2, + DEFAULT_PROTOCOL_RACING, + DEFAULT_TIMEOUT, +) +from ..types import CookiesTypes, IdentifierTypes, MethodTypes, SessionId, URLTypes +from ..utils import to_base64, to_bytes, to_json +from .encoders import StreamEncoder +from .libraries import TLSLibrary +from .status_codes import StatusCodes + +__all__ = ( "TLSClient", "TLSResponse", "TLSConfig", "CustomTLSClientConfig", -] + "TLSRequestCookiesConfig", +) + T = TypeVar("T", bound="_BaseConfig") @@ -80,13 +90,13 @@ class TLSClient: >>> print(response) """ - _library = None - _getCookiesFromSession = None - _addCookiesToSession = None - _destroySession = None - _destroyAll = None - _request = None - _freeMemory = None + _library: Optional[Any] = None + _getCookiesFromSession: Optional[Callable] = None + _addCookiesToSession: Optional[Callable] = None + _destroySession: Optional[Callable] = None + _destroyAll: Optional[Callable] = None + _request: Optional[Callable] = None + _freeMemory: Optional[Callable] = None def __init__(self) -> None: if self._library is None: @@ -106,52 +116,63 @@ def initialize(cls): setattr(cls, fn_name, getattr(cls._library, name, None)) fn = getattr(cls, fn_name, None) if fn and callable(fn): - fn.argtypes = [ctypes.c_char_p] - fn.restype = ctypes.c_char_p + fn.argtypes = [ctypes.c_char_p] # type: ignore + fn.restype = ctypes.c_char_p # type: ignore cls._destroyAll = cls._library.destroyAll - cls._destroyAll.restype = ctypes.c_char_p + cls._destroyAll.restype = ctypes.c_char_p # type: ignore return cls() @classmethod - def get_cookies(cls, session_id: TLSSessionId, url: str) -> "TLSResponse": - response = cls._send( - cls._getCookiesFromSession, {"sessionId": session_id, "url": url} - ) + def get_cookies(cls, session_id: SessionId, url: str) -> "TLSResponse": + if cls._getCookiesFromSession is None: + cls.initialize() + response = cls._send(cls._getCookiesFromSession, {"sessionId": session_id, "url": url}) # type: ignore[arg-type] return response @classmethod - def add_cookies(cls, session_id: TLSSessionId, payload: dict): + def add_cookies(cls, session_id: SessionId, payload: dict): + if cls._addCookiesToSession is None: + cls.initialize() payload["sessionId"] = session_id return cls._send( - cls._addCookiesToSession, + cls._addCookiesToSession, # type: ignore[arg-type] payload, ) @classmethod def destroy_all(cls) -> bool: - response = TLSResponse.from_bytes(cls._destroyAll()) + if cls._destroyAll is None: + cls.initialize() + response = TLSResponse.from_bytes(cls._destroyAll()) # type: ignore[misc] if response.success: return True return False @classmethod - def destroy_session(cls, session_id: TLSSessionId) -> bool: - response = cls._send(cls._destroySession, {"sessionId": session_id}) + def destroy_session(cls, session_id: SessionId) -> bool: + if cls._destroySession is None: + cls.initialize() + response = cls._send(cls._destroySession, {"sessionId": session_id}) # type: ignore[arg-type] return response.success or False @classmethod def request(cls, payload): - return cls._send(cls._request, payload) + if cls._request is None: + cls.initialize() + return cls._send(cls._request, payload) # type: ignore[arg-type] @classmethod def free_memory(cls, response_id: str) -> None: - cls._freeMemory(to_bytes(response_id)) + if cls._freeMemory is None: + cls.initialize() + cls._freeMemory(to_bytes(response_id)) # type: ignore[misc] @classmethod def response(cls, raw: bytes) -> "TLSResponse": response = TLSResponse.from_bytes(raw) - cls.free_memory(response.id) + if response.id: + cls.free_memory(response.id) return response @classmethod @@ -162,14 +183,16 @@ async def aresponse(cls, raw: bytes): @classmethod async def arequest(cls, payload): - return await cls._aread(cls._request, payload) + if cls._request is None: + cls.initialize() + return await cls._aread(cls._request, payload) # type: ignore[arg-type] @classmethod - def _send(cls, fn: callable, payload: dict): + def _send(cls, fn: Callable, payload: dict): return cls.response(fn(to_bytes(payload))) @classmethod - async def _aread(cls, fn: callable, payload: dict): + async def _aread(cls, fn: Callable, payload: dict): return await cls.aresponse(fn(to_bytes(payload))) @@ -177,25 +200,35 @@ async def _aread(cls, fn: callable, payload: dict): class _BaseConfig: """Base configuration for TLSSession""" + _extra_config: dict = field(default_factory=dict, init=False, repr=False) + @classmethod def model_fields_set(cls) -> Set[str]: - return { - model_field.name - for model_field in get_fields(cls) - if not model_field.name.startswith("_") - } + return {model_field.name for model_field in get_fields(cls) if not model_field.name.startswith("_")} @classmethod - def from_kwargs(cls, **kwargs: Any) -> T: + def from_kwargs(cls: type[T], **kwargs: Any) -> T: model_fields_set = cls.model_fields_set() - return cls(**{k: v for k, v in kwargs.items() if k in model_fields_set and v}) + known_kwargs = {cls.to_camel_case(k): v for k, v in kwargs.items() if k in model_fields_set} + extra_kwargs = {cls.to_camel_case(k): v for k, v in kwargs.items() if k not in model_fields_set} + instance = cls(**known_kwargs) + instance._extra_config = extra_kwargs + return instance def to_dict(self) -> dict: - return {k: v for k, v in asdict(self).items() if not k.startswith("_")} + data = asdict(self) + if hasattr(self, "_extra_config"): + data.update(self._extra_config) + return {k: v for k, v in data.items() if not k.startswith("_") and v is not None} def to_payload(self) -> dict: return self.to_dict() + @classmethod + def to_camel_case(cls, name: str) -> str: + """Convert a string to camelCase.""" + return "".join(word.capitalize() if i > 0 else word for i, word in enumerate(name.split("_"))) + @dataclass class TLSResponse(_BaseConfig): @@ -223,18 +256,19 @@ class TLSResponse(_BaseConfig): id: Optional[str] = None sessionId: Optional[str] = None - status: Optional[int] = 0 + status: int = 0 target: Optional[str] = None body: Optional[str] = None - headers: Optional[dict] = field(default_factory=dict) - cookies: Optional[dict] = field(default_factory=dict) - success: Optional[bool] = False - usedProtocol: Optional[str] = "HTTP/1.1" + headers: Dict[str, Any] = field(default_factory=dict) + cookies: Dict[str, Any] = field(default_factory=dict) + success: bool = False + usedProtocol: str = "HTTP/1.1" @classmethod def from_bytes(cls, raw: bytes) -> "TLSResponse": with StreamEncoder.from_bytes(raw) as stream: - return cls.from_kwargs(**to_json(b"".join(stream))) + payload = b"".join(stream) + return cls.from_kwargs(**to_json(payload)) @property def reason(self) -> str: @@ -341,19 +375,19 @@ class CustomTLSClientConfig(_BaseConfig): """ - alpnProtocols: List[str] = None - alpsProtocols: List[str] = None - certCompressionAlgo: str = None - connectionFlow: int = None - h2Settings: List[str] = None - h2SettingsOrder: List[str] = None - headerPriority: List[str] = None - ja3String: str = None - keyShareCurves: List[str] = None - priorityFrames: List[str] = None - pseudoHeaderOrder: List[str] = None - supportedSignatureAlgorithms: List[str] = None - supportedVersions: List[str] = None + alpnProtocols: Optional[List[str]] = None + alpsProtocols: Optional[List[str]] = None + certCompressionAlgo: Optional[str] = None + connectionFlow: Optional[int] = None + h2Settings: Optional[Dict[str, int]] = None + h2SettingsOrder: Optional[List[str]] = None + headerPriority: Optional[List[str]] = None + ja3String: Optional[str] = None + keyShareCurves: Optional[List[str]] = None + priorityFrames: Optional[List[str]] = None + pseudoHeaderOrder: Optional[List[str]] = None + supportedSignatureAlgorithms: Optional[List[str]] = None + supportedVersions: Optional[List[str]] = None @dataclass @@ -435,15 +469,18 @@ class TLSConfig(_BaseConfig): isByteResponse: bool = True isRotatingProxy: bool = False proxyUrl: str = "" - requestBody: Union[str, bytes, bytearray, None] = None + requestBody: Union[str, bytes, bytearray, Optional[None]] = None requestCookies: List[TLSRequestCookiesConfig] = field(default_factory=list) - requestMethod: MethodTypes = None + requestMethod: Optional[MethodTypes] = None requestUrl: Optional[str] = None sessionId: str = field(default_factory=lambda: str(uuid.uuid4())) + streamID: Optional[int] = None timeoutSeconds: int = 30 - tlsClientIdentifier: Optional[TLSIdentifierTypes] = DEFAULT_TLS_IDENTIFIER - withDebug: bool = False + tlsClientIdentifier: Optional[IdentifierTypes] = DEFAULT_CLIENT_IDENTIFIER + withAllowHTTP: bool = DEFAULT_ALLOW_HTTP + withDebug: bool = DEFAULT_DEBUG withDefaultCookieJar: bool = False + withProtocolRacing: bool = DEFAULT_PROTOCOL_RACING withRandomTLSExtensionOrder: bool = True withoutCookieJar: bool = False @@ -457,99 +494,169 @@ def to_dict(self) -> dict: if self.requestBody and isinstance(self.requestBody, (bytes, bytearray)): self.isByteRequest = True self.requestBody = to_base64(self.requestBody) + elif self.requestBody: + self.isByteRequest = False else: self.isByteRequest = False self.requestBody = None self.timeoutSeconds = ( - int(self.timeoutSeconds) - if isinstance(self.timeoutSeconds, (float, int)) - else DEFAULT_TIMEOUT + int(self.timeoutSeconds) if isinstance(self.timeoutSeconds, (float, int)) else DEFAULT_TIMEOUT ) - return asdict(self) + return super().to_dict() def copy_with( self, - session_id: str = None, - headers: Mapping[str, str] = None, - cookies: TLSCookiesTypes = None, - method: MethodTypes = None, - url: URLTypes = None, - body: Union[str, bytes, bytearray] = None, - is_byte_request: bool = None, - proxy: str = None, - http2: bool = None, - timeout: Union[float, int] = None, - verify: bool = None, - tls_identifier: Optional[TLSIdentifierTypes] = None, - tls_debug: bool = None, - **kwargs, + session_id: Optional[str] = None, + headers: Optional[Mapping[str, str]] = None, + cookies: Optional[CookiesTypes] = None, + method: Optional[MethodTypes] = None, + url: Optional[URLTypes] = None, + body: Optional[Union[str, bytes, bytearray]] = None, + is_byte_request: Optional[bool] = None, + proxy: Optional[str] = None, + http2: Optional[bool] = None, + timeout: Optional[Union[float, int]] = None, + verify: Optional[bool] = None, + client_identifier: Optional[IdentifierTypes] = None, + debug: Optional[bool] = None, + protocol_racing: Optional[bool] = None, + allow_http: Optional[bool] = None, + stream_id: Optional[int] = None, + **kwargs: Any, ) -> "TLSConfig": """Creates a new `TLSConfig` object with updated properties.""" - kwargs.update( - dict( - sessionId=session_id, - headers=headers, - requestCookies=cookies, - requestMethod=method, - requestUrl=url, - requestBody=body, - isByteRequest=is_byte_request, - proxyUrl=proxy, - forceHttp1=not http2, - timeoutSeconds=timeout, - insecureSkipVerify=not verify, - tlsClientIdentifier=tls_identifier, - withDebug=tls_debug, - ) - ) + mapping = { + "sessionId": session_id, + "headers": headers, + "requestCookies": cookies, + "requestMethod": method, + "requestUrl": url, + "requestBody": body, + "isByteRequest": is_byte_request, + "proxyUrl": proxy, + "timeoutSeconds": timeout, + "insecureSkipVerify": None if verify is None else not verify, + "tlsClientIdentifier": client_identifier, + "withDebug": debug, + "withProtocolRacing": protocol_racing, + "withAllowHTTP": allow_http, + "streamID": stream_id, + } + if http2 is not None: + mapping["forceHttp1"] = not http2 + + # Filter out None values to avoid overwriting existing config with defaults + filtered_mapping = {k: v for k, v in mapping.items() if v is not None} + kwargs.update(filtered_mapping) + current_kwargs = asdict(self) - for k, v in current_kwargs.items(): - if kwargs.get(k) is not None: - current_kwargs[k] = kwargs[k] + if hasattr(self, "_extra_config"): + current_kwargs.update(self._extra_config) + + for k, v in kwargs.items(): + current_kwargs[k] = v - return self.__class__(**current_kwargs) + return super().from_kwargs(**current_kwargs) @classmethod def from_kwargs( cls, - session_id: str = None, - headers: Mapping[str, str] = None, - cookies: TLSCookiesTypes = None, - method: MethodTypes = None, - url: URLTypes = None, - body: Union[str, bytes, bytearray] = None, + session_id: Optional[str] = None, + headers: Optional[Mapping[str, str]] = None, + cookies: Optional[CookiesTypes] = None, + method: Optional[MethodTypes] = None, + url: Optional[URLTypes] = None, + body: Optional[Union[str, bytes, bytearray]] = None, is_byte_request: bool = False, - proxy: str = None, - http2: bool = DEFAULT_TLS_HTTP2, + proxy: Optional[str] = None, + http2: Optional[Union[bool, str]] = DEFAULT_HTTP2, timeout: Union[float, int] = DEFAULT_TIMEOUT, verify: bool = True, - tls_identifier: Optional[TLSIdentifierTypes] = DEFAULT_TLS_IDENTIFIER, - tls_debug: bool = DEFAULT_TLS_DEBUG, + client_identifier: Optional[IdentifierTypes] = None, + debug: bool = DEFAULT_DEBUG, + protocol_racing: bool = DEFAULT_PROTOCOL_RACING, + allow_http: bool = DEFAULT_ALLOW_HTTP, + stream_id: Optional[int] = None, **kwargs: Any, ) -> "TLSConfig": """Creates a `TLSConfig` instance from keyword arguments.""" - kwargs.update( - dict( - sessionId=session_id, - headers=dict(headers) if headers else DEFAULT_HEADERS, - requestCookies=cookies or [], - requestMethod=method, - requestUrl=url, - requestBody=body, - isByteRequest=is_byte_request, - proxyUrl=proxy, - forceHttp1=bool(not http2), - timeoutSeconds=( - int(timeout) - if isinstance(timeout, (float, int)) - else DEFAULT_TIMEOUT - ), - insecureSkipVerify=not verify, - tlsClientIdentifier=tls_identifier, - withDebug=tls_debug, - ) - ) + # 1. Handle Snake Case Aliases + if client_identifier is not None: + kwargs.setdefault("tlsClientIdentifier", client_identifier) + if debug is not None: + kwargs.setdefault("withDebug", debug) + if protocol_racing is not None: + kwargs.setdefault("withProtocolRacing", protocol_racing) + if allow_http is not None: + kwargs.setdefault("withAllowHTTP", allow_http) + if stream_id is not None: + kwargs.setdefault("streamID", stream_id) + + # 2. Resolve Identifier (Prioritize explicit arg, then kwargs, then default) + identifier = client_identifier or kwargs.get("tlsClientIdentifier") or DEFAULT_CLIENT_IDENTIFIER + identifier_str = str(identifier).lower() + + # 3. Dynamic Header Mapping based on identifier + # Resolve Headers (Prioritize explicit arg, then kwargs) + resolved_headers = headers if headers is not None else kwargs.get("headers") + + injected_headers = {} + if not resolved_headers: # Only inject if headers are missing or empty + for browser, browser_headers in BROWSER_HEADERS.items(): + if browser in identifier_str: + injected_headers = browser_headers.copy() + + # 4. Dynamic Version Replacement + if browser == "chrome": + match = re.search(r"chrome_(\d+)", identifier_str) + if match: + version = match.group(1) + ua = injected_headers.get("user-agent", "") + injected_headers["user-agent"] = re.sub(r"Chrome/\d+", f"Chrome/{version}", ua) + if "sec-ch-ua" in injected_headers: + val = injected_headers["sec-ch-ua"] + injected_headers["sec-ch-ua"] = val.replace("133", version) + elif browser == "firefox": + match = re.search(r"firefox_(\d+)", identifier_str) + if match: + version = match.group(1) + ua = injected_headers.get("user-agent", "") + # Firefox has version in two places: rv:XX and Firefox/XX + ua = re.sub(r"rv:\d+", f"rv:{version}", ua) + injected_headers["user-agent"] = re.sub(r"Firefox/\d+", f"Firefox/{version}", ua) + elif browser == "safari": + match = re.search(r"safari_ios_(\d+)", identifier_str) or re.search( + r"safari_(\d+)", identifier_str + ) + if match: + version = match.group(1) + ua = injected_headers.get("user-agent", "") + injected_headers["user-agent"] = re.sub(r"Version/\d+", f"Version/{version}", ua) + break + + defaults = { + "sessionId": session_id, + "headers": dict(resolved_headers) if resolved_headers else injected_headers, + "requestCookies": cookies or [], + "requestMethod": method, + "requestUrl": url, + "requestBody": body, + "isByteRequest": is_byte_request, + "proxyUrl": proxy, + "forceHttp1": bool(not http2), + "timeoutSeconds": (int(timeout) if isinstance(timeout, (float, int)) else DEFAULT_TIMEOUT), + "insecureSkipVerify": not verify, + "tlsClientIdentifier": identifier, + "withDebug": debug, + "withProtocolRacing": protocol_racing, + "withAllowHTTP": allow_http, + "streamID": stream_id, + } + + for key, value in defaults.items(): + kwargs.setdefault(key, value) + return super().from_kwargs(**kwargs) diff --git a/src/tls_requests/models/urls.py b/src/tls_requests/models/urls.py new file mode 100644 index 0000000..f374250 --- /dev/null +++ b/src/tls_requests/models/urls.py @@ -0,0 +1,761 @@ +from __future__ import annotations + +import ipaddress +import time +from collections.abc import Mapping, MutableMapping +from typing import Any, Dict, ItemsView, Iterator, KeysView, Optional, Union, ValuesView +from urllib.parse import ParseResult, quote, unquote, urlencode, urlparse + +import idna + +from ..exceptions import ProxyError, URLError, URLParamsError +from ..types import URL_ALLOWED_PARAMS, URLParamTypes, URLTypes +from ..utils import to_str + +__all__ = ( + "URL", + "URLParams", + "Proxy", +) + + +class URLParams(MutableMapping): + """ + A mapping-like object for managing URL query parameters. + + This class provides a dictionary-like interface for URL parameters, + handling the normalization of keys and values into the correct string format + and encoding them into a query string. It supports multi-value parameters. + + Attributes: + params (str): The URL-encoded query string representation of the parameters. + + Examples: + >>> params = URLParams({'key1': 'value1', 'key2': ['value2', 'value3']}) + >>> print(str(params)) + 'key1=value1&key2=value2&key2=value3' + + >>> params.update({'key3': 4, 'active': True}) + >>> print(params) + 'key1=value1&key2=value2&key2=value3&key3=4&active=true' + + >>> 'key1' in params + True + """ + + def __init__(self, params: Optional[URLParamTypes] = None, **kwargs): + """ + Initializes the URLParams object. + + Args: + params: A dictionary, another URLParams instance, or a list of tuples + to initialize the parameters. + **kwargs: Additional key-value pairs to add or overwrite parameters. + + Raises: + URLParamsError: If `params` is not a valid mapping type. + """ + self._data: Dict[str, Any] = self._prepare(params, **kwargs) + + @property + def params(self) -> str: + """Returns the encoded URL parameters as a query string.""" + return str(self) + + def update(self, params: Optional[URLParamTypes] = None, **kwargs: Any) -> None: # type: ignore[override] + """ + Updates the current parameters with new ones from a mapping or keyword args. + + Args: + params: A dictionary-like object of parameters to add. + **kwargs: Additional key-value pairs to add. + """ + self._data.update(self._prepare(params, **kwargs)) + + def keys(self) -> KeysView: + """Returns a view of the parameter keys.""" + return self._data.keys() + + def values(self) -> ValuesView: + """Returns a view of the parameter values.""" + return self._data.values() + + def items(self) -> ItemsView: + """Returns a view of the parameter key-value pairs.""" + return self._data.items() + + def copy(self) -> URLParams: + """Returns a shallow copy of the current instance.""" + return self.__class__(self._data.copy()) # type: ignore[arg-type] + + def __str__(self): + """Returns the URL-encoded string representation of the parameters.""" + return urlencode(self._data, doseq=True) + + def __repr__(self): + """Returns the official string representation of the object.""" + return "<%s: %s>" % (self.__class__.__name__, self.items()) + + def __contains__(self, key: Any) -> bool: + """Checks if a key exists in the parameters.""" + return key in self._data + + def __setitem__(self, key: str, value: Any) -> None: + """Sets a parameter key-value pair, normalizing the input.""" + self._data[key] = value + + def __getitem__(self, key: str) -> Any: + """Retrieves a parameter value.""" + return self._data[key] + + def __delitem__(self, key: str) -> None: + """Deletes a parameter key.""" + del self._data[key] + + def __iter__(self) -> Iterator: + """Returns an iterator over the parameter keys.""" + return iter(self._data) + + def __len__(self) -> int: + """Returns the number of parameters.""" + return len(self._data) + + def __hash__(self) -> int: + """Returns the hash of the encoded parameter string.""" + return hash(str(self)) + + def __eq__(self, other) -> bool: + """Checks for equality based on the encoded parameter string.""" + if not isinstance(other, self.__class__): + if isinstance(other, Mapping): + other = self.__class__(other) + else: + return False + return bool(self.params == other.params) + + def _prepare(self, params: Optional[URLParamTypes] = None, **kwargs: Any) -> Dict[str, Any]: + """ + Normalizes and prepares the input parameters. + + Args: + params: A dictionary-like object of parameters. + **kwargs: Additional keyword arguments. + + Returns: + A dictionary with normalized keys and values. + + Raises: + URLParamsError: If keys or values are of an invalid type. + """ + if params is None: + prepared = {} + elif isinstance(params, self.__class__): + prepared = dict(params.items()) + elif isinstance(params, Mapping): + prepared = dict(params) + else: + raise URLParamsError("Invalid parameters.") + + prepared.update(kwargs) + result = {} + for k, v in prepared.items(): + if not isinstance(k, (str, bytes)): + raise URLParamsError("Invalid parameters key type.") + + if isinstance(v, (list, tuple, set)): + v = [self.normalize(s) for s in v] + else: + v = self.normalize(v) + + result[self.normalize(k)] = v + return result + + def normalize(self, s: URL_ALLOWED_PARAMS): + """ + Converts a supported type into a string. + + Args: + s: The value to normalize (str, bytes, int, float, bool). + + Returns: + The normalized string value. + + Raises: + URLParamsError: If the value type is not supported. + """ + if not isinstance(s, (str, bytes, int, float, bool)): + raise URLParamsError("Invalid parameters value type.") + + if isinstance(s, bool): + return str(s).lower() + + if isinstance(s, bytes): + return s.decode("utf-8") + + return str(s) + + +class URL: + """ + A class for parsing, manipulating, and constructing URLs. + + This class provides a structured way to interact with URL components, + integrating with `URLParams` for easy query string management. It handles + IDNA encoding for hostnames and ensures proper URL construction. + + Attributes: + url (str): The full URL string. Can be set to re-parse. + params (URLParams): An object managing the URL's query parameters. + parsed (ParseResult): The result from `urllib.parse.urlparse`. + scheme (str): The URL scheme (e.g., "https"). + netloc (str): The network location part (e.g., "user:pass@host:port"). + host (str): The hostname, IDNA-encoded. + port (str): The port number as a string, if present. + path (str): The hierarchical path. + query (str): The complete query string, combining original and added params. + fragment (str): The fragment identifier. + username (str): The username for authentication. + password (str): The password for authentication. + auth (tuple): A (username, password) tuple. + + Examples: + >>> url = URL("https://example.com/path?q=1#fragment", params={"key": "value"}) + >>> print(url.scheme) + 'https' + >>> print(url.host) + 'example.com' + >>> print(url.query) + 'q=1&key=value' + + >>> url.params.update({'key2': 'value2'}) + >>> print(unquote(url.url)) + 'https://example.com/path?q=1&key=value&key2=value2#fragment' + + >>> url.url = 'https://httpbin.org/get' + >>> print(unquote(url.url)) + 'https://httpbin.org/get?key=value&key2=value2' + """ + + __attrs__ = ( + "auth", + "scheme", + "host", + "port", + "path", + "fragment", + "username", + "password", + ) + + def __init__(self, url: URLTypes, params: URLParamTypes = None, **kwargs): + """ + Initializes the URL object. + + Args: + url: The URL string, bytes, or another URL object. + params: A dictionary-like object to be used as query parameters. + **kwargs: Additional keyword arguments for URLParams. + + Raises: + URLError: If the provided URL is invalid. + """ + self._parsed = self._prepare(url) + self._url: Optional[str] = None + self._params = URLParams(params) + + @property + def url(self): + """The full, reconstructed URL string.""" + if self._url is None: + self._url = self._build(False) + return self._url + + @url.setter + def url(self, value): + """Allows setting a new URL, which will be parsed.""" + self._parsed = self._prepare(value) + self._url = self._build(False) + + @property + def params(self): + """The `URLParams` object for managing query parameters.""" + return self._params + + @params.setter + def params(self, value): + """Sets a new `URLParams` object.""" + self._url = None + self._params = URLParams(value) + + @property + def parsed(self) -> ParseResult: + """The `ParseResult` object from the standard library.""" + return self._parsed + + @property + def netloc(self) -> str: + """The network location, including host and port.""" + host = self.host + if ":" in host and not (host.startswith("[") and host.endswith("]")): + host = "[%s]" % host + return ":".join([host, self.port]) if self.port else host + + @property + def query(self) -> str: + """The combined query string from the original URL and the `params`.""" + query = "" + if self.parsed.query and self.params.params: + query = "&".join([quote(self.parsed.query), self.params.params]) + elif self.params.params: + query = self.params.params + elif self.parsed.query: + query = self.parsed.query + return query + + def __str__(self): + """Returns the full URL string with the real password.""" + return self._build() + + def __repr__(self): + """Returns a representation of the URL with a secured password.""" + return "<%s: %s>" % (self.__class__.__name__, unquote(self._build(True))) + + def _prepare(self, url: Union["URL", str, bytes]) -> ParseResult: + """ + Validates, decodes, and parses the input URL. + + Args: + url: The URL to prepare. + + Returns: + A `ParseResult` object. + + Raises: + URLError: For invalid URL types or formats. + """ + if isinstance(url, bytes): + url = url.decode("utf-8") + elif isinstance(url, self.__class__) or issubclass(self.__class__, url.__class__): + url = str(url) + + if not isinstance(url, str): + raise URLError(f"Invalid URL: {url}") + + url_to_parse = url.lstrip() + + # 0. Pre-parsing: default to http if scheme is missing + if "://" not in url_to_parse and not url_to_parse.startswith("/") and not url_to_parse.startswith("./"): + # Check if it doesn't look like a potential relative URL with query/fragment + if not (url_to_parse.startswith("?") or url_to_parse.startswith("#")): + url_to_parse = f"http://{url_to_parse}" + + # 1. Pre-parsing repair for raw IPv6 addresses + if ":" in url_to_parse: + # Extract authority candidate: part between scheme and path + if "://" in url_to_parse: + authority_candidate = url_to_parse.split("://", 1)[1].split("/", 1)[0].split("?", 1)[0].split("#", 1)[0] + else: + authority_candidate = url_to_parse.split("/", 1)[0].split("?", 1)[0].split("#", 1)[0] + + # Extract host part (ignoring user:pass@) + host_candidate = authority_candidate.rsplit("@", 1)[-1] + + # If it looks like IPv6 but lacks brackets + if host_candidate.count(":") > 1 and not (host_candidate.startswith("[") and "]" in host_candidate): + # Try to determine if it's IP:PORT or just IP + # We prioritize IP:PORT if the last part is digits + possible_ips = [] + h_p, _, port = host_candidate.rpartition(":") + if port.isdigit() and h_p.count(":") >= 1: + possible_ips.append((h_p, port)) + possible_ips.append((host_candidate, "")) + + for ip, p_val in possible_ips: + try: + ipaddress.IPv6Address(ip) + repaired = f"[{ip}]" + if p_val: + repaired += f":{p_val}" + url_to_parse = url_to_parse.replace(host_candidate, repaired, 1) + break + except ValueError: + continue + + for attr in self.__attrs__: + setattr(self, attr, None) + + # 2. Parse and Validate + try: + # First, check for malformed brackets in the string we're about to parse + # We strictly enforce one '[' and one ']' in the authority if any exist + authority = "" + if "://" in url_to_parse: + authority = url_to_parse.split("://", 1)[1].split("/", 1)[0] + else: + authority = url_to_parse.split("/", 1)[0] + + if "[" in authority or "]" in authority: + if authority.count("[") != 1 or authority.count("]") != 1: + raise ValueError("Malformed bracketed host") + + start = authority.find("[") + end = authority.find("]") + if start > end: + raise ValueError("Invalid bracket order") + + # Content inside brackets MUST be a valid IPv6 + ip_content = authority[start + 1 : end] + try: + ipaddress.IPv6Address(ip_content) + except ValueError: + raise ValueError(f"Invalid IPv6 in brackets: {ip_content}") + + parsed = urlparse(url_to_parse) + + except (ValueError, AttributeError) as e: + raise URLError(f"Invalid URL: {url}. {str(e)}") from e + + self.auth = parsed.username, parsed.password + self.scheme = parsed.scheme + + # Handle Hostname (Supports IPv4, IPv6 and IDNA Domain) + hostname = (parsed.hostname or "").lower() + if not hostname: + self.host = "" + else: + try: + # If hostname is an IP address, keep the format as is + ipaddress.ip_address(hostname) + self.host = hostname + except ValueError: + # If not an IP, apply IDNA encoding for domain names + try: + self.host = idna.encode(hostname).decode("ascii") + except idna.IDNAError: + raise URLError(f"Invalid IDNA hostname: {hostname}") + + self.port = "" + try: + if parsed.port: + self.port = str(parsed.port) + except ValueError as e: + raise URLError(f"{e.args[0]}. port range must be 0 - 65535.") + + self.path = parsed.path + self.fragment = parsed.fragment + self.username = parsed.username or "" + self.password = parsed.password or "" + return parsed + + def _build(self, secure: bool = False) -> str: + """ + Constructs the URL string from its components. + + Args: + secure: If True, masks the password in the output. + + Returns: + The final URL string. + """ + scheme = self.scheme or "" + urls = [scheme, "://"] if scheme else [] + authority = self.netloc + if self.username or self.password: + username = self.username or "" + password = self.password or "" + if secure: + password = "[secure]" + + authority = "@".join( + [ + ":".join([username, password]), + self.netloc, + ] + ) + + urls.append(authority) + path = self.path or "" + if self.query: + urls.append("?".join([path, self.query])) + else: + urls.append(path) + + if self.fragment: + urls.append("#" + self.fragment) + + return "".join(urls) + + +class Proxy(URL): + """ + A specialized URL class for managing proxy configurations and performance. + + This class inherits from `URL` and extends it with features for proxy + rotation strategies, such as weighting, success/failure tracking, and + metadata. It restricts the allowed URL schemes to those common for proxies. + + Attributes: + ALLOWED_SCHEMES (tuple): Allowed proxy schemes ('http', 'https', 'socks5', 'socks5h'). + weight (float): The weight of the proxy, used in selection algorithms. + region (Optional[str]): A geographical or logical region identifier. + latency (Optional[float]): The last recorded latency in seconds. + success_rate (Optional[float]): A score indicating reliability (0.0 to 1.0). + meta (Dict[str, Any]): A dictionary for arbitrary user-defined data. + failures (int): A counter for consecutive connection failures. + last_used (Optional[float]): A timestamp of the last time the proxy was used. + + Examples: + >>> proxy = Proxy("http://user:pass@127.0.0.1:8080", weight=5.0, region="us-east") + >>> proxy.mark_failed() + >>> print(proxy.failures) + 1 + >>> print(proxy.weight) + 4.25 + >>> proxy.mark_success() + >>> data = proxy.to_dict() + >>> print(data['url']) + 'http://user:pass@127.0.0.1:8080' + """ + + ALLOWED_SCHEMES = ("http", "https", "socks5", "socks5h") + + def __init__( + self, + url: URLTypes, + params: URLParamTypes = None, + *, + weight: float = 1.0, + region: Optional[str] = None, + latency: Optional[float] = None, + success_rate: Optional[float] = None, + meta: Optional[Dict[str, Any]] = None, + **kwargs, + ): + """ + Initializes the Proxy object. + + Args: + url: The proxy URL string, bytes, or another URL object. + params: URL parameters (rarely used for proxies). + weight: The initial weight for proxy selection (higher is more likely). + region: An identifier for the proxy's region. + latency: The initial or last known latency in seconds. + success_rate: A score from 0.0 to 1.0 indicating reliability. + meta: A dictionary for storing arbitrary user data. + **kwargs: Additional keyword arguments passed to the parent `URL` class. + + Raises: + ProxyError: If the URL is invalid or the scheme is not supported. + """ + self._weight = weight or 1.0 + self.region = region + self.latency = latency + self.success_rate = success_rate + self.meta = meta or {} + self.failures: int = 0 + self.last_used: Optional[float] = None + super().__init__(url, **kwargs) + + def __repr__(self): + """Returns a secure representation of the proxy with its weight.""" + return "<%s: %s, weight=%s>" % ( + self.__class__.__name__, + unquote(self._build(True)), + getattr(self, "weight", "unset"), + ) + + @property + def weight(self) -> float: + return self._weight or 1.0 + + @weight.setter + def weight(self, weight: float) -> None: + try: + self._weight = float(weight) + except ValueError: + raise ProxyError("Weight must be an integer or float.") + + def _prepare(self, url: Union["URL", str, bytes]) -> ParseResult: + """ + Parses the proxy URL, ensuring it has a valid scheme and format. + + Overrides the parent `_prepare` to enforce proxy-specific rules. + + Args: + url: The proxy URL to prepare. + + Returns: + A `ParseResult` object containing only scheme and netloc. + + Raises: + ProxyError: If the URL is invalid or the scheme is not allowed. + """ + try: + if isinstance(url, bytes): + url_str = url.decode("utf-8") + else: + url_str = str(url) + + url_str = url_str.strip() + + if "://" not in url_str: + url_str = f"http://{url_str}" + + parsed = super(Proxy, self)._prepare(url_str) + if str(parsed.scheme).lower() not in self.ALLOWED_SCHEMES: + raise ProxyError( + f"Invalid proxy scheme `{parsed.scheme}`. The allowed schemes are ('http', 'https', 'socks5', 'socks5h')." + ) + + # Re-parse to create a clean object with only scheme and netloc + parsed = urlparse(str(to_str(parsed.scheme)) + "://" + str(to_str(parsed.netloc))) + self.path = "" + self.fragment = "" + return parsed + except URLError: + raise ProxyError(f"Invalid proxy: {to_str(url)}") + + def _build(self, secure: bool = False) -> str: + """ + Constructs the proxy URL string. + + Overrides the parent `_build` to exclude path, query, and fragment. + + Args: + secure: If True, masks the password. + + Returns: + The proxy URL string. + """ + urls = [self.scheme or "http", "://"] + authority = self.netloc + if self.username or self.password: + userinfo = ":".join([self.username, self.password]) + if secure: + userinfo = "[secure]" + + authority = "@".join( + [ + userinfo, + self.netloc, + ] + ) + + urls.append(authority) + return "".join(urls) + + def mark_used(self): + """Sets the `last_used` timestamp to the current time.""" + self.last_used = time.time() + + def mark_failed(self): + """ + Records a connection failure. + + Increments the failure count and applies a decay factor to the weight. + """ + self.failures += 1 + self.weight = max(0.1, self.weight * 0.85) + + def mark_success(self, latency: Optional[float] = None): + """ + Records a connection success. + + Resets failure count, updates latency, and improves success rate and weight. + + Args: + latency: The observed connection latency in seconds for this success. + """ + if latency: + self.latency = latency + + self.failures = max(0, self.failures - 1) + self.success_rate = (self.success_rate or 1.0) * 0.95 + 0.05 + self.weight = min(10.0, self.weight * 1.05) + + def to_dict(self): + """ + Serializes the proxy's state to a dictionary. + + Returns: + A dictionary containing the proxy's URL and performance metrics. + """ + return { + "url": self.url, + "weight": self.weight, + "region": self.region, + "latency": self.latency, + "success_rate": self.success_rate, + "last_used": self.last_used, + } + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "Proxy": + """ + Creates a Proxy object from a dictionary. + + Args: + data: A dictionary containing a 'url' key and other optional + proxy attributes (`weight`, `region`, etc.). + + Returns: + A new `Proxy` instance. + + Raises: + ProxyError: If the 'url' key is missing from the dictionary. + """ + if "url" not in data: + raise ProxyError("Missing required key: 'url'. The proxy configuration dictionary must include a 'url'.") + + url = data.pop("url") + return cls( + url=url, + **data, + ) + + @classmethod + def from_string(cls, raw: str, separator: str = "|") -> "Proxy": + """ + Parses a proxy from a string with optional attributes. + + Handles various common formats for representing proxies in text files. + Comments (#) and blank lines should be handled by the calling code. + + Supported Formats: + - `http://user:pass@host:port` + - `socks5://host:port` + - `host:port` (defaults to http) + - `host:port|weight` + - `host:port|weight|region` + - `http://user:pass@host:port|weight|region` + + Args: + raw: The raw proxy string. + separator: The character used to separate attributes (default: '|'). + + Returns: + A new `Proxy` instance. + + Raises: + ProxyError: If the proxy string is empty or malformed. + """ + raw = raw.strip() + if not raw: + raise ProxyError("Empty proxy string.") + + parts = [p.strip() for p in raw.split(separator)] + url = parts[0] + weight = 1.0 + region = None + if len(parts) >= 2 and parts[1]: + try: + weight = float(parts[1]) + except Exception: + pass + if len(parts) >= 3 and parts[2]: + region = parts[2] + + if "://" not in url: + url = "http://" + url + + return cls(url, weight=weight, region=region) diff --git a/src/tls_requests/settings.py b/src/tls_requests/settings.py new file mode 100644 index 0000000..472c154 --- /dev/null +++ b/src/tls_requests/settings.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from typing import Dict, Literal + +CHUNK_SIZE: int = 65_536 +DEFAULT_TIMEOUT: float = 30.0 +DEFAULT_MAX_REDIRECTS: int = 9 +DEFAULT_FOLLOW_REDIRECTS: bool = True +DEFAULT_DEBUG: bool = False +DEFAULT_PROTOCOL_RACING: bool = True +DEFAULT_ALLOW_HTTP: bool = False +DEFAULT_INSECURE_SKIP_VERIFY: bool = False +DEFAULT_HTTP2: Literal["auto", "http1", "http2"] = "auto" +DEFAULT_CLIENT_IDENTIFIER: str = "chrome_133" + +BROWSER_HEADERS: Dict[str, Dict[str, str]] = { + "chrome": { + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "accept-language": "en-US,en;q=0.9", + "accept-encoding": "gzip, deflate, br, zstd", + "sec-ch-ua": '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "document", + "sec-fetch-mode": "navigate", + "sec-fetch-site": "none", + "sec-fetch-user": "?1", + "upgrade-insecure-requests": "1", + }, + "firefox": { + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0", + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "accept-language": "en-US,en;q=0.5", + "accept-encoding": "gzip, deflate, br, zstd", + "upgrade-insecure-requests": "1", + "sec-fetch-dest": "document", + "sec-fetch-mode": "navigate", + "sec-fetch-site": "none", + "sec-fetch-user": "?1", + }, + "safari": { + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15", + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "accept-language": "en-US,en;q=0.9", + "accept-encoding": "gzip, deflate, br", + "sec-fetch-dest": "document", + "sec-fetch-mode": "navigate", + "sec-fetch-site": "none", + }, +} diff --git a/tls_requests/types.py b/src/tls_requests/types.py similarity index 64% rename from tls_requests/types.py rename to src/tls_requests/types.py index ffe0b35..c41dbc5 100644 --- a/tls_requests/types.py +++ b/src/tls_requests/types.py @@ -2,13 +2,42 @@ Type definitions for type checking purposes. """ +from __future__ import annotations + from http.cookiejar import CookieJar -from typing import (IO, TYPE_CHECKING, Any, BinaryIO, Callable, Dict, List, - Literal, Mapping, Optional, Sequence, Set, Tuple, Union) +from io import BufferedReader, BytesIO +from typing import ( + IO, + TYPE_CHECKING, + Any, + BinaryIO, + Callable, + Dict, + List, + Literal, + Mapping, + Optional, + Sequence, + Set, + Tuple, + Union, +) from uuid import UUID if TYPE_CHECKING: # pragma: no cover - from .models import Cookies, Headers, Request # noqa: F401 + from .models import ( + URL, + Auth, + BasicAuth, + Cookies, + HeaderRotator, + Headers, # noqa: F401 + Proxy, + ProxyRotator, + Response, + TLSIdentifierRotator, + URLParams, + ) AuthTypes = Optional[ Union[ @@ -19,7 +48,7 @@ ] ] URLTypes = Union["URL", str, bytes] -ProxyTypes = Union[str, bytes, "Proxy", "URL"] +ProxyTypes = Optional[Union[str, bytes, "Proxy", "URL", "ProxyRotator"]] URL_ALLOWED_PARAMS = Union[str, bytes, int, float, bool] URLParamTypes = Optional[ Union[ @@ -35,16 +64,14 @@ ], ] ] -MethodTypes = Union[ - "Method", Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"] -] +MethodTypes = Union[str, Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"]] ProtocolTypes = Optional[Union[Literal["auto", "http1", "http2"], bool]] HookTypes = Optional[Mapping[Literal["request", "response"], Sequence[Callable]]] -TLSSession = Union["TLSSession", None] -TLSSessionId = Union[str, UUID] -TLSPayload = Union[dict, str, bytes, bytearray] -TLSCookiesTypes = Optional[List[Dict[str, str]]] -TLSIdentifierTypes = Literal[ +Session = Optional[Any] +SessionId = Union[str, UUID] +Payload = Union[dict, str, bytes, bytearray] +CookiesTypes = Optional[List[Dict[str, str]]] +IdentifierLiterals = Literal[ "chrome_103", "chrome_104", "chrome_105", @@ -60,11 +87,14 @@ "chrome_117", "chrome_120", "chrome_124", - "safari_15_6_1", - "safari_16_0", - "safari_ios_15_5", - "safari_ios_15_6", - "safari_ios_16_0", + "chrome_130_PSK", + "chrome_131", + "chrome_131_PSK", + "chrome_133", + "chrome_133_PSK", + "confirmed_android", + "confirmed_android_2", + "confirmed_ios", "firefox_102", "firefox_104", "firefox_105", @@ -73,39 +103,53 @@ "firefox_110", "firefox_117", "firefox_120", - "opera_89", - "opera_90", - "opera_91", - "okhttp4_android_7", - "okhttp4_android_8", - "okhttp4_android_9", + "firefox_123", + "firefox_132", + "firefox_133", + "mesh_android", + "mesh_android_1", + "mesh_android_2", + "mesh_ios", + "mesh_ios_1", + "mesh_ios_2", + "mms_ios", + "mms_ios_1", + "mms_ios_2", + "mms_ios_3", + "nike_android_mobile", + "nike_ios_mobile", "okhttp4_android_10", "okhttp4_android_11", "okhttp4_android_12", "okhttp4_android_13", - "zalando_ios_mobile", + "okhttp4_android_7", + "okhttp4_android_8", + "okhttp4_android_9", + "opera_89", + "opera_90", + "opera_91", + "safari_15_6_1", + "safari_16_0", + "safari_ipad_15_6", + "safari_ios_15_5", + "safari_ios_15_6", + "safari_ios_16_0", + "safari_ios_17_0", + "safari_ios_18_0", + "safari_ios_18_5", "zalando_android_mobile", - "nike_ios_mobile", - "nike_android_mobile", - "mms_ios", - "mms_ios_2", - "mms_ios_3", - "mesh_ios", - "mesh_ios_2", - "mesh_android", - "mesh_android_2", - "confirmed_ios", - "confirmed_android", - "confirmed_android_2", + "zalando_ios_mobile", ] +IdentifierTypes = Union[IdentifierLiterals, str] +IdentifierArgTypes = Optional[Union[IdentifierTypes, "TLSIdentifierRotator"]] AnyList = List[ Union[ - List[Union[str, Union[str, int, float]]], - Tuple[Union[str, Union[str, int, float]]], - Set[Union[str, Union[str, int, float]]], + List[Union[str, int, float]], + Tuple[Union[str, int, float], ...], + Set[Union[str, int, float]], List[Union[str, bytes]], - Tuple[Union[str, bytes]], + Tuple[Union[str, bytes], ...], Set[Union[str, bytes]], ] ] @@ -113,6 +157,7 @@ HeaderTypes = Optional[ Union[ "Headers", + "HeaderRotator", Mapping[str, Union[str, int, float]], Mapping[bytes, bytes], AnyList, @@ -136,9 +181,7 @@ RequestFileValue = Union[ FileContent, # file (or file path, str and bytes) Tuple[ByteOrStr, FileContent], # filename, file (or file path, str and bytes)) - Tuple[ - ByteOrStr, FileContent, ByteOrStr - ], # filename, file (or file path, str and bytes)), content type + Tuple[ByteOrStr, FileContent, ByteOrStr], # filename, file (or file path, str and bytes)), content type ] RequestData = Mapping[str, Any] RequestJson = Mapping[str, Any] diff --git a/tls_requests/utils.py b/src/tls_requests/utils.py similarity index 71% rename from tls_requests/utils.py rename to src/tls_requests/utils.py index ba36416..b67f551 100644 --- a/tls_requests/utils.py +++ b/src/tls_requests/utils.py @@ -5,22 +5,23 @@ import logging from typing import Any, AnyStr, Union -FORMAT = "%(levelname)s:%(asctime)s:%(name)s:%(funcName)s:%(lineno)d >>> %(message)s" -DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" +FORMAT = "[%(asctime)s] %(levelname)-8s %(name)s:%(funcName)s:%(lineno)d - %(message)s" +DATE_FORMAT = "%Y-%m-%d %H:%M:%S" -def import_module(name: Union[str, list[str]]): +def import_module(name: Union[str, list[str]]) -> Any: modules = name if isinstance(name, list) else [name] - for module in modules: - if isinstance(module, str): + for module_name in modules: + if isinstance(module_name, str): try: - module = importlib.import_module(module) + module = importlib.import_module(module_name) return module except ImportError: pass + return None -chardet = import_module(["chardet", "charset_normalizer"]) +chardet = import_module(["charset_normalizer", "chardet"]) orjson = import_module(["orjson"]) if orjson: jsonlib = orjson @@ -30,12 +31,16 @@ def import_module(name: Union[str, list[str]]): jsonlib = json -def get_logger( - name: str = "TLSRequests", level: int | str = logging.INFO -) -> logging.Logger: - logging.basicConfig(format=FORMAT, datefmt=DATE_FORMAT, level=level) +def get_logger(name: str = "TLSRequests", level: Union[int, str] = logging.INFO) -> logging.Logger: logger = logging.getLogger(name) logger.setLevel(level) + + if not logger.handlers: + handler = logging.StreamHandler() + formatter = logging.Formatter(FORMAT, datefmt=DATE_FORMAT) + handler.setFormatter(formatter) + logger.addHandler(handler) + return logger @@ -57,7 +62,7 @@ def to_str( value = value.decode(encoding) if isinstance(value, (bytes, bytearray)) else value if isinstance(value, (dict, list, tuple, set)): value = json_dumps( - value if isinstance(value, dict) else list[value], + value if isinstance(value, dict) else list(value), **dict( ensure_ascii=True if str(encoding).lower() == "ascii" else False, default=str, @@ -73,7 +78,7 @@ def to_str( return str(value) -def to_base64(value: Union[dict, str, bytes], encoding: str = "utf-8") -> AnyStr: +def to_base64(value: Union[dict, str, bytes], encoding: str = "utf-8") -> str: return base64.b64encode(to_bytes(value, encoding)).decode(encoding) @@ -81,7 +86,7 @@ def b64decode(value: AnyStr) -> bytes: return base64.b64decode(value) -def to_json(value: Union[str, bytes], encoding: str = "utf-8", **kwargs) -> dict: +def to_json(value: Union[str, bytes], **kwargs) -> dict: if isinstance(value, dict): return value try: diff --git a/tests/conftest.py b/tests/conftest.py index 95feb18..970275d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,8 @@ +from __future__ import annotations + import tls_requests -pytest_plugins = ['pytest_httpserver', 'pytest_asyncio'] +pytest_plugins = ["pytest_httpserver", "pytest_asyncio"] def pytest_configure(config): diff --git a/tests/test_api.py b/tests/test_api.py index cc26310..ce2ce5f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import tls_requests RESPONSE_BYTE = b"Hello World!" @@ -13,7 +15,7 @@ def assert_response(response): def make_request(request_fn, httpserver, is_assert_response: bool = True): httpserver.expect_request("/api").respond_with_data(RESPONSE_BYTE) - response = request_fn(httpserver.url_for('/api')) + response = request_fn(httpserver.url_for("/api")) if is_assert_response: assert_response(response) diff --git a/tests/test_auth.py b/tests/test_auth.py index 0ef845c..120d94b 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,8 +1,11 @@ +from __future__ import annotations + from base64 import b64encode import pytest import tls_requests +from tls_requests.models.auth import Auth, AuthenticationError, BasicAuth auth = ("user", "pass") AUTH_TOKEN = "Basic %s" % b64encode(b":".join([s.encode() for s in auth])).decode() @@ -18,7 +21,7 @@ def auth_function(request): @pytest.fixture def auth_url(httpserver): - return httpserver.url_for('/auth') + return httpserver.url_for("/auth") @pytest.fixture @@ -55,7 +58,7 @@ def test_client_auth(http_auth, auth_url): def test_client_auth_cross_sharing(http_auth, auth_url): - with tls_requests.Client(auth=('1', '2')) as client: + with tls_requests.Client(auth=("1", "2")) as client: response = client.get(auth_url, auth=auth) assert response.status_code == 200 @@ -100,3 +103,14 @@ async def test_async_auth_function_cross_sharing(http_auth_function, auth_url): assert response.status_code == 200 assert bool(response.closed == client.closed) is True assert response.request.headers[AUTH_FUNCTION_KEY] == AUTH_FUNCTION_VALUE + + +def test_auth_base(): + a = Auth() + assert a.build_auth(None) is None + + +def test_basic_auth_invalid_type(): + with pytest.raises(AuthenticationError): + b = BasicAuth(123, "pass") + b._encode(123) diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..106a9a1 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,857 @@ +from __future__ import annotations + +import base64 +import datetime +import time +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from tls_requests.client import AsyncClient, BaseClient, Client, ClientState +from tls_requests.exceptions import AuthenticationError, ProxyError, RemoteProtocolError, TooManyRedirects +from tls_requests.models import ( + URL, + Auth, + HeaderRotator, + Headers, + Proxy, + ProxyRotator, + Request, + Response, + TLSIdentifierRotator, +) +from tls_requests.models.cookies import Cookies, RequestsCookieJar +from tls_requests.models.tls import TLSClient, TLSConfig, TLSResponse, _BaseConfig +from tls_requests.settings import DEFAULT_CLIENT_IDENTIFIER + +VALID_B64_BODY = base64.b64encode(b'{"ok": true}').decode() + + +def test_cookies_extra_coverage(): + jar = RequestsCookieJar() + expires = time.time() + 3600 + jar.set("a", "b", expires=expires) + assert jar.get("a") == "b" + + cookies = Cookies() + cookies.set("x", "y") + assert cookies["x"] == "y" + + +def test_urls_extra_coverage(): + u = URL("https://user:pass@host:8080/path?a=b#hash") + assert u.username == "user" + assert u.password == "pass" + + +def test_client_hooks_none(): + client = BaseClient() + assert client._rebuild_hooks(None) is None + + +def test_client_hooks_empty_dict(): + client = BaseClient() + assert client._rebuild_hooks({}) == {} + + +def test_client_redirect_no_location(): + client = Client() + req = MagicMock() + req.url = URL("https://host") + resp = MagicMock() + resp.headers = {} + with pytest.raises(RemoteProtocolError, match="without 'Location'"): + client._rebuild_redirect_url(req, resp) + + +def test_client_redirect_invalid_location_final(): + client = Client() + req = MagicMock() + req.url = URL("https://host") + resp = MagicMock() + resp.headers = {"Location": "http://other"} + with patch("tls_requests.client.URL") as mock_url_cls: + mock_url = MagicMock() + mock_url.netloc = "other" + mock_url.scheme = "https" + mock_url.url = "" + mock_url_cls.return_value = mock_url + with pytest.raises(RemoteProtocolError, match="Invalid URL in Location headers"): + client._rebuild_redirect_url(req, resp) + + +def test_response_private_stream_consumed(): + resp = Response(200) + assert resp._is_stream_consumed is False + resp._is_stream_consumed = True + assert resp._is_stream_consumed is True + + +def test_auth_extra_coverage(): + from tls_requests.models.auth import BasicAuth + + auth = BasicAuth(123, 456) + with pytest.raises(AuthenticationError): + auth.build_auth(MagicMock()) + + +def test_cookies_get_dict_missing_branches_final(): + jar = RequestsCookieJar() + res = jar.get_dict(domain="example.com", path="/") + assert isinstance(res, dict) + + +def test_utils_to_str_final(): + from tls_requests.utils import to_str + + assert to_str(None) == "" + assert to_str(123) == "123" + assert to_str([1, 2]) == "[1,2]" + + +def test_client_auth_request_coverage(): + def my_auth(request): + return request + + with patch("tls_requests.client.TLSClient.request") as mock_request: + mock_request.return_value = TLSResponse(success=True, status=200, body=VALID_B64_BODY) + client = Client(auth=my_auth) + client.get("http://example.com") + + +@pytest.mark.asyncio +async def test_aclient_auth_request_coverage(): + def my_auth(request): + return request + + with patch("tls_requests.client.TLSClient.arequest") as mock_request: + mock_request.return_value = TLSResponse(success=True, status=200, body=VALID_B64_BODY) + async with AsyncClient(auth=my_auth) as client: + await client.get("http://example.com") + + +def test_client_redirect_303(): + with patch("tls_requests.client.TLSClient.request") as mock_request: + mock_request.side_effect = [ + TLSResponse(success=True, status=303, headers={"Location": "/new"}), + TLSResponse(success=True, status=200, body=VALID_B64_BODY), + ] + client = Client(follow_redirects=True) + resp = client.post("http://example.com/old") + assert resp.request.method == "GET" + + +def test_client_redirect_301_post(): + with patch("tls_requests.client.TLSClient.request") as mock_request: + mock_request.side_effect = [ + TLSResponse(success=True, status=301, headers={"Location": "/new"}), + TLSResponse(success=True, status=200, body=VALID_B64_BODY), + ] + client = Client(follow_redirects=True) + resp = client.post("http://example.com/old") + assert resp.request.method == "GET" + + +@pytest.mark.asyncio +async def test_aclient_response_hook_coverage(): + def my_hook(response): + return response + + with patch("tls_requests.client.TLSClient.arequest") as mock_request: + mock_request.return_value = TLSResponse(success=True, status=200, body=VALID_B64_BODY) + async with AsyncClient(hooks={"response": [my_hook]}) as client: + await client.get("http://example.com") + + +def test_build_hook_response_none_coverage(): + client = Client(hooks={"response": []}) # empty sequence + req = Request("GET", "http://example.com") + resp = Response(status_code=200) + resp.request = req + assert client.build_hook_response(resp) is None + + +def test_client_request_hook_returns_request(): + def my_hook(request): + request.headers["X-Custom"] = "hooked" + return request + + with patch("tls_requests.client.TLSClient.request") as mock_request: + mock_request.return_value = TLSResponse(success=True, status=200, body=VALID_B64_BODY) + client = Client(hooks={"request": [my_hook]}) + resp = client.get("http://example.com") + assert resp.request.headers["X-Custom"] == "hooked" + + +@pytest.mark.asyncio +async def test_aclient_request_hook_returns_request(): + def my_hook(request): + request.headers["X-Custom"] = "hooked" + return request + + with patch("tls_requests.client.TLSClient.arequest") as mock_request: + mock_request.return_value = TLSResponse(success=True, status=200, body=VALID_B64_BODY) + async with AsyncClient(hooks={"request": [my_hook]}) as client: + resp = await client.get("http://example.com") + # Line 1204 in client.py + assert resp.request.headers["X-Custom"] == "hooked" + + +def test_client_redirect_302_get(): + with patch("tls_requests.client.TLSClient.request") as mock_request: + mock_request.side_effect = [ + TLSResponse(success=True, status=302, headers={"Location": "/new"}), + TLSResponse(success=True, status=200, body=VALID_B64_BODY), + ] + client = Client(follow_redirects=True) + resp = client.post("http://example.com/old", data={"a": 1}) + assert resp.status_code == 200 + assert resp.history[0].status_code == 302 + assert resp.request.method == "GET" # 302 POST -> GET + + +def test_client_redirect_scheme_mismatch(): + client = Client() + req = Request("GET", "http://example.com") + resp = Response(status_code=301, headers={"Location": "https://example.com/new"}) + resp.request = req + + new_req = client._rebuild_redirect_request(req, resp) + assert new_req.url.scheme == "http" # line 425 covers this + + +def test_client_proxy_rotator_marking(): + rotator = ProxyRotator(["http://proxy1:8080"]) + with patch("tls_requests.client.TLSClient.request") as mock_request: + mock_request.return_value = TLSResponse(success=True, status=200, body=VALID_B64_BODY) + client = Client(proxy=rotator) + with patch.object(rotator, "mark_result") as mock_mark: + client.get("http://example.com") + assert mock_mark.called + + +@pytest.mark.asyncio +async def test_aclient_proxy_rotator_marking(): + rotator = ProxyRotator(["http://proxy1:8080"]) + with patch("tls_requests.client.TLSClient.arequest") as mock_request: + mock_request.return_value = TLSResponse(success=True, status=200, body=VALID_B64_BODY) + async with AsyncClient(proxy=rotator) as client: + with patch.object(rotator, "amark_result", new_callable=AsyncMock) as mock_mark: + await client.get("http://example.com") + assert mock_mark.called + + +@pytest.mark.asyncio +async def test_aclient_redirect_too_many(): + with patch("tls_requests.client.TLSClient.arequest") as mock_request: + mock_request.return_value = TLSResponse( + success=True, status=302, headers={"Location": "http://example.com/loop"} + ) + async with AsyncClient(follow_redirects=True, max_redirects=1) as client: + with pytest.raises(TooManyRedirects): + await client.get("http://example.com/loop") + + +@pytest.mark.asyncio +async def test_aclient_enter_errors(): + client = AsyncClient() + async with client: + with pytest.raises(RuntimeError, match="not possible to open a client instance more than once"): + await client.__aenter__() + + with pytest.raises(RuntimeError, match="cannot be reopened after it has been closed"): + await client.__aenter__() + + +def test_tls_config_to_payload(): + config = TLSConfig() + assert config.to_payload() == config.to_dict() + + +def test_tls_initialize_in_init(): + with patch("tls_requests.models.tls.TLSLibrary.load") as mock_load: + TLSClient._library = None + TLSClient() + assert mock_load.called + + +def test_tls_initialize_in_destroy_all(): + with patch("tls_requests.models.tls.TLSLibrary.load") as mock_load: + mock_lib = MagicMock() + mock_lib.destroyAll.return_value = b'{"success": true}' + mock_load.return_value = mock_lib + + TLSClient._library = None + TLSClient._destroyAll = None + TLSClient.destroy_all() + assert mock_load.called + + +def test_tls_initialize_return(): + with patch("tls_requests.models.tls.TLSLibrary.load"): + TLSClient._library = None + res = TLSClient.initialize() + assert isinstance(res, TLSClient) + + +def test_tls_destroy_all_fail(): + with patch("tls_requests.models.tls.TLSClient._destroyAll") as mock_destroy: + mock_destroy.return_value = b'{"success": false}' + assert TLSClient.destroy_all() is False + + +def test_base_config_from_kwargs(): + config = _BaseConfig.from_kwargs(extra_param="value") + assert hasattr(config, "_extra_config") + + +def test_tls_config_stream_id(): + config = TLSConfig.from_kwargs(stream_id=123) + assert config.streamID == 123 + + +def test_base_client_deprecated_tls_identifier(): + with patch("tls_requests.client.TLSClient.initialize"): + client = BaseClient(tls_identifier="custom_chrome") + # client_identifier should be set from tls_identifier + assert client._config.tlsClientIdentifier == "custom_chrome" + + +def test_base_client_headers_initialization(): + with patch("tls_requests.client.TLSClient.initialize"): + # Test rotator + rotator = HeaderRotator([]) + client = BaseClient(headers=rotator) + assert client._header_rotator == rotator + + # Test list (converted to rotator) + with patch("tls_requests.models.HeaderRotator.from_file") as mock_from_file: + BaseClient(headers=["file.json"]) + mock_from_file.assert_called_with(["file.json"]) + + +def test_base_client_properties_and_setters(): + with patch("tls_requests.client.TLSClient.initialize"): + client = BaseClient() + + # closed + assert client.closed is False + client._state = ClientState.CLOSED + assert client.closed is True + + # headers setter + rotator = HeaderRotator([]) + client.headers = rotator + assert client._header_rotator == rotator + + client.headers = {"a": "b"} + assert client.headers["a"] == "b" + + with patch("tls_requests.models.HeaderRotator.from_file") as mock_from_file: + client.headers = ["file.json"] + mock_from_file.assert_called() + + # cookies setter + client.cookies = {"c": "d"} + assert client.cookies["c"] == "d" + + # params setter + client.params = {"p": "v"} + assert client.params["p"] == "v" + + # hooks setter + def dummy_hook(r): + return r + + client.hooks = {"request": [dummy_hook]} + assert client.hooks["request"] == [dummy_hook] + + +def test_prepare_auth(): + with patch("tls_requests.client.TLSClient.initialize"): + client = BaseClient() + req = MagicMock(spec=Request) + req.headers = {} + + # Tuple auth + client.prepare_auth(req, ("user", "pass")) + assert "Authorization" in req.headers + + # Callable auth + mock_auth_func = MagicMock() + client.prepare_auth(req, mock_auth_func) + mock_auth_func.assert_called_with(req) + + # Auth instance + class MyAuth(Auth): + def build_auth(self, request): + return "authorized" + + auth_inst = MyAuth() + assert client.prepare_auth(req, auth_inst) == "authorized" + + +def test_prepare_headers_rotator(): + with patch("tls_requests.client.TLSClient.initialize"): + client = BaseClient() + rotator = MagicMock(spec=HeaderRotator) + rotator.next.return_value = Headers({"X-Rotated": "1"}) + + # Client rotator + client._header_rotator = rotator + res = client.prepare_headers() + assert res["X-Rotated"] == "1" + + # Specific rotator + res2 = client.prepare_headers(headers=rotator) + assert res2["X-Rotated"] == "1" + + +def test_prepare_proxy_types(): + with patch("tls_requests.client.TLSClient.initialize"): + client = BaseClient() + + # None + assert client.prepare_proxy(None) is None + + # ProxyRotator + rotator = MagicMock(spec=ProxyRotator) + rotator.next.return_value = "http://proxy:8080" + res = client.prepare_proxy(rotator) + assert res.url == "http://proxy:8080" + + # String/Bytes + assert client.prepare_proxy("http://host:80").url == "http://host:80" + + # Proxy instance + p = Proxy("http://p") + assert client.prepare_proxy(p) == p + + # URL instance + u = URL("http://u") + assert client.prepare_proxy(u).url == "http://u" + + # Invalid + with pytest.raises(ProxyError): + client.prepare_proxy(123) + + +def test_prepare_client_identifier_rotator(): + with patch("tls_requests.client.TLSClient.initialize"): + client = BaseClient() + rotator = MagicMock(spec=TLSIdentifierRotator) + rotator.next.return_value = "chrome_99" + assert client.prepare_client_identifier(rotator) == "chrome_99" + + +def test_rebuild_hooks_edge_cases(): + with patch("tls_requests.client.TLSClient.initialize"): + client = BaseClient() + # Invalid keys or non-callable items + hooks = {"invalid": [lambda x: x], "request": ["not_callable"]} + rebuilt = client._rebuild_hooks(hooks) + assert "invalid" not in rebuilt + assert rebuilt["request"] == [] + + +def test_redirect_url_errors(): + with patch("tls_requests.client.TLSClient.initialize"): + client = BaseClient() + req = MagicMock(spec=Request) + req.url = URL("https://example.com") + + # Missing Location + resp_no_loc = MagicMock(spec=Response) + resp_no_loc.headers = {} + with pytest.raises(RemoteProtocolError, match="without 'Location'"): + client._rebuild_redirect_url(req, resp_no_loc) + + # Invalid URL in Location + resp_bad_loc = MagicMock(spec=Response) + resp_bad_loc.headers = {"Location": "http://[invalid]"} + with pytest.raises(RemoteProtocolError, match="Invalid URL"): + client._rebuild_redirect_url(req, resp_bad_loc) + + +def test_redirect_url_netloc_missing(): + with patch("tls_requests.client.TLSClient.initialize"): + client = BaseClient() + req = MagicMock(spec=Request) + req.url = URL("https://host:443/path") + + resp = MagicMock(spec=Response) + resp.headers = {"Location": "/newpath"} + + url = client._rebuild_redirect_url(req, resp) + assert url.host == "host" + assert url.scheme == "https" + assert str(url.port) == "443" + + +def test_redirect_scheme_change_h2_error(): + with patch("tls_requests.client.TLSClient.initialize"): + # http2='http2' forces it to not be 'auto' or None + client = BaseClient(http2="http2") + req = MagicMock(spec=Request) + req.url = URL("https://host") + + resp = MagicMock(spec=Response) + resp.headers = {"Location": "http://otherhost"} # Switch to http + + with pytest.raises(RemoteProtocolError, match="Switching remote scheme from HTTP/2 to HTTP/1"): + client._rebuild_redirect_url(req, resp) + + +def test_lifecycle_errors(): + with patch("tls_requests.client.TLSClient.initialize"): + client = BaseClient() + client._state = ClientState.OPENED + with pytest.raises(RuntimeError, match="more than once"): + client.__enter__() + + client._state = ClientState.CLOSED + with pytest.raises(RuntimeError, match="cannot be reopened"): + client.__enter__() + + +@pytest.mark.asyncio +async def test_async_client_proxy_rotator_mark(): + with patch("tls_requests.client.TLSClient.initialize"): + rotator = MagicMock(spec=ProxyRotator) + client = AsyncClient(proxy=rotator) + + req = MagicMock(spec=Request) + req.proxy = Proxy("http://p") + + resp = MagicMock(spec=Response) + resp.status_code = 200 + resp.elapsed = datetime.timedelta(seconds=1) + resp.request = req + resp.is_redirect = False + + with patch.object(AsyncClient, "_send", return_value=resp): + await client.send(req) + rotator.amark_result.assert_called() + + +def test_client_all_request_methods(): + with patch("tls_requests.client.TLSClient.initialize"): + client = Client() + # Mock _send to avoid actual requests + mock_resp = MagicMock(spec=Response) + mock_resp.status_code = 200 + mock_resp.is_redirect = False + mock_resp.request = MagicMock(spec=Request) + mock_resp.request.proxy = None + + with patch.object(Client, "_send", return_value=mock_resp): + client.get("https://test") + client.post("https://test") + client.put("https://test") + client.patch("https://test") + client.delete("https://test") + client.head("https://test") + client.options("https://test") + + +@pytest.mark.asyncio +async def test_async_client_all_request_methods(): + with patch("tls_requests.client.TLSClient.initialize"): + client = AsyncClient() + mock_resp = MagicMock(spec=Response) + mock_resp.status_code = 200 + mock_resp.is_redirect = False + mock_resp.request = MagicMock(spec=Request) + mock_resp.request.proxy = None + + with patch.object(AsyncClient, "_send", return_value=mock_resp): + await client.get("https://test") + await client.post("https://test") + await client.put("https://test") + await client.patch("https://test") + await client.delete("https://test") + await client.head("https://test") + await client.options("https://test") + + +def test_client_send_closed(): + with patch("tls_requests.client.TLSClient.initialize"): + client = Client() + client.close() + with pytest.raises(RuntimeError, match="client has been closed"): + client.get("https://test") + + +def test_client_with_hooks(): + with patch("tls_requests.client.TLSClient.initialize"): + req_hook_called = False + resp_hook_called = False + + def req_hook(r): + nonlocal req_hook_called + req_hook_called = True + return r + + def resp_hook(resp): + nonlocal resp_hook_called + resp_hook_called = True + return resp + + client = Client(hooks={"request": [req_hook], "response": [resp_hook]}) + mock_resp = MagicMock(spec=Response) + mock_resp.status_code = 200 + mock_resp.is_redirect = False + mock_resp.request = MagicMock(spec=Request) + mock_resp.request.proxy = None + + with patch.object(Client, "_send", return_value=mock_resp): + client.get("https://test") + assert req_hook_called is True + assert resp_hook_called is True + + +def test_redirect_limit(): + with patch("tls_requests.client.TLSClient.initialize"): + client = Client(max_redirects=2) + + # We need to mock the TLSClient response precisely to avoid decoding errors + with patch.object(Client, "session") as mock_session: + # Return redirect response repeatedly + mock_tls_resp = MagicMock() + mock_tls_resp.status = 302 + mock_tls_resp.headers = {"Location": "https://test2"} + mock_tls_resp.body = "data:text/plain;base64,YWJj" # "abc" + mock_tls_resp.cookies = {} + mock_tls_resp.id = "1" + mock_tls_resp.target = "https://test1" + mock_tls_resp.success = True + mock_tls_resp.usedProtocol = "HTTP/1.1" + + mock_session.request.return_value = mock_tls_resp + + with pytest.raises(TooManyRedirects): + client.get("https://test1") + + +@pytest.mark.asyncio +async def test_async_redirect_limit(): + with patch("tls_requests.client.TLSClient.initialize"): + client = AsyncClient(max_redirects=2) + + with patch.object(AsyncClient, "session") as mock_session: + mock_tls_resp = MagicMock() + mock_tls_resp.status = 302 + mock_tls_resp.headers = {"Location": "https://test2"} + mock_tls_resp.body = "data:text/plain;base64,YWJj" + mock_tls_resp.cookies = {} + mock_tls_resp.id = "1" + mock_tls_resp.target = "https://test1" + mock_tls_resp.success = True + mock_tls_resp.usedProtocol = "HTTP/1.1" + + # Use AsyncMock for arequest + mock_session.arequest = AsyncMock(return_value=mock_tls_resp) + + with pytest.raises(TooManyRedirects): + await client.get("https://test1") + + +@pytest.mark.asyncio +async def test_async_client_send_closed(): + with patch("tls_requests.client.TLSClient.initialize"): + client = AsyncClient() + await client.aclose() + with pytest.raises(RuntimeError, match="client has been closed"): + await client.get("https://test") + + +def test_prepare_client_identifier_default(): + with patch("tls_requests.client.TLSClient.initialize"): + client = BaseClient() + assert client.prepare_client_identifier(None) == str(DEFAULT_CLIENT_IDENTIFIER) + + +def test_prepare_headers_basic(): + with patch("tls_requests.client.TLSClient.initialize"): + client = BaseClient() + # Coverage for line 261 + res = client.prepare_headers(headers={"X-Test": "1"}) + assert res["X-Test"] == "1" + + +def test_redirect_url_scheme_http_to_http(): + with patch("tls_requests.client.TLSClient.initialize"): + client = BaseClient() + req = MagicMock(spec=Request) + req.url = URL("http://host") + + resp = MagicMock(spec=Response) + resp.headers = {"Location": "http://otherhost"} + + # Coverage for line 425 + url = client._rebuild_redirect_url(req, resp) + assert url.scheme == "http" + + +def test_redirect_url_invalid_final_check(): + with patch("tls_requests.client.TLSClient.initialize"): + client = BaseClient() + req = MagicMock(spec=Request) + req.url = URL("https://host") + + resp = MagicMock(spec=Response) + resp.headers = {"Location": "https://otherhost"} + + # Simulate url.url being empty after processing + with patch("tls_requests.client.URL") as mock_url_cls: + mock_url = MagicMock() + mock_url.netloc = "otherhost" + mock_url.scheme = "https" + mock_url.url = "" # Coverage for line 438 + mock_url_cls.return_value = mock_url + + with pytest.raises(RemoteProtocolError, match="Invalid URL in Location headers"): + client._rebuild_redirect_url(req, resp) + + +@pytest.mark.asyncio +async def test_async_prepare_methods(): + with patch("tls_requests.client.TLSClient.initialize"): + rotator = MagicMock(spec=HeaderRotator) + rotator.anext = AsyncMock(return_value=Headers({"X-Async": "1"})) + + client = AsyncClient(headers=rotator) + + # aprepare_headers + h1 = await client.aprepare_headers() + assert h1["X-Async"] == "1" + + h2 = await client.aprepare_headers(headers=rotator) + assert h2["X-Async"] == "1" + + h3 = await client.aprepare_headers(headers={"X": "Y"}) + assert h3["X"] == "Y" + + # aprepare_proxy + client.proxy = "http://p" + assert (await client.aprepare_proxy(None)) is None + assert (await client.aprepare_proxy("http://p2")).url == "http://p2" + + # Coverage for line 882-886 (Proxy and URL types) + assert (await client.aprepare_proxy(Proxy("http://p3"))).url == "http://p3" + assert (await client.aprepare_proxy(URL("http://p4"))).url == "http://p4" + with pytest.raises(ProxyError): + await client.aprepare_proxy(123) + + # aprepare_client_identifier + assert await client.aprepare_client_identifier("chrome") == "chrome" + + id_rotator = MagicMock(spec=TLSIdentifierRotator) + id_rotator.anext = AsyncMock(return_value="firefox") + assert await client.aprepare_client_identifier(id_rotator) == "firefox" + + assert await client.aprepare_client_identifier(None) == DEFAULT_CLIENT_IDENTIFIER + + +def test_redirect_scheme_change_auto(): + with patch("tls_requests.client.TLSClient.initialize"): + # http2='auto' allows session reset + client = BaseClient(http2="auto") + old_session_id = client._config.sessionId + + req = MagicMock(spec=Request) + req.url = URL("https://host") + + resp = MagicMock(spec=Response) + resp.headers = {"Location": "http://otherhost"} + + url = client._rebuild_redirect_url(req, resp) + assert url.scheme == "http" + assert client._config.sessionId != old_session_id + # session.destroy_session should be called for old session + client.session.destroy_session.assert_called_with(old_session_id) + + +def test_base_client_headers_list_init(): + with patch("tls_requests.client.TLSClient.initialize"): + with patch("tls_requests.models.HeaderRotator.from_file"): + client = BaseClient(headers=["list"]) + assert client._header_rotator is not None + + +def test_base_client_init_headers_dict(): + with patch("tls_requests.client.TLSClient.initialize"): + client = BaseClient(headers={"a": "b"}) + assert client.headers["a"] == "b" + + +@pytest.mark.asyncio +async def test_async_client_send_with_auth_and_rotator(): + with patch("tls_requests.client.TLSClient.initialize"): + rotator = AsyncMock(spec=ProxyRotator) + rotator.anext.return_value = Proxy("http://proxyhost:8080") + rotator.amark_result = AsyncMock() + + client = AsyncClient(proxy=rotator, auth=("user", "pass")) + + req = MagicMock(spec=Request) + req.proxy = Proxy("http://p") + req.headers = {} + + resp = MagicMock(spec=Response) + resp.status_code = 200 + resp.elapsed = datetime.timedelta(seconds=1) + resp.request = req + resp.is_redirect = False + resp.read = AsyncMock() + resp.aread = AsyncMock() + resp.close = AsyncMock() + resp.aclose = AsyncMock() + + with patch.object(AsyncClient, "_send", return_value=resp): + await client.get("https://test") + rotator.amark_result.assert_called() + + +def test_base_client_exit_reentry_errors(): + with patch("tls_requests.client.TLSClient.initialize"): + client = BaseClient() + with client: + pass + # client is now closed + with pytest.raises(RuntimeError, match="cannot be reopened"): + with client: + pass + + +@pytest.mark.asyncio +async def test_async_client_proxy_rotator_failure_mark(): + with patch("tls_requests.client.TLSClient.initialize"): + rotator = MagicMock(spec=ProxyRotator) + rotator.amark_result = AsyncMock() + client = AsyncClient(proxy=rotator) + + req = MagicMock(spec=Request) + req.proxy = Proxy("http://p") + + resp = MagicMock(spec=Response) + resp.status_code = 500 + resp.elapsed = datetime.timedelta(seconds=1) + resp.request = req + resp.is_redirect = False + + with patch.object(AsyncClient, "_send", return_value=resp): + await client.send(req) + # Verify success=False was passed + rotator.amark_result.assert_called_with(proxy=req.proxy, success=False, latency=1.0) + + +@pytest.mark.asyncio +async def test_async_client_context_manager_autoclose(): + with patch("tls_requests.client.TLSClient.initialize"): + async with AsyncClient() as client: + session_id = client._config.sessionId + mock_session = client.session + assert client.closed is False + + mock_session.destroy_session.assert_called_with(session_id) diff --git a/tests/test_cookies.py b/tests/test_cookies.py index 4b0277f..a49baa1 100644 --- a/tests/test_cookies.py +++ b/tests/test_cookies.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pytest_httpserver import HTTPServer from werkzeug import Request, Response diff --git a/tests/test_encoders.py b/tests/test_encoders.py index 4c7e080..83c72ca 100644 --- a/tests/test_encoders.py +++ b/tests/test_encoders.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from io import BytesIO from mimetypes import guess_type from pathlib import Path @@ -5,16 +8,25 @@ from pytest_httpserver import HTTPServer import tls_requests +from tls_requests.models.encoders import ( + BaseEncoder, + DataField, + FileField, + JsonEncoder, + MultipartEncoder, + StreamEncoder, + format_header, +) BASE_DIR = Path(__file__).resolve(strict=True).parent.parent CHUNK_SIZE = 65_536 -FILENAME = BASE_DIR / 'tests' / 'files' / 'coingecko.png' +FILENAME = BASE_DIR / "tests" / "files" / "coingecko.png" def get_image_bytes(filename: str = FILENAME): response_bytes = b"" - with open(filename, 'rb') as f: + with open(filename, "rb") as f: while chunk := f.read(CHUNK_SIZE): response_bytes += chunk @@ -32,49 +44,49 @@ def file_bytes(filename: str = FILENAME) -> bytes: def hook_files(_request, response): - image = _request.files['image'] + image = _request.files["image"] image_bytes = b"".join(image) origin_bytes = get_image_bytes() - response.headers['X-Image'] = 1 if image_bytes == origin_bytes else 0 - response.headers['X-Image-Content-Type'] = image.content_type + response.headers["X-Image"] = 1 if image_bytes == origin_bytes else 0 + response.headers["X-Image-Content-Type"] = image.content_type return response def hook_multipart(_request, response): - response.headers["X-Data-Values"] = ", ".join(_request.form.getlist('key1')) + response.headers["X-Data-Values"] = ", ".join(_request.form.getlist("key1")) response.headers["X-Image-Content-Type"] = _request.files["image"].content_type return response def test_file(httpserver: HTTPServer): httpserver.expect_request("/files").with_post_hook(hook_files).respond_with_data(status=201) - files = {'image': open(FILENAME, 'rb')} + files = {"image": open(FILENAME, "rb")} response = tls_requests.post(httpserver.url_for("/files"), files=files) assert response.status_code == 201 - assert response.headers.get('X-Image') == '1' + assert response.headers.get("X-Image") == "1" def test_file_tuple_2(httpserver: HTTPServer): httpserver.expect_request("/files").with_post_hook(hook_files).respond_with_data(status=201) - files = {'image': ('coingecko.png', open(FILENAME, 'rb'))} + files = {"image": ("coingecko.png", open(FILENAME, "rb"))} response = tls_requests.post(httpserver.url_for("/files"), files=files) assert response.status_code == 201 - assert response.headers.get('X-Image') == '1' + assert response.headers.get("X-Image") == "1" def test_file_tuple_3(httpserver: HTTPServer): httpserver.expect_request("/files").with_post_hook(hook_files).respond_with_data(status=201) - files = {'image': ('coingecko.png', open(FILENAME, 'rb'), 'image/png')} + files = {"image": ("coingecko.png", open(FILENAME, "rb"), "image/png")} response = tls_requests.post(httpserver.url_for("/files"), files=files) assert response.status_code == 201 - assert response.headers.get('X-Image') == '1' - assert response.headers.get('X-Image-Content-Type') == 'image/png' + assert response.headers.get("X-Image") == "1" + assert response.headers.get("X-Image-Content-Type") == "image/png" def test_multipart(httpserver: HTTPServer, file_bytes, mimetype): - data = {'key1': ['value1', 'value2']} + data = {"key1": ["value1", "value2"]} httpserver.expect_request("/multipart").with_post_hook(hook_multipart).respond_with_data(status=201) - files = {'image': ('coingecko.png', open(FILENAME, 'rb'), 'image/png')} + files = {"image": ("coingecko.png", open(FILENAME, "rb"), "image/png")} response = tls_requests.post(httpserver.url_for("/multipart"), data=data, files=files) assert response.status_code == 201 assert response.headers["X-Image-Content-Type"] == "image/png" @@ -82,13 +94,74 @@ def test_multipart(httpserver: HTTPServer, file_bytes, mimetype): def test_json(httpserver: HTTPServer): - data = { - 'integer': 1, - 'boolean': True, - 'list': ['1', '2', '3'], - 'data': {'key': 'value'} - } + data = {"integer": 1, "boolean": True, "list": ["1", "2", "3"], "data": {"key": "value"}} httpserver.expect_request("/json", json=data).respond_with_data(b"OK", status=201) response = tls_requests.post(httpserver.url_for("/json"), json=data) assert response.status_code == 201 assert response.content == b"OK" + + +def test_format_header_bytes(): + assert format_header("name", b"value") == b'name="value"' + + +def test_file_field_unpack_variations(tmp_path): + # Tuple len 1 + f = FileField("test", (BytesIO(b"data"),)) + assert f.filename == "upload" + + # String as value (becomes buffer) + f2 = FileField("test", "simple string content") + assert f2.filename == "upload" + assert f2._buffer.read() == b"simple string content" + + # TextIOWrapper + p = tmp_path / "test.txt" + p.write_text("hello") + with open(p, "r") as tf: + f3 = FileField("test", tf) + assert f3.filename == "test.txt" + assert f3._buffer.read() == b"hello" + + # Invalid buffer type + with pytest.raises(ValueError): + FileField("test", 123) + + +def test_base_encoder_context_manager(): + e = BaseEncoder() + assert e.closed is False + with e as entered: + assert entered is e + assert e.closed is True + + +def test_multipart_headers_empty(): + e = MultipartEncoder() + assert e.headers == {} + + +def test_stream_encoder_from_bytes(): + e = StreamEncoder.from_bytes(b"raw data") + assert b"".join(e) == b"raw data" + assert e.closed is True + + +def test_base_field_properties(): + f = DataField("foo", "bar") + assert b"Content-Disposition" in f.headers + assert b"foo" in f.render_parts() + + +def test_async_iter_encoder(): + import asyncio + + async def run(): + e = JsonEncoder({"a": 1}) + chunks = [] + async for chunk in e: + chunks.append(chunk) + return b"".join(chunks) + + res = asyncio.run(run()) + assert b'{"a":1}' in res diff --git a/tests/test_headers.py b/tests/test_headers.py index 49f764c..e911dfc 100644 --- a/tests/test_headers.py +++ b/tests/test_headers.py @@ -1,7 +1,12 @@ +from __future__ import annotations + +import pytest from pytest_httpserver import HTTPServer from werkzeug import Request, Response import tls_requests +from tls_requests.models.headers import HeaderAlias, HeaderError, Headers +from tls_requests.models.tls import TLSConfig def hook_request_headers(_request: Request, response: Response) -> Response: @@ -23,18 +28,168 @@ def test_request_headers(httpserver: HTTPServer): httpserver.expect_request("/headers").with_post_hook(hook_request_headers).respond_with_data(b"OK") response = tls_requests.get(httpserver.url_for("/headers"), headers={"foo": "bar"}) assert response.status_code == 200 - assert response.headers.get("foo") == "bar" + assert response.request.headers["foo"] == "bar" def test_response_headers(httpserver: HTTPServer): httpserver.expect_request("/headers").with_post_hook(hook_response_headers).respond_with_data(b"OK") response = tls_requests.get(httpserver.url_for("/headers")) assert response.status_code, 200 - assert response.headers.get("foo") == "bar" + assert response.headers["foo"] == "bar" def test_response_case_insensitive_headers(httpserver: HTTPServer): - httpserver.expect_request("/headers").with_post_hook(hook_response_case_insensitive_headers).respond_with_data(b"OK") + httpserver.expect_request("/headers").with_post_hook(hook_response_case_insensitive_headers).respond_with_data( + b"OK" + ) response = tls_requests.get(httpserver.url_for("/headers")) assert response.status_code, 200 - assert response.headers.get("foo") == "bar" + assert response.headers["foo"] == "bar" + + +def test_chrome_dynamic_headers(): + # Test chrome_112 + config = TLSConfig.from_kwargs(client_identifier="chrome_112") + headers = config.headers + assert "Chrome/112" in headers.get("user-agent", "") + assert 'v="112"' in headers.get("sec-ch-ua", "") + + # Test chrome_133 (default) + config_default = TLSConfig.from_kwargs(client_identifier="chrome_133") + headers_default = config_default.headers + assert "Chrome/133" in headers_default.get("user-agent", "") + assert 'v="133"' in headers_default.get("sec-ch-ua", "") + + +def test_firefox_dynamic_headers(): + # Test firefox_120 + config = TLSConfig.from_kwargs(client_identifier="firefox_120") + headers = config.headers + assert "Firefox/120" in headers.get("user-agent", "") + assert "rv:120" in headers.get("user-agent", "") + + +def test_safari_dynamic_headers(): + # Test safari_17 + config = TLSConfig.from_kwargs(client_identifier="safari_17") + headers = config.headers + assert "Version/17" in headers.get("user-agent", "") + + +def test_custom_headers_override(): + # Custom headers should not be overridden by dynamic injection + custom_headers = {"User-Agent": "MyCustomUA", "X-Test": "Value"} + config = TLSConfig.from_kwargs(client_identifier="chrome_112", headers=custom_headers) + assert config.headers["User-Agent"] == "MyCustomUA" + assert config.headers["X-Test"] == "Value" + assert "sec-ch-ua" not in config.headers # Should not inject if headers provided + + +def test_no_injection_for_non_browser(): + # Injection should only happen for chrome/firefox/safari + config = TLSConfig.from_kwargs(client_identifier="okhttp4_android_12") + assert not config.headers # Should be empty or only basic defaults if any + + +def test_header_alias_contains(): + assert HeaderAlias.contains("lower") is True + assert HeaderAlias.contains("capitalize") is True + assert HeaderAlias.contains("*") is True + assert HeaderAlias.contains("invalid") is False + + +def test_headers_init_alias(): + h = Headers(alias="*") + assert h.alias == "*" + h2 = Headers(alias="invalid") + assert h2.alias == "lower" + + +def test_headers_keys_values(): + h = Headers({"A": "1", "B": "2"}) + assert list(h.keys()) == ["a", "b"] + assert list(h.values()) == ["1", "2"] + + +def test_headers_update_none(): + h = Headers({"A": "1"}) + h.update(None) + assert h["a"] == "1" + + +def test_headers_prepare_from_headers_instance(): + h1 = Headers({"A": "1"}) + h2 = Headers(h1) + assert h2["a"] == "1" + + +def test_headers_prepare_invalid_format(): + with pytest.raises(HeaderError): + Headers(123) + + # Test valid but weird formats + h = Headers([("A", "1", "extra ignored")]) + assert h["a"] == "1" + + with pytest.raises(HeaderError): + Headers([(1,)]) # too few items + + +def test_headers_normalize_key_all(): + h = Headers({"User-Agent": "test"}, alias="*") + assert "User-Agent" in h + assert "user-agent" not in h + + +def test_headers_normalize_value_errors(): + h = Headers() + with pytest.raises(HeaderError) as exc: + h["A"] = {"invalid": "dict"} + assert "cannot be a dictionary" in str(exc.value) + + with pytest.raises(HeaderError) as exc: + h["A"] = [{"dict": "inside list"}] + assert "items cannot be a dictionary" in str(exc.value) + + +def test_headers_setitem_overwrite(): + h = Headers({"A": "1"}) + h["a"] = "2" + assert h["a"] == "2" + assert len(h) == 1 + + +def test_headers_delitem_missing(): + h = Headers({"A": "1"}) + del h["B"] # should not raise error + assert len(h) == 1 + + +def test_headers_eq_variations(): + h1 = Headers({"A": "1"}) + assert h1 == {"a": "1"} + assert h1 == [("a", "1")] + assert h1 != 123 + assert h1 != {"a": "2"} + + # Mock HeaderError in eq + class BadHeaders: + def items(self): + raise ValueError("bad") + + assert h1 != BadHeaders() + + +def test_headers_repr_secure(): + h = Headers({"Authorization": "Bearer secret", "X-Normal": "val"}) + r = repr(h) + assert "[secure]" in r + assert "secret" not in r + assert "val" in r + + +def test_headers_normalize_value_list(): + h = Headers({"A": ["1", "2"]}) + assert h["a"] == "1,2" + h["b"] = ("3", "4") + assert h["b"] == "3,4" diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 79e2d58..644bf91 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -1,15 +1,19 @@ +from __future__ import annotations + +import time + from pytest_httpserver import HTTPServer import tls_requests def log_request_return(request): - request.headers["X-Hook"] = '123456' + request.headers["X-Hook"] = "123456" return request def log_request_no_return(request): - request.headers["X-Hook"] = '123456' + request.headers["X-Hook"] = "123456" def log_response_raise_on_4xx_5xx(response): @@ -31,8 +35,21 @@ def test_request_hook_no_return(httpserver: HTTPServer): def test_response_hook(httpserver: HTTPServer): - httpserver.expect_request("/hooks", ).respond_with_data(status=404) + httpserver.expect_request( + "/hooks", + ).respond_with_data(status=404) try: _ = tls_requests.get(httpserver.url_for("/hooks"), hooks={"response": [log_response_raise_on_4xx_5xx]}) except Exception as e: assert e, tls_requests.exceptions.HTTPError + + +def timeout_hook(_request, response): + time.sleep(3) + return response + + +def test_timeout(httpserver: HTTPServer): + httpserver.expect_request("/timeout").with_post_hook(timeout_hook).respond_with_data(b"OK") + response = tls_requests.get(httpserver.url_for("/timeout"), timeout=1) + assert response.status_code == 0 diff --git a/tests/test_libraries.py b/tests/test_libraries.py new file mode 100644 index 0000000..41dc911 --- /dev/null +++ b/tests/test_libraries.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +import json +from unittest.mock import MagicMock, patch + +from tls_requests.models.libraries import Release, ReleaseAsset, TLSLibrary + + +def test_libraries_platform_branches(): + r = Release.from_kwargs(name="rel", tag_name="v1", assets=[{"name": "a1", "browser_download_url": "url"}]) + assert r.name == "rel" + assert len(r.assets) == 1 + assert isinstance(r.assets[0], ReleaseAsset) + test_data = {"tag_name": "vTEST"} + TLSLibrary.export_config(test_data) + loaded = TLSLibrary.import_config() + assert loaded["tag_name"] == "vTEST" + + +def test_libraries_fetch_api_mock(): + with patch("urllib.request.urlopen") as mock_open: + mock_resp = MagicMock() + mock_resp.status = 200 + mock_resp.read.return_value = json.dumps([{"tag_name": "v1.0.0", "assets": []}]).encode() + mock_open.return_value.__enter__.return_value = mock_resp + results = list(TLSLibrary.fetch_api()) + assert len(results) >= 0 + + +def test_libraries_download_mock(): + with ( + patch.object(TLSLibrary, "fetch_api", return_value=iter(["http://e.com/lib.so"])), + patch("urllib.request.urlopen") as mock_open, + patch("os.makedirs"), + patch("os.chmod"), + ): + mock_resp = MagicMock() + mock_resp.status = 200 + mock_resp.headers = {"content-length": "10"} + mock_resp.read.side_effect = [b"data", b""] + mock_open.return_value.__enter__.return_value = mock_resp + with patch("builtins.open", MagicMock()): + res = TLSLibrary.download() + assert res is not None diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 8233c79..24ac04f 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import tls_requests @@ -5,7 +7,7 @@ def test_http_proxy(): proxy = tls_requests.Proxy("http://localhost:8080") assert proxy.scheme == "http" assert proxy.host == "localhost" - assert proxy.port == '8080' + assert proxy.port == "8080" assert proxy.url == "http://localhost:8080" @@ -13,7 +15,7 @@ def test_https_proxy(): proxy = tls_requests.Proxy("https://localhost:8080") assert proxy.scheme == "https" assert proxy.host == "localhost" - assert proxy.port == '8080' + assert proxy.port == "8080" assert proxy.url == "https://localhost:8080" @@ -21,7 +23,7 @@ def test_socks5_proxy(): proxy = tls_requests.Proxy("socks5://localhost:8080") assert proxy.scheme == "socks5" assert proxy.host == "localhost" - assert proxy.port == '8080' + assert proxy.port == "8080" assert proxy.url == "socks5://localhost:8080" @@ -29,7 +31,7 @@ def test_proxy_with_params(): proxy = tls_requests.Proxy("http://localhost:8080?a=b", params={"foo": "bar"}) assert proxy.scheme == "http" assert proxy.host == "localhost" - assert proxy.port == '8080' + assert proxy.port == "8080" assert proxy.url == "http://localhost:8080" @@ -37,7 +39,7 @@ def test_auth_proxy(): proxy = tls_requests.Proxy("http://username:password@localhost:8080") assert proxy.scheme == "http" assert proxy.host == "localhost" - assert proxy.port == '8080' + assert proxy.port == "8080" assert proxy.auth == ("username", "password") assert proxy.url == "http://username:password@localhost:8080" @@ -47,3 +49,40 @@ def test_unsupported_proxy_scheme(): _ = tls_requests.Proxy("unknown://localhost:8080") except Exception as e: assert isinstance(e, tls_requests.exceptions.ProxyError) + + +def test_ipv6_proxy(): + # IPv6 without port + proxy = tls_requests.Proxy("http://[::1]") + assert proxy.host == "::1" + assert proxy.url == "http://[::1]" + + # IPv6 with port + proxy2 = tls_requests.Proxy("http://[2001:db8::1]:8080") + assert proxy2.host == "2001:db8::1" + assert proxy2.port == "8080" + assert proxy2.url == "http://[2001:db8::1]:8080" + + +def test_ipv6_proxy_auth(): + # IPv6 with auth and port + proxy = tls_requests.Proxy("socks5://user:pass@[::1]:1080") + assert proxy.scheme == "socks5" + assert proxy.host == "::1" + assert proxy.port == "1080" + assert proxy.auth == ("user", "pass") + assert proxy.url == "socks5://user:pass@[::1]:1080" + + +def test_ipv6_no_brackets(): + # Should handle IPv6 even if brackets are missing by default in some simple strings + # though usually URL expects brackets for IPv6. + # Our Proxy.from_string or URL may handle it if it detects it's an IP. + # Actually URL._prepare uses ipaddress.ip_address(hostname) + + # Testing Proxy.from_string with IPv6 + proxy = tls_requests.Proxy.from_string("[::1]:8080|5.0|us") + assert proxy.host == "::1" + assert proxy.weight == 5.0 + assert proxy.region == "us" + assert "[::1]:8080" in proxy.url diff --git a/tests/test_redirects.py b/tests/test_redirects.py index 3baf8f5..287408e 100644 --- a/tests/test_redirects.py +++ b/tests/test_redirects.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from pytest_httpserver import HTTPServer import tls_requests @@ -6,7 +8,9 @@ def test_missing_host_redirects(httpserver: HTTPServer): httpserver.expect_request("/redirects/3").respond_with_data(b"OK", status=302, headers={"Location": "/redirects/1"}) httpserver.expect_request("/redirects/1").respond_with_data(b"OK", status=302, headers={"Location": "/redirects/2"}) - httpserver.expect_request("/redirects/2").respond_with_data(b"OK", status=302, headers={"Location": "/redirects/ok"}) + httpserver.expect_request("/redirects/2").respond_with_data( + b"OK", status=302, headers={"Location": "/redirects/ok"} + ) httpserver.expect_request("/redirects/ok").respond_with_data(b"OK") response = tls_requests.get(httpserver.url_for("/redirects/3")) assert response.status_code == 200 @@ -15,9 +19,15 @@ def test_missing_host_redirects(httpserver: HTTPServer): def test_full_path_redirects(httpserver: HTTPServer): - httpserver.expect_request("/redirects/3").respond_with_data(b"OK", status=302, headers={"Location": httpserver.url_for("/redirects/1")}) - httpserver.expect_request("/redirects/1").respond_with_data(b"OK", status=302, headers={"Location": httpserver.url_for("/redirects/2")}) - httpserver.expect_request("/redirects/2").respond_with_data(b"OK", status=302, headers={"Location": httpserver.url_for("/redirects/ok")}) + httpserver.expect_request("/redirects/3").respond_with_data( + b"OK", status=302, headers={"Location": httpserver.url_for("/redirects/1")} + ) + httpserver.expect_request("/redirects/1").respond_with_data( + b"OK", status=302, headers={"Location": httpserver.url_for("/redirects/2")} + ) + httpserver.expect_request("/redirects/2").respond_with_data( + b"OK", status=302, headers={"Location": httpserver.url_for("/redirects/ok")} + ) httpserver.expect_request("/redirects/ok").respond_with_data(b"OK") response = tls_requests.get(httpserver.url_for("/redirects/3")) assert response.status_code == 200 @@ -26,7 +36,9 @@ def test_full_path_redirects(httpserver: HTTPServer): def test_fragment_redirects(httpserver: HTTPServer): - httpserver.expect_request("/redirects/3").respond_with_data(b"OK", status=302, headers={"Location": httpserver.url_for("/redirects/ok#fragment")}) + httpserver.expect_request("/redirects/3").respond_with_data( + b"OK", status=302, headers={"Location": httpserver.url_for("/redirects/ok#fragment")} + ) httpserver.expect_request("/redirects/ok").respond_with_data(b"OK") response = tls_requests.get(httpserver.url_for("/redirects/3")) assert response.status_code == 200 diff --git a/tests/test_request.py b/tests/test_request.py new file mode 100644 index 0000000..dcceee3 --- /dev/null +++ b/tests/test_request.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import asyncio + +from tls_requests.models.request import Request + + +def test_request_properties(): + req = Request("GET", "http://ex.com") + assert req.id == "" + assert req.content == b"" + + # repr + assert "GET" in repr(req) + assert "http://ex.com" in repr(req) + + +def test_request_aread(): + req = Request("POST", "http://ex.com", data={"a": 1}) + + async def run(): + return await req.aread() + + res = asyncio.run(run()) + assert b"a=1" in res diff --git a/tests/test_response.py b/tests/test_response.py new file mode 100644 index 0000000..f888be3 --- /dev/null +++ b/tests/test_response.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +import datetime + +import pytest + +from tls_requests.exceptions import Base64DecodeError, HTTPError +from tls_requests.models.request import Request +from tls_requests.models.response import Response +from tls_requests.models.tls import TLSResponse + + +def test_response_elapsed_setter(): + r = Response(200) + delta = datetime.timedelta(seconds=1) + r.elapsed = delta + assert r.elapsed == delta + assert r.elapsed.total_seconds() == 1 + + +def test_response_request_error(): + r = Response(200) + with pytest.raises(RuntimeError): + _ = r.request + + +def test_response_next_setter(): + r = Response(200) + req = Request("GET", "http://ex.com") + r.next = req + assert r.next is req + + +def test_response_cookies_backfill(): + req = Request("GET", "http://example.com") + # Response with cookie metadata but no domain + r = Response(200, cookies={"session": "123"}, request=req) + # The domain should be backfilled from request.url.host + c = list(r.cookies.cookiejar)[0] + assert c.domain == "example.com" + + +def test_response_charset_missing(): + r = Response(200, headers={"Content-Type": "application/json"}) + assert r.charset is None # No charset param in content-type + + r2 = Response(200) + assert r2.charset is None + + +def test_response_encoding_variations(): + r = Response(200, headers={"Content-Type": "text/html; charset=gbk,utf-8"}) + # it should take the first one + assert r.encoding == "gbk" + + r2 = Response(200, body=b"hello") + # Should fallback to utf-8 if no charset detected + assert r2.encoding == "utf-8" + + # Test callable encoding + def my_encoding(resp): + return "ascii" + + r3 = Response(200, default_encoding=my_encoding) + assert r3.encoding == "ascii" + + +def test_response_ok_and_bool(): + req = Request("GET", "http://ex.com") + r_ok = Response(200, request=req) + assert r_ok.ok is True + assert bool(r_ok) is True + + r_fail = Response(404, request=req) + assert r_fail.ok is False + assert bool(r_fail) is False + + +def test_response_is_permanent_redirect(): + r1 = Response(301, headers={"Location": "/new"}) + assert r1.is_permanent_redirect is True + r2 = Response(308, headers={"Location": "/new"}) + assert r2.is_permanent_redirect is True + r3 = Response(302, headers={"Location": "/new"}) + assert r3.is_permanent_redirect is False + + +def test_response_raise_for_status_messages(): + # Code < 100 + r1 = Response(50, body=b"TLS Error", request=Request("GET", "http://ex.com")) + with pytest.raises(HTTPError) as exc: + r1.raise_for_status() + assert "TLS Client Error" in str(exc.value) + + # 500 error + r2 = Response(500, request=Request("GET", "http://ex.com")) + with pytest.raises(HTTPError) as exc: + r2.raise_for_status() + assert "Server Error" in str(exc.value) + + +def test_response_json(): + r = Response(200, body=b'{"key": "value"}') + r.read() # Must read to populate content/text + assert r.json() == {"key": "value"} + + +def test_response_read_none_stream(): + r = Response(200) + r.stream = None + assert r.read() == b"" + + +def test_response_from_tls_response_b64_error(): + tr = TLSResponse(status=200, body="not_base64_and_has_comma,invalid", id="123") + with pytest.raises(Base64DecodeError): + Response.from_tls_response(tr, is_byte_response=True) + + +def test_response_repr(): + r = Response(200) + assert repr(r) == "" diff --git a/tests/test_rotators.py b/tests/test_rotators.py new file mode 100644 index 0000000..039fb55 --- /dev/null +++ b/tests/test_rotators.py @@ -0,0 +1,331 @@ +from __future__ import annotations + +import asyncio +import itertools +import json +from collections import Counter +from pathlib import Path + +import pytest + +from tls_requests.models.headers import Headers +from tls_requests.models.rotators import BaseRotator, HeaderRotator, ProxyRotator, RotatorError, TLSIdentifierRotator +from tls_requests.models.urls import Proxy + + +@pytest.fixture +def proxy_list_fixture(): + return ["proxy1:8000", "proxy2:8000", "proxy3:8000"] + + +@pytest.fixture +def proxy_txt_file_fixture(tmp_path: Path): + content = """ + # This is a comment, should be skipped + proxy1:8000 + proxy2:8000 + + proxy3:8000|2.5|us-east # proxy with weight and region + """ + file_path = tmp_path / "proxies.txt" + file_path.write_text(content) + return file_path + + +@pytest.fixture +def proxy_json_file_fixture(tmp_path: Path): + data = [ + {"url": "http://proxy1:8000", "weight": 1.0, "region": "eu"}, + {"url": "http://proxy2:8000", "weight": 3.0, "region": "us"}, + ] + file_path = tmp_path / "proxies.json" + file_path.write_text(json.dumps(data)) + return file_path + + +@pytest.fixture +def header_list_fixture(): + return [ + { + "Accept": "application/json", + "User-Agent": "Test-UA-1", + }, + { + "Accept": "text/html", + "User-Agent": "Test-UA-2", + }, + ] + + +class TestBaseRotator: + def test_initialization(self, proxy_list_fixture): + rotator = ProxyRotator(items=[Proxy(p) for p in proxy_list_fixture]) + assert len(rotator) == 3 + assert isinstance(rotator.items[0], Proxy) + + def test_from_file_list(self, proxy_list_fixture): + rotator = ProxyRotator.from_file(proxy_list_fixture) + assert len(rotator) == 3 + assert rotator.items[0].url == "http://proxy1:8000" + + def test_from_file_txt(self, proxy_txt_file_fixture): + rotator = ProxyRotator.from_file(proxy_txt_file_fixture) + assert len(rotator) == 3, "Blank lines and comments should be ignored." + assert rotator.items[2].weight == 2.5 + assert rotator.items[2].region == "us-east" + + def test_from_file_json(self, proxy_json_file_fixture): + rotator = ProxyRotator.from_file(proxy_json_file_fixture) + assert len(rotator) == 2 + assert rotator.items[1].url == "http://proxy2:8000" + assert rotator.items[1].weight == 3.0 + + def test_from_file_not_found(self): + with pytest.raises(FileNotFoundError): + ProxyRotator.from_file("non_existent_file.txt") + + def test_empty_rotator_raises_error(self): + rotator = ProxyRotator.from_file([]) + with pytest.raises(ValueError, match="Rotator is empty"): + rotator.next() + + @pytest.mark.asyncio + async def test_async_empty_rotator_raises_error(self): + rotator = ProxyRotator.from_file([]) + with pytest.raises(ValueError, match="Rotator is empty"): + await rotator.anext() + + def test_add_remove(self): + rotator = TLSIdentifierRotator.from_file(["chrome_120"]) + assert len(rotator) == 1 + rotator.add("firefox_120") + assert len(rotator) == 2 + assert "firefox_120" in rotator.items + rotator.remove("chrome_120") + assert len(rotator) == 1 + assert "chrome_120" not in rotator.items + + @pytest.mark.asyncio + async def test_async_add_remove(self): + rotator = TLSIdentifierRotator.from_file(["chrome_120"]) + assert len(rotator) == 1 + await rotator.aadd("firefox_120") + assert len(rotator) == 2 + assert "firefox_120" in rotator.items + await rotator.aremove("chrome_120") + assert len(rotator) == 1 + assert "chrome_120" not in rotator.items + + def test_default_strategy_is_random(self, proxy_list_fixture): + """ + Tests that the default strategy is 'random' when none is specified. + This is crucial for stateless shortcut API usage like `tls_requests.get()`. + """ + # Initialize the rotator without specifying a `strategy` + rotator = ProxyRotator.from_file(proxy_list_fixture) + assert rotator.strategy == "random" + + # Check behavior: the results should not be a predictable round-robin sequence. + # With 10 iterations, the probability of a random choice perfectly + # matching a round-robin sequence is extremely low. + results = [rotator.next() for _ in range(10)] + + # Generate the expected round-robin sequence for comparison + round_robin_cycle = itertools.cycle(rotator.items) + expected_round_robin_results = [next(round_robin_cycle) for _ in range(10)] + + assert results != expected_round_robin_results, "Default strategy produced a predictable round-robin sequence." + + +class TestRotationStrategies: + @pytest.mark.parametrize("strategy", ["round_robin", "random", "weighted"]) + def test_sync_strategies(self, strategy, proxy_list_fixture): + proxies = [Proxy(p, weight=i + 1) for i, p in enumerate(proxy_list_fixture)] + rotator = ProxyRotator(proxies, strategy=strategy) + num_iterations = 100 if strategy == "weighted" else 10 + + results = [rotator.next() for _ in range(num_iterations)] + + if strategy == "round_robin": + assert results[0].url == "http://proxy1:8000" + assert results[1].url == "http://proxy2:8000" + assert results[2].url == "http://proxy3:8000" + assert results[3].url == "http://proxy1:8000" + + elif strategy == "random": + for res in results: + assert res in proxies + + elif strategy == "weighted": + counts = Counter(p.url for p in results) + assert counts["http://proxy3:8000"] > counts["http://proxy1:8000"] + + @pytest.mark.asyncio + @pytest.mark.parametrize("strategy", ["round_robin", "random", "weighted"]) + async def test_async_strategies(self, strategy, proxy_list_fixture): + proxies = [Proxy(p, weight=i + 1) for i, p in enumerate(proxy_list_fixture)] + rotator = ProxyRotator(proxies, strategy=strategy) + num_iterations = 100 if strategy == "weighted" else 10 + + results = [await rotator.anext() for _ in range(num_iterations)] + + if strategy == "round_robin": + assert results[0].url == "http://proxy1:8000" + assert results[3].url == "http://proxy1:8000" + + elif strategy == "random": + for res in results: + assert res in proxies + + elif strategy == "weighted": + counts = Counter(p.url for p in results) + assert counts["http://proxy3:8000"] > counts["http://proxy1:8000"] + + +class TestProxyRotator: + def test_mark_result_weighted(self): + proxy = Proxy("proxy.example.com:8080", weight=2.0) + rotator = ProxyRotator([proxy], strategy="weighted") + + initial_weight = proxy.weight + rotator.mark_result(proxy, success=True) + assert proxy.weight > initial_weight + + initial_weight = proxy.weight + rotator.mark_result(proxy, success=False) + assert proxy.weight < initial_weight + + @pytest.mark.asyncio + async def test_async_mark_result_weighted(self): + proxy = Proxy("proxy.example.com:8080", weight=2.0) + rotator = ProxyRotator([proxy], strategy="weighted") + + initial_weight = proxy.weight + await rotator.amark_result(proxy, success=True) + assert proxy.weight > initial_weight + + initial_weight = proxy.weight + await rotator.amark_result(proxy, success=False) + assert proxy.weight < initial_weight + + +class TestHeaderRotator: + def test_rebuild_item(self): + item = HeaderRotator.rebuild_item({"User-Agent": "Test"}) + assert isinstance(item, Headers) + assert item["user-agent"] == "Test" + + def test_next_with_user_agent_override(self, header_list_fixture): + """ + Tests that overriding the User-Agent returns a modified COPY, + and does NOT mutate the original header object in the rotator. + This test is independent of the rotation strategy. + """ + rotator = HeaderRotator.from_file(header_list_fixture) # Uses default (random) strategy + custom_ua = "My-Custom-Bot/1.0" + + # Get a header (randomly) and override its UA + modified_header = rotator.next(user_agent=custom_ua) + assert modified_header["User-Agent"] == custom_ua + + # Find the original header object in the rotator's list that corresponds + # to the one we pulled (using the unique 'Accept' header from our fixture). + original_header_in_list = next(h for h in rotator.items if h["Accept"] == modified_header["Accept"]) + + # The most important check: ensure the original object was NOT changed. + assert original_header_in_list["User-Agent"] != custom_ua + assert "Test-UA-" in original_header_in_list["User-Agent"] + + # Ensure it is a copy, not the same object. + assert modified_header is not original_header_in_list + + @pytest.mark.asyncio + async def test_anext_with_user_agent_override(self, header_list_fixture): + """ + Tests the async version of the User-Agent override, ensuring the + original object in the rotator remains unchanged. + """ + rotator = HeaderRotator.from_file(header_list_fixture) # Uses default (random) strategy + custom_ua = "My-Custom-Bot/1.0" + + # Get a header (randomly) and override its UA + modified_header = await rotator.anext(user_agent=custom_ua) + assert modified_header["User-Agent"] == custom_ua + + # Find the original header object in the list + original_header_in_list = next(h for h in rotator.items if h["Accept"] == modified_header["Accept"]) + + # Ensure the original object was NOT changed + assert original_header_in_list["User-Agent"] != custom_ua + + +def test_base_rotator_errors(tmp_path): + class ConcreteRotator(BaseRotator): + @classmethod + def rebuild_item(cls, item): + return item + + # Unsupported source type + with pytest.raises(RotatorError): + ConcreteRotator.from_file(123) + + # Empty rotator next + rot = ConcreteRotator() + with pytest.raises(ValueError) as exc: + rot.next() + assert "Rotator is empty" in str(exc.value) + + # Async empty rotator + with pytest.raises(ValueError): + asyncio.run(rot.anext()) + + # Unsupported strategy + rot.items = ["a"] + rot.strategy = "invalid" + with pytest.raises(ValueError): + rot._rebuild_iterator() + + +def test_base_rotator_rebuild_item_not_implemented(): + class MyRotator(BaseRotator): + @classmethod + def rebuild_item(cls, item): + return super().rebuild_item(item) + + # We must instantiate it via a concrete subclass that tries to call the super method + with pytest.raises(NotImplementedError): + MyRotator.rebuild_item("test") + + +def test_proxy_rotator_extra(): + # rebuild_item Exception and None branches + assert ProxyRotator.rebuild_item(123) is None + + # Weighted strategy rebuild on result + p = Proxy("http://ex.com") + rot = ProxyRotator([p], strategy="weighted") + rot.mark_result(p, True) + # This should trigger _rebuild_iterator + assert rot._iterator is not None + + +def test_tls_identifier_rotator_extra(): + rot = TLSIdentifierRotator(["custom"]) + assert rot.items == ["custom"] + + assert rot.rebuild_item(123) is None + + +def test_header_rotator_extra(): + rot = HeaderRotator([{"User-Agent": "test"}]) + assert rot.items[0]["User-Agent"] == "test" + + assert HeaderRotator.rebuild_item(123) is None + + +def test_from_file_json(tmp_path): + p = tmp_path / "proxies.json" + p.write_text(json.dumps(["http://p1", "http://p2"])) + rot = ProxyRotator.from_file(p) + assert len(rot) == 2 + assert isinstance(rot.items[0], Proxy) diff --git a/tests/test_timeout.py b/tests/test_timeout.py deleted file mode 100644 index 030f956..0000000 --- a/tests/test_timeout.py +++ /dev/null @@ -1,16 +0,0 @@ -import time - -from pytest_httpserver import HTTPServer - -import tls_requests - - -def timeout_hook(_request, response): - time.sleep(3) - return response - - -def test_timeout(httpserver: HTTPServer): - httpserver.expect_request("/timeout").with_post_hook(timeout_hook).respond_with_data(b"OK") - response = tls_requests.get(httpserver.url_for("/timeout"), timeout=1) - assert response.status_code == 0 diff --git a/tests/test_tls.py b/tests/test_tls.py new file mode 100644 index 0000000..df96496 --- /dev/null +++ b/tests/test_tls.py @@ -0,0 +1,289 @@ +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +import pytest + +from tls_requests.models.tls import ( + CustomTLSClientConfig, + TLSClient, + TLSConfig, + TLSRequestCookiesConfig, + TLSResponse, + _BaseConfig, +) + + +def test_base_config_to_camel_case(): + assert _BaseConfig.to_camel_case("test_case") == "testCase" + assert _BaseConfig.to_camel_case("test") == "test" + assert _BaseConfig.to_camel_case("longer_test_case_name") == "longerTestCaseName" + + +def test_base_config_model_fields_set(): + from dataclasses import dataclass + + @dataclass + class SubConfig(_BaseConfig): + field1: str = "" + fieldTwo: int = 0 + + fields = SubConfig.model_fields_set() + assert "field1" in fields + assert "fieldTwo" in fields + assert "_extra_config" not in fields + + +def test_base_config_from_kwargs_and_to_dict(): + from dataclasses import dataclass + + @dataclass + class SubDataConfig(_BaseConfig): + fieldOne: str = "" + + # Pass camelCase directly or skip snake_case if it doesn't work for BaseConfig + instance = SubDataConfig.from_kwargs(fieldOne="value", extra_field="extra") + assert instance.fieldOne == "value" + assert instance._extra_config == {"extraField": "extra"} + + d = instance.to_dict() + assert d["fieldOne"] == "value" + assert d["extraField"] == "extra" + + +def test_tls_response_reason(): + resp = TLSResponse(status=200) + assert resp.reason == "OK" + assert resp.reason == resp.reason # test cached or re-calculated + + resp = TLSResponse(status=404) + assert resp.reason == "Not Found" + + +def test_tls_response_repr(): + resp = TLSResponse(status=200) + assert repr(resp) == "" + + +def test_tls_request_cookies_config(): + cookie = TLSRequestCookiesConfig(name="foo", value="bar") + assert cookie.name == "foo" + assert cookie.value == "bar" + + +def test_custom_tls_client_config(): + config = CustomTLSClientConfig.from_kwargs(alpnProtocols=["h2", "http/1.1"], ja3String="some-ja3") + assert config.alpnProtocols == ["h2", "http/1.1"] + assert config.ja3String == "some-ja3" + + +def test_tls_config_to_dict_request_body_bytes(): + config = TLSConfig(requestBody=b"hello") + d = config.to_dict() + assert d["isByteRequest"] is True + assert d["requestBody"] == "aGVsbG8=" # base64 for "hello" + + +def test_tls_config_to_dict_request_body_str(): + config = TLSConfig(requestBody="hello") + d = config.to_dict() + assert d["isByteRequest"] is False + assert d["requestBody"] == "hello" + + +def test_tls_config_copy_with(): + config = TLSConfig(sessionId="123", timeoutSeconds=10) + new_config = config.copy_with(session_id="456", timeout=20) + assert new_config.sessionId == "456" + assert new_config.timeoutSeconds == 20 + assert config.sessionId == "123" # Original unchanged + + +def test_tls_config_from_kwargs_chrome_headers(): + # Test chrome header injection + config = TLSConfig.from_kwargs(client_identifier="chrome_120") + assert "user-agent" in config.headers + assert "Chrome/120" in config.headers["user-agent"] + assert "sec-ch-ua" in config.headers + assert "120" in config.headers["sec-ch-ua"] + + +def test_tls_config_from_kwargs_firefox_headers(): + # Test firefox header injection + config = TLSConfig.from_kwargs(client_identifier="firefox_115") + assert "user-agent" in config.headers + assert "Firefox/115" in config.headers["user-agent"] + assert "rv:115" in config.headers["user-agent"] + + +def test_tls_config_from_kwargs_safari_headers(): + # Test safari header injection + config = TLSConfig.from_kwargs(client_identifier="safari_17") + assert "user-agent" in config.headers + assert "Version/17" in config.headers["user-agent"] + + +def test_tls_client_initialize_mock(): + with patch("tls_requests.models.libraries.TLSLibrary.load") as mock_load: + mock_lib = MagicMock() + mock_load.return_value = mock_lib + + # Reset TLSClient class variables for clean test + TLSClient._library = None + TLSClient._getCookiesFromSession = None + + TLSClient.initialize() + assert TLSClient._library == mock_lib + assert mock_load.called + + +def test_tls_client_destroy_all_success(): + with patch("tls_requests.models.libraries.TLSLibrary.load") as mock_load: + mock_lib = MagicMock() + mock_lib.destroyAll.return_value = b'{"success": true}' + mock_load.return_value = mock_lib + + TLSClient._library = None + TLSClient._destroyAll = None + TLSClient.initialize() + + assert TLSClient.destroy_all() is True + + +def test_tls_client_destroy_all_failure(): + with patch("tls_requests.models.libraries.TLSLibrary.load") as mock_load: + mock_lib = MagicMock() + mock_lib.destroyAll.return_value = b'{"success": false}' + mock_load.return_value = mock_lib + + TLSClient._library = None + TLSClient._destroyAll = None + TLSClient.initialize() + + assert TLSClient.destroy_all() is False + + +def test_tls_client_get_cookies(): + with patch("tls_requests.models.libraries.TLSLibrary.load") as mock_load: + mock_lib = MagicMock() + mock_lib.getCookiesFromSession.return_value = b'{"success": true, "cookies": {"a": "b"}}' + mock_load.return_value = mock_lib + + TLSClient._library = None + TLSClient._getCookiesFromSession = None + resp = TLSClient.get_cookies("sess", "http://example.com") + assert resp.success is True + assert resp.cookies == {"a": "b"} + + +def test_tls_client_add_cookies(): + with patch("tls_requests.models.libraries.TLSLibrary.load") as mock_load: + mock_lib = MagicMock() + mock_lib.addCookiesToSession.return_value = b'{"success": true}' + mock_load.return_value = mock_lib + + TLSClient._library = None + TLSClient._addCookiesToSession = None + resp = TLSClient.add_cookies("sess", {"cookie": "val"}) + assert resp.success is True + + +def test_tls_client_destroy_session(): + with patch("tls_requests.models.libraries.TLSLibrary.load") as mock_load: + mock_lib = MagicMock() + mock_lib.destroySession.return_value = b'{"success": true}' + mock_load.return_value = mock_lib + + TLSClient._library = None + TLSClient._destroySession = None + assert TLSClient.destroy_session("sess") is True + + +def test_tls_client_request(): + with patch("tls_requests.models.libraries.TLSLibrary.load") as mock_load: + mock_lib = MagicMock() + mock_lib.request.return_value = b'{"success": true, "status": 200}' + mock_load.return_value = mock_lib + + TLSClient._library = None + TLSClient._request = None + resp = TLSClient.request({"url": "http://test"}) + assert resp.status == 200 + + +def test_tls_client_free_memory(): + with patch("tls_requests.models.libraries.TLSLibrary.load") as mock_load: + mock_lib = MagicMock() + mock_lib.freeMemory.return_value = None + mock_load.return_value = mock_lib + + TLSClient._library = None + TLSClient._freeMemory = None + TLSClient.free_memory("some-id") + assert mock_lib.freeMemory.called + + +def test_tls_client_response_with_free_memory(): + with patch("tls_requests.models.libraries.TLSLibrary.load") as mock_load: + mock_lib = MagicMock() + mock_lib.freeMemory.return_value = None + mock_load.return_value = mock_lib + + TLSClient._library = None + TLSClient._freeMemory = None + # Response with an ID should trigger free_memory + raw = b'{"id": "resp-123", "status": 200}' + resp = TLSClient.response(raw) + assert resp.id == "resp-123" + assert mock_lib.freeMemory.called + + +@pytest.mark.asyncio +async def test_tls_client_async_methods(): + with patch("tls_requests.models.libraries.TLSLibrary.load") as mock_load: + mock_lib = MagicMock() + mock_lib.request.return_value = b'{"success": true, "status": 200}' + mock_load.return_value = mock_lib + + TLSClient._library = None + TLSClient._request = None + resp = await TLSClient.arequest({"url": "http://test"}) + assert resp.status == 200 + + +def test_tls_config_custom_tls_client_to_dict(): + custom = CustomTLSClientConfig(ja3String="ja3") + config = TLSConfig(customTlsClient=custom) + d = config.to_dict() + assert d.get("tlsClientIdentifier") is None + # Depending on how extra config works, we might need to check if it's in the dict + # but the logic in to_dict() explicitly sets it to None + + +def test_tls_config_http2_copy_with(): + config = TLSConfig(forceHttp1=True) + new_config = config.copy_with(http2=True) + assert new_config.forceHttp1 is False + + new_config2 = config.copy_with(http2=False) + assert new_config2.forceHttp1 is True + + +def test_tls_config_from_kwargs_unknown_browser(): + config = TLSConfig.from_kwargs(client_identifier="opera_100") + # Should not have injected headers (or at least not chrome/firefox/safari specific ones) + # Actually BROWSER_HEADERS only has chrome, firefox, safari + assert config.headers == {} + + +def test_tls_config_from_kwargs_with_headers_skips_injection(): + custom_headers = {"X-Test": "Value"} + config = TLSConfig.from_kwargs(client_identifier="chrome_120", headers=custom_headers) + assert config.headers == custom_headers + assert "user-agent" not in config.headers + + +def test_tls_response_from_bytes_empty(): + # Test from_bytes with minimal json + resp = TLSResponse.from_bytes(b'{"status": 200}') + assert resp.status == 200 diff --git a/tests/test_urls.py b/tests/test_urls.py index b21afe2..941b078 100644 --- a/tests/test_urls.py +++ b/tests/test_urls.py @@ -1,12 +1,16 @@ +from __future__ import annotations + from urllib.parse import unquote +import pytest from pytest_httpserver import HTTPServer import tls_requests +from tls_requests.models.urls import URL, Proxy, ProxyError, URLError, URLParams, URLParamsError def request_hook(_request, response): - response.headers['x-path'] = _request.full_path + response.headers["x-path"] = _request.full_path return response @@ -24,3 +28,219 @@ def test_request_multi_params(httpserver: HTTPServer): response = tls_requests.get(httpserver.url_for("/params"), params=params) assert response.status_code == 200 assert unquote(str(response.url)).endswith(unquote(response.headers["x-path"])) + + +def test_url_basic_parsing(): + url = URL("https://example.com:8080/path?q=1#fragment") + assert url.scheme == "https" + assert url.host == "example.com" + assert url.port == "8080" + assert url.path == "/path" + assert url.fragment == "fragment" + assert url.query == "q=1" + assert str(url) == "https://example.com:8080/path?q=1#fragment" + + +def test_url_ipv6(): + # Simple IPv6 + url1 = URL("http://[::1]/") + assert url1.host == "::1" + assert str(url1) == "http://[::1]/" + + # IPv6 with port and path + url2 = URL("https://[2001:db8::1]:443/api/v1") + assert url2.host == "2001:db8::1" + assert url2.port == "443" + assert str(url2) == "https://[2001:db8::1]:443/api/v1" + + +def test_url_idna(): + # Test with internationalized domain name + url = URL("https://tú-phạm.com/") + assert url.host == "xn--t-phm-7ua4524c.com" + assert "xn--t-phm-7ua4524c.com" in str(url) + + +def test_url_auth(): + url = URL("http://user:pass@example.com/") + assert url.username == "user" + assert url.password == "pass" + assert "user:pass@example.com" in str(url) + # Check secure representation + assert "[secure]" in repr(url) + assert "pass" not in repr(url) + + +def test_proxy_specifics(): + # Proxy should strip path, query, and fragment + proxy = Proxy("http://user:pass@127.0.0.1:8080/path?query=1#hash") + assert proxy.host == "127.0.0.1" + assert proxy.path == "" + assert proxy.query == "" + assert str(proxy) == "http://user:pass@127.0.0.1:8080" + + # Test allowed schemes + with pytest.raises(tls_requests.exceptions.ProxyError): + Proxy("ftp://127.0.0.1") + + # Test weight and metrics + proxy.mark_failed() + assert proxy.failures == 1 + assert proxy.weight < 1.0 + + proxy.mark_success() + assert proxy.failures == 0 + + +def test_url_params_extra(): + p = URLParams({"a": 1}) + # copy + p2 = p.copy() + assert p2["a"] == "1" + + # hash + assert hash(p) == hash(str(p)) + + # eq + assert p == {"a": "1"} + assert p == URLParams({"a": "1"}) + assert p != {"a": "2"} + assert p != 123 + + # keys/values/items + assert list(p.keys()) == ["a"] + assert list(p.values()) == ["1"] + assert list(p.items()) == [("a", "1")] + + +def test_url_params_normalize_types(): + p = URLParams() + assert p.normalize(True) == "true" + assert p.normalize(False) == "false" + assert p.normalize(b"bytes") == "bytes" + + +def test_url_ipv6_repair_and_validation(): + # 1. Automatic repair of naked IPv6 + url = URL("http://2001:db8::1:8080/path") + assert url.host == "2001:db8::1" + assert url.port == "8080" + assert str(url) == "http://[2001:db8::1]:8080/path" + + # Naked IPv6 without port + url2 = URL("2001:db8::1") + assert url2.host == "2001:db8::1" + assert str(url2) == "http://[2001:db8::1]" + + # 2. Strict validation of bracketed hosts + with pytest.raises(URLError, match="Invalid IPv6 in brackets"): + URL("http://[invalid]") + + with pytest.raises(URLError, match="Malformed bracketed host"): + URL("http://[::1]]") + + with pytest.raises(URLError, match="Invalid bracket order"): + URL("http://]::1[") + + # Valid bracketed IPv6 with port + url3 = URL("http://[::1]:9999") + assert url3.host == "::1" + assert url3.port == "9999" + + +def test_url_params_normalize_extended(): + p = URLParams() + assert p.normalize(1.5) == "1.5" + + with pytest.raises(URLParamsError): + p.normalize(None) + + # To hit invalid key type check in _prepare without triggering Python kwarg error + # we pass it in the params dict instead of kwargs + with pytest.raises(URLParamsError) as exc: + p._prepare({123: "val"}) + assert "key type" in str(exc.value) + + +def test_url_netloc_ipv6(): + u = URL("http://[::1]:8080") + assert u.host == "::1" + assert u.netloc == "[::1]:8080" + + # Host with colon but not bracketed + # Set port to empty to verify host-only netloc + u.port = "" + u.host = "my:host" + assert u.netloc == "[my:host]" + + +def test_url_query_combinations(): + u = URL("http://ex.com/p?q=1") + u.params.update({"a": "2"}) + # quote(parsed.query) + params.params + assert u.query == "q%3D1&a=2" + + u2 = URL("http://ex.com/p") + u2.params.update({"a": "2"}) + assert u2.query == "a=2" + + +def test_url_prepare_errors(): + with pytest.raises(URLError) as exc: + u = URL(b"http://ex.com") + assert u.host == "ex.com" + + URL(123) + assert "Invalid URL" in str(exc.value) + + with pytest.raises(URLError) as exc: + URL("http://invalid_idna_ÿ.com") + assert "Invalid IDNA" in str(exc.value) + + with pytest.raises(URLError) as exc: + URL("http://ex.com:70000") + assert "port range" in str(exc.value) + + +def test_proxy_extra(): + p = Proxy("127.0.0.1:8080", weight=None) + assert p.weight == 1.0 + assert "Proxy" in repr(p) + + with pytest.raises(ProxyError): + p.weight = "not a number" + + # Prepare from bytes + p2 = Proxy(b"socks5://localhost:9050") + assert p2.scheme == "socks5" + + # Prepare without scheme + p3 = Proxy("localhost:1080") + assert p3.scheme == "http" + + # Prepare with invalid scheme + with pytest.raises(ProxyError): + Proxy("ftp://localhost") + + +def test_proxy_from_methods(): + # from_dict errors + with pytest.raises(ProxyError): + Proxy.from_dict({}) + + # from_string variations + p1 = Proxy.from_string("host:8080|2.0|us") + assert p1.weight == 2.0 + assert p1.region == "us" + + p2 = Proxy.from_string(" user:pass@host:8080 ") + assert p2.host == "host" + + with pytest.raises(ProxyError): + Proxy.from_string("") + + +def test_url_build_secure(): + u = URL("http://user:pass@example.com") + assert "pass" in u._build(secure=False) + assert "[secure]" in u._build(secure=True) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..54cf5e4 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import pytest + +from tls_requests import utils + + +def test_import_module_none(): + assert utils.import_module("non_existent_module_xyz") is None + assert utils.import_module(["none1", "none2"]) is None + + +def test_get_logger_handlers(): + # Call with a new name to ensure handlers are added + logger = utils.get_logger("NewLogger") + assert len(logger.handlers) > 0 + # Call again, should not add more handlers + utils.get_logger("NewLogger") + assert len(logger.handlers) == 1 + + +def test_to_str_extra(): + assert utils.to_str(None) == "" + assert utils.to_str(b"hello", encoding="utf-8") == "hello" + assert utils.to_str(True) == "true" + assert utils.to_str({"a": 1}, encoding="ascii") == '{"a":1}' + assert utils.to_str([1, 2]) == "[1,2]" + + +def test_to_json_edge_cases(): + data = {"a": 1} + assert utils.to_json(data) is data + + # orjson/json loads error + with pytest.raises(Exception): # Broken raise in utils.py causes TypeError + utils.to_json("invalid json") + + +def test_to_base64(): + assert utils.to_base64("hello") == "aGVsbG8=" + assert utils.to_base64({"a": 1}) != "" + + +def test_json_dumps_extra(): + # If orjson is present, it pops some kwargs + if utils.orjson: + assert utils.json_dumps({"a": 1}, indent=4) == '{"a":1}' # indent popped or ignored + else: + assert '"a": 1' in utils.json_dumps({"a": 1}, indent=4) diff --git a/tls_requests/__version__.py b/tls_requests/__version__.py deleted file mode 100644 index 4951617..0000000 --- a/tls_requests/__version__.py +++ /dev/null @@ -1,7 +0,0 @@ -__title__ = "wrapper-tls-requests" -__description__ = "A powerful and lightweight Python library for making secure and reliable HTTP/TLS Fingerprint requests." -__url__ = "https://github.com/thewebscraping/tls-requests" -__author__ = "Tu Pham" -__author_email__ = "thetwofarm@gmail.com" -__version__ = "1.1.2" -__license__ = "MIT" diff --git a/tls_requests/models/__init__.py b/tls_requests/models/__init__.py deleted file mode 100644 index 30049c3..0000000 --- a/tls_requests/models/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from .auth import Auth, BasicAuth -from .cookies import Cookies -from .encoders import (JsonEncoder, MultipartEncoder, StreamEncoder, - UrlencodedEncoder) -from .headers import Headers -from .libraries import TLSLibrary -from .request import Request -from .response import Response -from .status_codes import StatusCodes -from .tls import CustomTLSClientConfig, TLSClient, TLSConfig, TLSResponse -from .urls import URL, Proxy, URLParams diff --git a/tls_requests/models/libraries.py b/tls_requests/models/libraries.py deleted file mode 100644 index ef81f67..0000000 --- a/tls_requests/models/libraries.py +++ /dev/null @@ -1,372 +0,0 @@ -import ctypes -import glob -import os -import platform -import re -import sys -from dataclasses import dataclass, field, fields -from pathlib import Path -from platform import machine -from typing import List, Optional - -import requests -from tqdm import tqdm - -__all__ = ["TLSLibrary"] - -BIN_DIR = os.path.join(Path(__file__).resolve(strict=True).parent.parent / "bin") -GITHUB_API_URL = "https://api.github.com/repos/bogdanfinn/tls-client/releases" -PLATFORM = sys.platform -IS_UBUNTU = False -ARCH_MAPPING = { - "amd64": "amd64", - "x86_64": "amd64", - "x86": "386", - "i686": "386", - "i386": "386", - "arm64": "arm64", - "aarch64": "arm64", - "armv5l": "arm-5", - "armv6l": "arm-6", - "armv7l": "arm-7", - "ppc64le": "ppc64le", - "riscv64": "riscv64", - "s390x": "s390x", -} - -FILE_EXT = ".unk" -MACHINE = ARCH_MAPPING.get(machine()) or machine() -if PLATFORM == "linux": - FILE_EXT = "so" - try: - platform_data = platform.freedesktop_os_release() - if "ID" in platform_data: - curr_system = platform_data["ID"] - else: - curr_system = platform_data.get("id") - - if "ubuntu" in str(curr_system).lower(): - IS_UBUNTU = True - - except Exception as e: # noqa - pass - -elif PLATFORM in ("win32", "cygwin"): - PLATFORM = "windows" - FILE_EXT = "dll" -elif PLATFORM == "darwin": - FILE_EXT = "dylib" - -PATTERN_RE = re.compile(r"%s-%s.*%s" % (PLATFORM, MACHINE, FILE_EXT), re.I) -PATTERN_UBUNTU_RE = re.compile(r"%s-%s.*%s" % ("ubuntu", MACHINE, FILE_EXT), re.I) - -TLS_LIBRARY_PATH = os.getenv("TLS_LIBRARY_PATH") - - -@dataclass -class BaseRelease: - - @classmethod - def model_fields_set(cls) -> set: - return {model_field.name for model_field in fields(cls)} - - @classmethod - def from_kwargs(cls, **kwargs): - model_fields_set = cls.model_fields_set() - return cls(**{k: v for k, v in kwargs.items() if k in model_fields_set}) - - -@dataclass -class ReleaseAsset(BaseRelease): - browser_download_url: str - name: Optional[str] = None - - -@dataclass -class Release(BaseRelease): - name: Optional[str] = None - tag_name: Optional[str] = None - assets: List[ReleaseAsset] = field(default_factory=list) - - @classmethod - def from_kwargs(cls, **kwargs): - model_fields_set = cls.model_fields_set() - assets = kwargs.pop("assets", []) or [] - kwargs["assets"] = [ - ReleaseAsset.from_kwargs(**asset_kwargs) for asset_kwargs in assets - ] - return cls(**{k: v for k, v in kwargs.items() if k in model_fields_set}) - - -class TLSLibrary: - """TLS Library - - A utility class for managing the TLS library, including discovery, validation, - downloading, and loading. This class facilitates interaction with system-specific - binaries, ensuring compatibility with the platform and machine architecture. - - Class Attributes: - _PATH (str): The current path to the loaded TLS library. - - Methods: - fetch_api(version: Optional[str] = None, retries: int = 3) -> Generator[str, None, None]: - Fetches library download URLs from the GitHub API for the specified version. - - is_valid(fp: str) -> bool: - Validates a file path against platform-specific patterns. - - find() -> str: - Finds the first valid library binary in the binary directory. - - find_all() -> list[str]: - Lists all library binaries in the binary directory. - - download(version: Optional[str] = None) -> str: - Downloads the library binary for the specified version. - - set_path(fp: str): - Sets the path to the currently loaded library. - - load() -> ctypes.CDLL: - Loads the library, either from an existing path or by discovering and downloading it. - """ - - _PATH: str = None - _STATIC_API_DATA = { - "name": "v1.7.10", - "tag_name": "v1.7.10", - "assets": [ - { - "browser_download_url": "https://github.com/bogdanfinn/tls-client/releases/download/v1.7.10/tls-client-darwin-amd64-1.7.10.dylib", - "name": "tls-client-darwin-amd64-1.7.10.dylib", - }, - { - "browser_download_url": "https://github.com/bogdanfinn/tls-client/releases/download/v1.7.10/tls-client-darwin-arm64-1.7.10.dylib", - "name": "tls-client-darwin-arm64-1.7.10.dylib", - }, - { - "browser_download_url": "https://github.com/bogdanfinn/tls-client/releases/download/v1.7.10/tls-client-linux-arm64-1.7.10.so", - "name": "tls-client-linux-arm64-1.7.10.so", - }, - { - "browser_download_url": "https://github.com/bogdanfinn/tls-client/releases/download/v1.7.10/tls-client-linux-armv7-1.7.10.so", - "name": "tls-client-linux-armv7-1.7.10.so", - }, - { - "browser_download_url": "https://github.com/bogdanfinn/tls-client/releases/download/v1.7.10/tls-client-linux-ubuntu-amd64-1.7.10.so", - "name": "tls-client-linux-ubuntu-amd64-1.7.10.so", - }, - { - "browser_download_url": "https://github.com/bogdanfinn/tls-client/releases/download/v1.7.10/tls-client-windows-32-1.7.10.dll", - "name": "tls-client-windows-32-1.7.10.dll", - }, - { - "browser_download_url": "https://github.com/bogdanfinn/tls-client/releases/download/v1.7.10/tls-client-windows-64-1.7.10.dll", - "name": "tls-client-windows-64-1.7.10.dll", - }, - { - "browser_download_url": "https://github.com/bogdanfinn/tls-client/releases/download/v1.7.10/tls-client-xgo-1.7.10-darwin-amd64.dylib", - "name": "tls-client-xgo-1.7.10-darwin-amd64.dylib", - }, - { - "browser_download_url": "https://github.com/bogdanfinn/tls-client/releases/download/v1.7.10/tls-client-xgo-1.7.10-darwin-arm64.dylib", - "name": "tls-client-xgo-1.7.10-darwin-arm64.dylib", - }, - { - "browser_download_url": "https://github.com/bogdanfinn/tls-client/releases/download/v1.7.10/tls-client-xgo-1.7.10-linux-386.so", - "name": "tls-client-xgo-1.7.10-linux-386.so", - }, - { - "browser_download_url": "https://github.com/bogdanfinn/tls-client/releases/download/v1.7.10/tls-client-xgo-1.7.10-linux-amd64.so", - "name": "tls-client-xgo-1.7.10-linux-amd64.so", - }, - { - "browser_download_url": "https://github.com/bogdanfinn/tls-client/releases/download/v1.7.10/tls-client-xgo-1.7.10-linux-arm-5.so", - "name": "tls-client-xgo-1.7.10-linux-arm-5.so", - }, - { - "browser_download_url": "https://github.com/bogdanfinn/tls-client/releases/download/v1.7.10/tls-client-xgo-1.7.10-linux-arm-6.so", - "name": "tls-client-xgo-1.7.10-linux-arm-6.so", - }, - { - "browser_download_url": "https://github.com/bogdanfinn/tls-client/releases/download/v1.7.10/tls-client-xgo-1.7.10-linux-arm-7.so", - "name": "tls-client-xgo-1.7.10-linux-arm-7.so", - }, - { - "browser_download_url": "https://github.com/bogdanfinn/tls-client/releases/download/v1.7.10/tls-client-xgo-1.7.10-linux-arm64.so", - "name": "tls-client-xgo-1.7.10-linux-arm64.so", - }, - { - "browser_download_url": "https://github.com/bogdanfinn/tls-client/releases/download/v1.7.10/tls-client-xgo-1.7.10-linux-ppc64le.so", - "name": "tls-client-xgo-1.7.10-linux-ppc64le.so", - }, - { - "browser_download_url": "https://github.com/bogdanfinn/tls-client/releases/download/v1.7.10/tls-client-xgo-1.7.10-linux-riscv64.so", - "name": "tls-client-xgo-1.7.10-linux-riscv64.so", - }, - { - "browser_download_url": "https://github.com/bogdanfinn/tls-client/releases/download/v1.7.10/tls-client-xgo-1.7.10-linux-s390x.so", - "name": "tls-client-xgo-1.7.10-linux-s390x.so", - }, - { - "browser_download_url": "https://github.com/bogdanfinn/tls-client/releases/download/v1.7.10/tls-client-xgo-1.7.10-windows-386.dll", - "name": "tls-client-xgo-1.7.10-windows-386.dll", - }, - { - "browser_download_url": "https://github.com/bogdanfinn/tls-client/releases/download/v1.7.10/tls-client-xgo-1.7.10-windows-amd64.dll", - "name": "tls-client-xgo-1.7.10-windows-amd64.dll", - }, - ], - } - - @classmethod - def fetch_api(cls, version: str = None, retries: int = 3): - def _find_release(data, version_: str = None): - releases = [ - Release.from_kwargs(**kwargs) for kwargs in data - ] - - if version_ is not None: - version_ = ( - "v%s" % version_ - if not str(version_).startswith("v") - else str(version_) - ) - releases = [ - release - for release in releases - if re.search(version_, release.name, re.I) - ] - - for release in releases: - for asset in release.assets: - if IS_UBUNTU and PATTERN_UBUNTU_RE.search( - asset.browser_download_url - ): - ubuntu_urls.append(asset.browser_download_url) - if PATTERN_RE.search(asset.browser_download_url): - asset_urls.append(asset.browser_download_url) - - asset_urls, ubuntu_urls = [], [] - for _ in range(retries): - try: - response = requests.get(GITHUB_API_URL) - if response.ok: - _find_release(response.json()) - break - - except Exception as ex: - print("Unable to fetch GitHub API: %s" % ex) - - if not asset_urls and not ubuntu_urls: - _find_release([cls._STATIC_API_DATA]) - - for url in ubuntu_urls: - yield url - - for url in asset_urls: - yield url - - @classmethod - def find(cls) -> str: - for fp in cls.find_all(): - if PATTERN_RE.search(fp): - return fp - - @classmethod - def find_all(cls) -> List[str]: - return [ - src - for src in glob.glob(os.path.join(BIN_DIR, r"*")) - if src.lower().endswith(("so", "dll", "dylib")) - ] - - @classmethod - def download(cls, version: str = None) -> str: - try: - print( - "System Info - Platform: %s, Machine: %s, File Ext : %s." - % ( - PLATFORM, - "%s (Ubuntu)" % MACHINE if IS_UBUNTU else MACHINE, - FILE_EXT, - ) - ) - download_url = None - for download_url in cls.fetch_api(version): - if download_url: - break - - print("Library Download URL: %s" % download_url) - if download_url: - destination = os.path.join(BIN_DIR, download_url.split("/")[-1]) - with requests.get(download_url, stream=True) as response: - response.raise_for_status() - os.makedirs(BIN_DIR, exist_ok=True) - total_size = int(response.headers.get("content-length", 0)) - chunk_size = 1024 - with open( - os.path.join(BIN_DIR, download_url.split("/")[-1]), "wb" - ) as file, tqdm( - desc=destination, - total=total_size, - unit="iB", - unit_scale=True, - unit_divisor=chunk_size, - ) as progress_bar: - for chunk in response.iter_content(chunk_size): - size = file.write(chunk) - progress_bar.update(size) - - return destination - - except requests.exceptions.HTTPError as ex: - print("Unable to download file: %s" % ex) - - @classmethod - def set_path(cls, fp: str): - cls._PATH = fp - - @classmethod - def load(cls): - """Load libraries""" - - def _load_libraries(fp_): - try: - lib = ctypes.cdll.LoadLibrary(fp_) - cls.set_path(fp_) - return lib - except Exception as ex: - print("Unable to load TLS Library, details: %s" % ex) - try: - os.remove(fp_) - except FileNotFoundError: - pass - - if cls._PATH is not None: - library = _load_libraries(cls._PATH) - if library: - return library - - if TLS_LIBRARY_PATH: - library = _load_libraries(TLS_LIBRARY_PATH) - if library: - return library - - for fp in cls.find_all(): - if IS_UBUNTU and PATTERN_UBUNTU_RE.search(fp): - library = _load_libraries(fp) - if library: - return library - if PATTERN_RE.search(fp): - library = _load_libraries(fp) - if library: - return library - - download_fp = cls.download() - if download_fp: - library = _load_libraries(download_fp) - if library: - return library - - raise OSError("Your system does not support TLS Library.") diff --git a/tls_requests/models/request.py b/tls_requests/models/request.py deleted file mode 100644 index ca961f0..0000000 --- a/tls_requests/models/request.py +++ /dev/null @@ -1,63 +0,0 @@ -from typing import Any - -from tls_requests.models.cookies import Cookies -from tls_requests.models.encoders import StreamEncoder -from tls_requests.models.headers import Headers -from tls_requests.models.urls import URL, Proxy -from tls_requests.settings import DEFAULT_TIMEOUT -from tls_requests.types import (CookieTypes, HeaderTypes, MethodTypes, - ProxyTypes, RequestData, RequestFiles, - TimeoutTypes, URLParamTypes, URLTypes) - -__all__ = ["Request"] - - -class Request: - def __init__( - self, - method: MethodTypes, - url: URLTypes, - *, - data: RequestData = None, - files: RequestFiles = None, - json: Any = None, - params: URLParamTypes = None, - headers: HeaderTypes = None, - cookies: CookieTypes = None, - proxy: ProxyTypes = None, - timeout: TimeoutTypes = None, - ) -> None: - self._content = None - self._session_id = None - self.url = URL(url, params=params) - self.method = method.upper() - self.cookies = Cookies(cookies) - self.proxy = Proxy(proxy) if proxy else None - self.timeout = timeout if isinstance(timeout, (float, int)) else DEFAULT_TIMEOUT - self.stream = StreamEncoder(data, files, json) - self.headers = self._prepare_headers(headers) - - def _prepare_headers(self, headers) -> Headers: - headers = Headers(headers) - headers.update(self.stream.headers) - if self.url.host and "Host" not in headers: - headers.setdefault(b"Host", self.url.host) - - return headers - - @property - def id(self): - return self._session_id - - @property - def content(self) -> bytes: - return self._content - - def read(self): - return b"".join(self.stream.render()) - - async def aread(self): - return b"".join(await self.stream.render()) - - def __repr__(self) -> str: - return "<%s: (%s, %s)>" % (self.__class__.__name__, self.method, self.url) diff --git a/tls_requests/models/urls.py b/tls_requests/models/urls.py deleted file mode 100644 index a2502bd..0000000 --- a/tls_requests/models/urls.py +++ /dev/null @@ -1,385 +0,0 @@ -from __future__ import annotations - -from abc import ABC -from collections.abc import Mapping -from typing import Any, ItemsView, KeysView, Union, ValuesView -from urllib.parse import ParseResult, quote, unquote, urlencode, urlparse - -import idna - -from tls_requests.exceptions import ProxyError, URLError, URLParamsError -from tls_requests.types import (URL_ALLOWED_PARAMS, ProxyTypes, URLParamTypes, - URLTypes) - -__all__ = ["URL", "URLParams", "Proxy"] - - -class URLParams(Mapping, ABC): - """URLParams - - Represents a mapping of URL parameters with utilities for normalization, encoding, and updating. - This class provides a dictionary-like interface for managing URL parameters, ensuring that keys - and values are properly validated and normalized. - - Attributes: - - params (str): Returns the encoded URL parameters as a query string. - - Methods: - - update(params: URLParamTypes = None, **kwargs): Updates the current parameters with new ones. - - keys() -> KeysView: Returns a view of the parameter keys. - - values() -> ValuesView: Returns a view of the parameter values. - - items() -> ItemsView: Returns a view of the parameter key-value pairs. - - copy() -> URLParams: Returns a copy of the current instance. - - normalize(s: URL_ALLOWED_PARAMS): Normalizes a key or value to a string. - - Raises: - - URLParamsError: Raised for invalid keys, values, or parameter types during initialization or updates. - - Example Usage: - >>> params = URLParams({'key1': 'value1', 'key2': ['value2', 'value3']}) - >>> print(str(params)) - 'key1=value1&key2=value2&key2=value3' - - >>> params.update({'key3': 'value4'}) - >>> print(params) - 'key1=value1&key2=value2&key2=value3&key3=value4' - - >>> 'key1' in params - True - """ - - def __init__(self, params: URLParamTypes = None, **kwargs): - self._data = self._prepare(params, **kwargs) - - @property - def params(self) -> str: - return str(self) - - def update(self, params: URLParamTypes = None, **kwargs): - self._data.update(self._prepare(params, **kwargs)) - return self - - def keys(self) -> KeysView: - return self._data.keys() - - def values(self) -> ValuesView: - return self._data.values() - - def items(self) -> ItemsView: - return self._data.items() - - def copy(self) -> URLParams: - return self.__class__(self._data.copy()) - - def __str__(self): - return urlencode(self._data, doseq=True) - - def __repr__(self): - return "<%s: %s>" % (self.__class__.__name__, self.items()) - - def __contains__(self, key: Any) -> bool: - return key in self._data - - def __setitem__(self, key, value): - self._data.update(self._prepare({key: value})) - - def __getitem__(self, key): - return self._data[key] - - def __delitem__(self, key): - del self._data[key] - - def __iter__(self): - return (k for k in self.keys()) - - def __len__(self) -> int: - return len(self._data) - - def __hash__(self) -> int: - return hash(str(self)) - - def __eq__(self, other) -> bool: - if not isinstance(other, self.__class__): - if isinstance(other, Mapping): - other = self.__class__(other) - else: - return False - return bool(self.params == other.params) - - def _prepare(self, params: URLParamTypes = None, **kwargs) -> Mapping: - params = params or {} - if not isinstance(params, (dict, self.__class__)): - raise URLParamsError("Invalid parameters.") - - params.update(kwargs) - for k, v in params.items(): - if not isinstance(k, (str, bytes)): - raise URLParamsError("Invalid parameters key type.") - - if isinstance(v, (list, tuple, set)): - v = [self.normalize(s) for s in v] - else: - v = self.normalize(v) - - params[self.normalize(k)] = v - return params - - def normalize(self, s: URL_ALLOWED_PARAMS): - if not isinstance(s, (str, bytes, int, float, bool)): - raise URLParamsError("Invalid parameters value type.") - - if isinstance(s, bool): - return str(s).lower() - - if isinstance(s, bytes): - return s.decode("utf-8") - - return str(s) - - -class URL: - """URL - - A utility class for parsing, manipulating, and constructing URLs. It integrates with the - `URLParams` class for managing query parameters and provides easy access to various components - of a URL, such as scheme, host, port, and path. - - Attributes: - - url (str): The raw or prepared URL string. - - params (URLParams): An instance of URLParams to manage query parameters. - - parsed (ParseResult): A `ParseResult` object containing the parsed components of the URL. - - auth (tuple): A tuple of (username, password) extracted from the URL. - - fragment (str): The fragment identifier of the URL. - - host (str): The hostname (IDNA-encoded if applicable). - - path (str): The path component of the URL. - - netloc (str): The network location (host:port if port is present). - - password (str): The password extracted from the URL. - - port (str): The port number of the URL. - - query (str): The query string, incorporating both existing and additional parameters. - - scheme (str): The URL scheme (e.g., "http", "https"). - - username (str): The username extracted from the URL. - - Methods: - - _prepare(url: Union[U, str, bytes]) -> str: Prepares and validates a URL string or bytes to ParseResult. - - _build(secure: bool = False) -> str: Constructs a URL string from its components. - - Raises: - - URLError: Raised when an invalid URL or component is encountered. - - Example Usage: - >>> url = URL("https://example.com/path?q=1#fragment", params={"key": "value"}) - >>> print(url.scheme) - 'https' - >>> print(url.host) - 'example.com' - >>> print(url.query) - 'q%3D1&key%3Dvalue' - >>> print(url.params) - 'key=value' - >>> url.params.update({'key2': 'value2'}) - >>> print(url.url) - 'https://example.com/path?q%3D1&key%3Dvalue%26key2%3Dvalue2#fragment' - >>> from urllib.parse import unquote - >>> print(unquote(url.url)) - 'https://example.com/path?q=1&key=value&key2=value2#fragment' - >>> url.url = 'https://example.org/' - >>> print(unquote(url.url)) - 'https://example.org/?key=value&key2=value2' - >>> url.url = 'https://httpbin.org/get' - >>> print(unquote(url.url)) - 'https://httpbin.org/get?key=value&key2=value2' - """ - - __attrs__ = ( - "auth", - "scheme", - "host", - "port", - "path", - "fragment", - "username", - "password", - ) - - def __init__(self, url: URLTypes, params: URLParamTypes = None, **kwargs): - self._parsed = self._prepare(url) - self._url = None - self._params = URLParams(params) - - @property - def url(self): - if self._url is None: - self._url = self._build(False) - return self._url - - @url.setter - def url(self, value): - self._parsed = self._prepare(value) - self._url = self._build(False) - - @property - def params(self): - return self._params - - @params.setter - def params(self, value): - self._url = None - self._params = URLParams(value) - - @property - def parsed(self) -> ParseResult: - return self._parsed - - @property - def netloc(self) -> str: - return ":".join([self.host, self.port]) if self.port else self.host - - @property - def query(self) -> str: - query = "" - if self.parsed.query and self.params.params: - query = "&".join([quote(self.parsed.query), self.params.params]) - elif self.params.params: - query = self.params.params - elif self.parsed.query: - query = self.parsed.query - return query - - def __str__(self): - return self._build() - - def __repr__(self): - return "<%s: %s>" % (self.__class__.__name__, unquote(self._build(True))) - - def _prepare(self, url: Union[T, str, bytes]) -> ParseResult: - if isinstance(url, bytes): - url = url.decode("utf-8") - elif isinstance(url, self.__class__) or issubclass( - self.__class__, url.__class__ - ): - url = str(url) - - if not isinstance(url, str): - raise URLError("Invalid URL: %s" % url) - - for attr in self.__attrs__: - setattr(self, attr, None) - - parsed = urlparse(url.lstrip()) - - self.auth = parsed.username, parsed.password - self.scheme = parsed.scheme - - try: - self.host = idna.encode(parsed.hostname.lower()).decode("ascii") - except AttributeError: - self.host = "" - except idna.IDNAError: - raise URLError("Invalid IDNA hostname.") - - self.port = "" - try: - if parsed.port: - self.port = str(parsed.port) - except ValueError as e: - raise URLError("%s. port range must be 0 - 65535." % e.args[0]) - - self.path = parsed.path - self.fragment = parsed.fragment - self.username = parsed.username or "" - self.password = parsed.password or "" - return parsed - - def _build(self, secure: bool = False) -> str: - urls = [self.scheme, "://"] - authority = self.netloc - if self.username or self.password: - password = self.password or "" - if secure: - password = "[secure]" - - authority = "@".join( - [ - ":".join([self.username, password]), - self.netloc, - ] - ) - - urls.append(authority) - if self.query: - urls.append("?".join([self.path, self.query])) - else: - urls.append(self.path) - - if self.fragment: - urls.append("#" + self.fragment) - - return "".join(urls) - - -class Proxy(URL): - """Proxy - - A specialized subclass of `URL` designed to handle proxy URLs with specific schemes and additional - validations. The class restricts allowed schemes to "http", "https", "socks5", and "socks5h". It - also modifies the URL construction process to focus on proxy-specific requirements. - - Attributes: - - ALLOWED_SCHEMES (tuple): A tuple of allowed schemes for the proxy ("http", "https", "socks5", "socks5h"). - Raises: - - ProxyError: Raised when an invalid proxy or unsupported protocol is encountered. - - Example Usage: - >>> proxy = Proxy("http://user:pass@127.0.0.1:8080") - >>> print(proxy.scheme) - 'http' - >>> print(proxy.netloc) - '127.0.0.1:8080' - >>> print(proxy) - 'http://user:pass@127.0.0.1:8080' - >>> print(proxy.__repr__()) - '' - - >>> socks5 = Proxy("socks5://127.0.0.1:8080") - >>> print(socks) - 'socks5://127.0.0.1:8080' - """ - - ALLOWED_SCHEMES = ("http", "https", "socks5", "socks5h") - - def _prepare(self, url: ProxyTypes) -> ParseResult: - try: - if isinstance(url, bytes): - url = url.decode("utf-8") - - if isinstance(url, str): - url = url.strip() - - parsed = super(Proxy, self)._prepare(url) - if str(parsed.scheme).lower() not in self.ALLOWED_SCHEMES: - raise ProxyError( - "Invalid proxy scheme `%s`. The allowed schemes are ('http', 'https', 'socks5', 'socks5h')." - % parsed.scheme - ) - - return urlparse("%s://%s" % (parsed.scheme, parsed.netloc)) - except URLError: - raise ProxyError("Invalid proxy: %s" % url) - - def _build(self, secure: bool = False) -> str: - urls = [self.scheme, "://"] - authority = self.netloc - if self.username or self.password: - userinfo = ":".join([self.username, self.password]) - if secure: - userinfo = "[secure]" - - authority = "@".join( - [ - userinfo, - self.netloc, - ] - ) - - urls.append(authority) - return "".join(urls) diff --git a/tls_requests/settings.py b/tls_requests/settings.py deleted file mode 100644 index ab80ff2..0000000 --- a/tls_requests/settings.py +++ /dev/null @@ -1,18 +0,0 @@ -from __future__ import annotations - -from .__version__ import __version__ - -CHUNK_SIZE = 65_536 -DEFAULT_TIMEOUT = 30.0 -DEFAULT_MAX_REDIRECTS = 9 -DEFAULT_FOLLOW_REDIRECTS = True -DEFAULT_TLS_DEBUG = False -DEFAULT_TLS_INSECURE_SKIP_VERIFY = False -DEFAULT_TLS_HTTP2 = "auto" -DEFAULT_TLS_IDENTIFIER = "chrome_120" -DEFAULT_HEADERS = { - "accept": "*/*", - "connection": "keep-alive", - "user-agent": f"Python-TLS-Requests/{__version__}", - "accept-encoding": "gzip, deflate, br, zstd", -} diff --git a/tox.ini b/tox.ini deleted file mode 100644 index a49b218..0000000 --- a/tox.ini +++ /dev/null @@ -1,9 +0,0 @@ -[tox] -envlist = py{38,39,310,311,312,313}-default - -[testenv] -deps = -r requirements-dev.txt -commands = - pytest {posargs:tests} - -[testenv:default] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..64144f7 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2770 @@ +version = 1 +revision = 3 +requires-python = ">=3.8" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version < '3.9'", +] + +[[package]] +name = "astunparse" +version = "1.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "python_full_version < '3.9'" }, + { name = "wheel", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290, upload-time = "2019-12-22T18:12:13.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732, upload-time = "2019-12-22T18:12:11.297Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[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 = "backrefs" +version = "5.7.post1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/df/30/903f35159c87ff1d92aa3fcf8cb52de97632a21e0ae43ed940f5d033e01a/backrefs-5.7.post1.tar.gz", hash = "sha256:8b0f83b770332ee2f1c8244f4e03c77d127a0fa529328e6a0e77fa25bee99678", size = 6582270, upload-time = "2024-06-16T18:38:20.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/bb/47fc255d1060dcfd55b460236380edd8ebfc5b2a42a0799ca90c9fc983e3/backrefs-5.7.post1-py310-none-any.whl", hash = "sha256:c5e3fd8fd185607a7cb1fefe878cfb09c34c0be3c18328f12c574245f1c0287e", size = 380429, upload-time = "2024-06-16T18:38:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/39ef491caef3abae945f5a5fd72830d3b596bfac0630508629283585e213/backrefs-5.7.post1-py311-none-any.whl", hash = "sha256:712ea7e494c5bf3291156e28954dd96d04dc44681d0e5c030adf2623d5606d51", size = 392234, upload-time = "2024-06-16T18:38:12.283Z" }, + { url = "https://files.pythonhosted.org/packages/6a/00/33403f581b732ca70fdebab558e8bbb426a29c34e0c3ed674a479b74beea/backrefs-5.7.post1-py312-none-any.whl", hash = "sha256:a6142201c8293e75bce7577ac29e1a9438c12e730d73a59efdd1b75528d1a6c5", size = 398110, upload-time = "2024-06-16T18:38:14.257Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ea/df0ac74a26838f6588aa012d5d801831448b87d0a7d0aefbbfabbe894870/backrefs-5.7.post1-py38-none-any.whl", hash = "sha256:ec61b1ee0a4bfa24267f6b67d0f8c5ffdc8e0d7dc2f18a2685fd1d8d9187054a", size = 369477, upload-time = "2024-06-16T18:38:16.196Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e8/e43f535c0a17a695e5768670fc855a0e5d52dc0d4135b3915bfa355f65ac/backrefs-5.7.post1-py39-none-any.whl", hash = "sha256:05c04af2bf752bb9a6c9dcebb2aff2fab372d3d9d311f2a138540e307756bd3a", size = 380429, upload-time = "2024-06-16T18:38:18.079Z" }, +] + +[[package]] +name = "backrefs" +version = "6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" }, + { url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584, upload-time = "2025-11-15T14:52:05.233Z" }, + { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" }, +] + +[[package]] +name = "cachetools" +version = "5.5.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, +] + +[[package]] +name = "cachetools" +version = "6.2.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/1d/ede8680603f6016887c062a2cf4fc8fdba905866a3ab8831aa8aa651320c/cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607", size = 31731, upload-time = "2025-12-15T18:24:53.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551, upload-time = "2025-12-15T18:24:52.332Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "chardet" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4e/3926a1c11f0433791985727965263f788af00db3482d89a7545ca5ecc921/charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", size = 198599, upload-time = "2025-10-14T04:41:53.213Z" }, + { url = "https://files.pythonhosted.org/packages/ec/7c/b92d1d1dcffc34592e71ea19c882b6709e43d20fa498042dea8b815638d7/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", size = 143090, upload-time = "2025-10-14T04:41:54.385Z" }, + { url = "https://files.pythonhosted.org/packages/84/ce/61a28d3bb77281eb24107b937a497f3c43089326d27832a63dcedaab0478/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", size = 139490, upload-time = "2025-10-14T04:41:55.551Z" }, + { url = "https://files.pythonhosted.org/packages/c0/bd/c9e59a91b2061c6f8bb98a150670cb16d4cd7c4ba7d11ad0cdf789155f41/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", size = 155334, upload-time = "2025-10-14T04:41:56.724Z" }, + { url = "https://files.pythonhosted.org/packages/bf/37/f17ae176a80f22ff823456af91ba3bc59df308154ff53aef0d39eb3d3419/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", size = 152823, upload-time = "2025-10-14T04:41:58.236Z" }, + { url = "https://files.pythonhosted.org/packages/bf/fa/cf5bb2409a385f78750e78c8d2e24780964976acdaaed65dbd6083ae5b40/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", size = 147618, upload-time = "2025-10-14T04:41:59.409Z" }, + { url = "https://files.pythonhosted.org/packages/9b/63/579784a65bc7de2d4518d40bb8f1870900163e86f17f21fd1384318c459d/charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", size = 145516, upload-time = "2025-10-14T04:42:00.579Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a9/94ec6266cd394e8f93a4d69cca651d61bf6ac58d2a0422163b30c698f2c7/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", size = 145266, upload-time = "2025-10-14T04:42:01.684Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/d6626eb97764b58c2779fa7928fa7d1a49adb8ce687c2dbba4db003c1939/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", size = 139559, upload-time = "2025-10-14T04:42:02.902Z" }, + { url = "https://files.pythonhosted.org/packages/09/01/ddbe6b01313ba191dbb0a43c7563bc770f2448c18127f9ea4b119c44dff0/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", size = 156653, upload-time = "2025-10-14T04:42:04.005Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/d05543378bea89296e9af4510b44c704626e191da447235c8fdedfc5b7b2/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", size = 145644, upload-time = "2025-10-14T04:42:05.211Z" }, + { url = "https://files.pythonhosted.org/packages/72/01/2866c4377998ef8a1f6802f6431e774a4c8ebe75b0a6e569ceec55c9cbfb/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", size = 153964, upload-time = "2025-10-14T04:42:06.341Z" }, + { url = "https://files.pythonhosted.org/packages/4a/66/66c72468a737b4cbd7851ba2c522fe35c600575fbeac944460b4fd4a06fe/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", size = 148777, upload-time = "2025-10-14T04:42:07.535Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/d0d56677fdddbffa8ca00ec411f67bb8c947f9876374ddc9d160d4f2c4b3/charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", size = 98687, upload-time = "2025-10-14T04:42:08.678Z" }, + { url = "https://files.pythonhosted.org/packages/00/64/c3bc303d1b586480b1c8e6e1e2191a6d6dd40255244e5cf16763dcec52e6/charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", size = 106115, upload-time = "2025-10-14T04:42:09.793Z" }, + { url = "https://files.pythonhosted.org/packages/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" }, + { url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" }, + { url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" }, + { url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" }, + { url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" }, + { url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" }, + { url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version < '3.9'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791, upload-time = "2024-08-04T19:45:30.9Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690, upload-time = "2024-08-04T19:43:07.695Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127, upload-time = "2024-08-04T19:43:10.15Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654, upload-time = "2024-08-04T19:43:12.405Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598, upload-time = "2024-08-04T19:43:14.078Z" }, + { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732, upload-time = "2024-08-04T19:43:16.632Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816, upload-time = "2024-08-04T19:43:19.049Z" }, + { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325, upload-time = "2024-08-04T19:43:21.246Z" }, + { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418, upload-time = "2024-08-04T19:43:22.945Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343, upload-time = "2024-08-04T19:43:25.121Z" }, + { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136, upload-time = "2024-08-04T19:43:26.851Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796, upload-time = "2024-08-04T19:43:29.115Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244, upload-time = "2024-08-04T19:43:31.285Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279, upload-time = "2024-08-04T19:43:33.581Z" }, + { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859, upload-time = "2024-08-04T19:43:35.301Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549, upload-time = "2024-08-04T19:43:37.578Z" }, + { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477, upload-time = "2024-08-04T19:43:39.92Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134, upload-time = "2024-08-04T19:43:41.453Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910, upload-time = "2024-08-04T19:43:43.037Z" }, + { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348, upload-time = "2024-08-04T19:43:44.787Z" }, + { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230, upload-time = "2024-08-04T19:43:46.707Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983, upload-time = "2024-08-04T19:43:49.082Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221, upload-time = "2024-08-04T19:43:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342, upload-time = "2024-08-04T19:43:53.746Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371, upload-time = "2024-08-04T19:43:55.993Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455, upload-time = "2024-08-04T19:43:57.618Z" }, + { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924, upload-time = "2024-08-04T19:44:00.012Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252, upload-time = "2024-08-04T19:44:01.713Z" }, + { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897, upload-time = "2024-08-04T19:44:03.898Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606, upload-time = "2024-08-04T19:44:05.532Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373, upload-time = "2024-08-04T19:44:07.079Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007, upload-time = "2024-08-04T19:44:09.453Z" }, + { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269, upload-time = "2024-08-04T19:44:11.045Z" }, + { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886, upload-time = "2024-08-04T19:44:12.83Z" }, + { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037, upload-time = "2024-08-04T19:44:15.393Z" }, + { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038, upload-time = "2024-08-04T19:44:17.466Z" }, + { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690, upload-time = "2024-08-04T19:44:19.336Z" }, + { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765, upload-time = "2024-08-04T19:44:20.994Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611, upload-time = "2024-08-04T19:44:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671, upload-time = "2024-08-04T19:44:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368, upload-time = "2024-08-04T19:44:26.276Z" }, + { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758, upload-time = "2024-08-04T19:44:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035, upload-time = "2024-08-04T19:44:30.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839, upload-time = "2024-08-04T19:44:32.412Z" }, + { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569, upload-time = "2024-08-04T19:44:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927, upload-time = "2024-08-04T19:44:36.313Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401, upload-time = "2024-08-04T19:44:38.155Z" }, + { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301, upload-time = "2024-08-04T19:44:39.883Z" }, + { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598, upload-time = "2024-08-04T19:44:41.59Z" }, + { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307, upload-time = "2024-08-04T19:44:43.301Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453, upload-time = "2024-08-04T19:44:45.677Z" }, + { url = "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674, upload-time = "2024-08-04T19:44:47.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101, upload-time = "2024-08-04T19:44:49.32Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554, upload-time = "2024-08-04T19:44:51.631Z" }, + { url = "https://files.pythonhosted.org/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440, upload-time = "2024-08-04T19:44:53.464Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889, upload-time = "2024-08-04T19:44:55.165Z" }, + { url = "https://files.pythonhosted.org/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142, upload-time = "2024-08-04T19:44:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805, upload-time = "2024-08-04T19:44:59.033Z" }, + { url = "https://files.pythonhosted.org/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655, upload-time = "2024-08-04T19:45:01.398Z" }, + { url = "https://files.pythonhosted.org/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296, upload-time = "2024-08-04T19:45:03.819Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137, upload-time = "2024-08-04T19:45:06.25Z" }, + { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688, upload-time = "2024-08-04T19:45:08.358Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120, upload-time = "2024-08-04T19:45:11.526Z" }, + { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249, upload-time = "2024-08-04T19:45:13.202Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237, upload-time = "2024-08-04T19:45:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311, upload-time = "2024-08-04T19:45:16.924Z" }, + { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453, upload-time = "2024-08-04T19:45:18.672Z" }, + { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958, upload-time = "2024-08-04T19:45:20.63Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938, upload-time = "2024-08-04T19:45:23.062Z" }, + { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352, upload-time = "2024-08-04T19:45:25.042Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153, upload-time = "2024-08-04T19:45:27.079Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926, upload-time = "2024-08-04T19:45:28.875Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version < '3.9'" }, +] + +[[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +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.9.*'" }, +] + +[[package]] +name = "coverage" +version = "7.13.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, + { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, + { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, + { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, + { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, + { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, + { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, + { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, + { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, + { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037, upload-time = "2024-09-17T19:02:01.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163, upload-time = "2024-09-17T19:02:00.268Z" }, +] + +[[package]] +name = "filelock" +version = "3.19.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffe" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "astunparse", marker = "python_full_version < '3.9'" }, + { name = "colorama", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e9/b2c86ad9d69053e497a24ceb25d661094fb321ab4ed39a8b71793dcbae82/griffe-1.4.0.tar.gz", hash = "sha256:8fccc585896d13f1221035d32c50dec65830c87d23f9adb9b1e6f3d63574f7f5", size = 381028, upload-time = "2024-10-11T12:53:54.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/7c/e9e66869c2e4c9b378474e49c993128ec0131ef4721038b6d06e50538caf/griffe-1.4.0-py3-none-any.whl", hash = "sha256:e589de8b8c137e99a46ec45f9598fc0ac5b6868ce824b24db09c02d117b89bc5", size = 127015, upload-time = "2024-10-11T12:53:52.383Z" }, +] + +[[package]] +name = "griffe" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/d7/6c09dd7ce4c7837e4cdb11dce980cb45ae3cd87677298dc3b781b6bce7d3/griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13", size = 424684, upload-time = "2025-09-05T15:02:29.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" }, +] + +[[package]] +name = "griffe" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, +] + +[[package]] +name = "identify" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/29/bb/25024dbcc93516c492b75919e76f389bac754a3e4248682fba32b250c880/identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98", size = 99097, upload-time = "2024-09-14T23:50:32.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/0c/4ef72754c050979fdcc06c744715ae70ea37e734816bb6514f79df77a42f/identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", size = 98972, upload-time = "2024-09-14T23:50:30.747Z" }, +] + +[[package]] +name = "identify" +version = "2.6.15" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, +] + +[[package]] +name = "identify" +version = "2.6.16" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, +] + +[[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 = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "zipp", version = "3.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "zipp", version = "3.23.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version < '3.9'", +] +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.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 = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "librt" +version = "0.7.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" }, + { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" }, + { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" }, + { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" }, + { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" }, + { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" }, + { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" }, + { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" }, + { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" }, + { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" }, + { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" }, + { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" }, + { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" }, + { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, + { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, + { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, + { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, + { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, + { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, + { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, + { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, + { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, + { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, + { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, + { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" }, + { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" }, + { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9b/2668bb01f568bc89ace53736df950845f8adfcacdf6da087d5cef12110cb/librt-0.7.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c7e8f88f79308d86d8f39c491773cbb533d6cb7fa6476f35d711076ee04fceb6", size = 56680, upload-time = "2026-01-14T12:56:02.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d4/dbb3edf2d0ec4ba08dcaf1865833d32737ad208962d4463c022cea6e9d3c/librt-0.7.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:389bd25a0db916e1d6bcb014f11aa9676cedaa485e9ec3752dfe19f196fd377b", size = 58612, upload-time = "2026-01-14T12:56:03.616Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/64b029de4ac9901fcd47832c650a0fd050555a452bd455ce8deddddfbb9f/librt-0.7.8-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73fd300f501a052f2ba52ede721232212f3b06503fa12665408ecfc9d8fd149c", size = 163654, upload-time = "2026-01-14T12:56:04.975Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/95e2abb1b48eb8f8c7fc2ae945321a6b82777947eb544cc785c3f37165b2/librt-0.7.8-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d772edc6a5f7835635c7562f6688e031f0b97e31d538412a852c49c9a6c92d5", size = 172477, upload-time = "2026-01-14T12:56:06.103Z" }, + { url = "https://files.pythonhosted.org/packages/7e/27/9bdf12e05b0eb089dd008d9c8aabc05748aad9d40458ade5e627c9538158/librt-0.7.8-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde8a130bd0f239e45503ab39fab239ace094d63ee1d6b67c25a63d741c0f71", size = 186220, upload-time = "2026-01-14T12:56:09.958Z" }, + { url = "https://files.pythonhosted.org/packages/53/6a/c3774f4cc95e68ed444a39f2c8bd383fd18673db7d6b98cfa709f6634b93/librt-0.7.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fdec6e2368ae4f796fc72fad7fd4bd1753715187e6d870932b0904609e7c878e", size = 183841, upload-time = "2026-01-14T12:56:11.109Z" }, + { url = "https://files.pythonhosted.org/packages/58/6b/48702c61cf83e9c04ad5cec8cad7e5e22a2cde23a13db8ef341598897ddd/librt-0.7.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:00105e7d541a8f2ee5be52caacea98a005e0478cfe78c8080fbb7b5d2b340c63", size = 179751, upload-time = "2026-01-14T12:56:12.278Z" }, + { url = "https://files.pythonhosted.org/packages/35/87/5f607fc73a131d4753f4db948833063c6aad18e18a4e6fbf64316c37ae65/librt-0.7.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c6f8947d3dfd7f91066c5b4385812c18be26c9d5a99ca56667547f2c39149d94", size = 199319, upload-time = "2026-01-14T12:56:13.425Z" }, + { url = "https://files.pythonhosted.org/packages/6e/cc/b7c5ac28ae0f0645a9681248bae4ede665bba15d6f761c291853c5c5b78e/librt-0.7.8-cp39-cp39-win32.whl", hash = "sha256:41d7bb1e07916aeb12ae4a44e3025db3691c4149ab788d0315781b4d29b86afb", size = 43434, upload-time = "2026-01-14T12:56:14.781Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5d/dce0c92f786495adf2c1e6784d9c50a52fb7feb1cfb17af97a08281a6e82/librt-0.7.8-cp39-cp39-win_amd64.whl", hash = "sha256:e90a8e237753c83b8e484d478d9a996dc5e39fd5bd4c6ce32563bc8123f132be", size = 49801, upload-time = "2026-01-14T12:56:15.827Z" }, +] + +[[package]] +name = "markdown" +version = "3.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086, upload-time = "2024-08-16T15:55:17.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349, upload-time = "2024-08-16T15:55:16.176Z" }, +] + +[[package]] +name = "markdown" +version = "3.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" }, +] + +[[package]] +name = "markdown" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload-time = "2024-02-02T16:31:22.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206, upload-time = "2024-02-02T16:30:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079, upload-time = "2024-02-02T16:30:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620, upload-time = "2024-02-02T16:30:08.31Z" }, + { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818, upload-time = "2024-02-02T16:30:09.577Z" }, + { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493, upload-time = "2024-02-02T16:30:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630, upload-time = "2024-02-02T16:30:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745, upload-time = "2024-02-02T16:30:14.222Z" }, + { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021, upload-time = "2024-02-02T16:30:16.032Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659, upload-time = "2024-02-02T16:30:17.079Z" }, + { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213, upload-time = "2024-02-02T16:30:18.251Z" }, + { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219, upload-time = "2024-02-02T16:30:19.988Z" }, + { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098, upload-time = "2024-02-02T16:30:21.063Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014, upload-time = "2024-02-02T16:30:22.926Z" }, + { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220, upload-time = "2024-02-02T16:30:24.76Z" }, + { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756, upload-time = "2024-02-02T16:30:25.877Z" }, + { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988, upload-time = "2024-02-02T16:30:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718, upload-time = "2024-02-02T16:30:28.111Z" }, + { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317, upload-time = "2024-02-02T16:30:29.214Z" }, + { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670, upload-time = "2024-02-02T16:30:30.915Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224, upload-time = "2024-02-02T16:30:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215, upload-time = "2024-02-02T16:30:33.081Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069, upload-time = "2024-02-02T16:30:34.148Z" }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452, upload-time = "2024-02-02T16:30:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462, upload-time = "2024-02-02T16:30:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869, upload-time = "2024-02-02T16:30:37.834Z" }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906, upload-time = "2024-02-02T16:30:39.366Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296, upload-time = "2024-02-02T16:30:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038, upload-time = "2024-02-02T16:30:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572, upload-time = "2024-02-02T16:30:43.326Z" }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127, upload-time = "2024-02-02T16:30:44.418Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ff/2c942a82c35a49df5de3a630ce0a8456ac2969691b230e530ac12314364c/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", size = 18192, upload-time = "2024-02-02T16:30:57.715Z" }, + { url = "https://files.pythonhosted.org/packages/4f/14/6f294b9c4f969d0c801a4615e221c1e084722ea6114ab2114189c5b8cbe0/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", size = 14072, upload-time = "2024-02-02T16:30:58.844Z" }, + { url = "https://files.pythonhosted.org/packages/81/d4/fd74714ed30a1dedd0b82427c02fa4deec64f173831ec716da11c51a50aa/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", size = 26928, upload-time = "2024-02-02T16:30:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/50319665ce81bb10e90d1cf76f9e1aa269ea6f7fa30ab4521f14d122a3df/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", size = 26106, upload-time = "2024-02-02T16:31:01.582Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6f/f2b0f675635b05f6afd5ea03c094557bdb8622fa8e673387444fe8d8e787/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68", size = 25781, upload-time = "2024-02-02T16:31:02.71Z" }, + { url = "https://files.pythonhosted.org/packages/51/e0/393467cf899b34a9d3678e78961c2c8cdf49fb902a959ba54ece01273fb1/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", size = 30518, upload-time = "2024-02-02T16:31:04.392Z" }, + { url = "https://files.pythonhosted.org/packages/f6/02/5437e2ad33047290dafced9df741d9efc3e716b75583bbd73a9984f1b6f7/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", size = 29669, upload-time = "2024-02-02T16:31:05.53Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7d/968284145ffd9d726183ed6237c77938c021abacde4e073020f920e060b2/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", size = 29933, upload-time = "2024-02-02T16:31:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f3/ecb00fc8ab02b7beae8699f34db9357ae49d9f21d4d3de6f305f34fa949e/MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", size = 16656, upload-time = "2024-02-02T16:31:07.767Z" }, + { url = "https://files.pythonhosted.org/packages/92/21/357205f03514a49b293e214ac39de01fadd0970a6e05e4bf1ddd0ffd0881/MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", size = 17206, upload-time = "2024-02-02T16:31:08.843Z" }, + { url = "https://files.pythonhosted.org/packages/0f/31/780bb297db036ba7b7bbede5e1d7f1e14d704ad4beb3ce53fb495d22bc62/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", size = 18193, upload-time = "2024-02-02T16:31:10.155Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d77701bbef72892affe060cdacb7a2ed7fd68dae3b477a8642f15ad3b132/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", size = 14073, upload-time = "2024-02-02T16:31:11.442Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a7/1e558b4f78454c8a3a0199292d96159eb4d091f983bc35ef258314fe7269/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", size = 26486, upload-time = "2024-02-02T16:31:12.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5a/360da85076688755ea0cceb92472923086993e86b5613bbae9fbc14136b0/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", size = 25685, upload-time = "2024-02-02T16:31:13.726Z" }, + { url = "https://files.pythonhosted.org/packages/6a/18/ae5a258e3401f9b8312f92b028c54d7026a97ec3ab20bfaddbdfa7d8cce8/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", size = 25338, upload-time = "2024-02-02T16:31:14.812Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cc/48206bd61c5b9d0129f4d75243b156929b04c94c09041321456fd06a876d/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", size = 30439, upload-time = "2024-02-02T16:31:15.946Z" }, + { url = "https://files.pythonhosted.org/packages/d1/06/a41c112ab9ffdeeb5f77bc3e331fdadf97fa65e52e44ba31880f4e7f983c/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", size = 29531, upload-time = "2024-02-02T16:31:17.13Z" }, + { url = "https://files.pythonhosted.org/packages/02/8c/ab9a463301a50dab04d5472e998acbd4080597abc048166ded5c7aa768c8/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", size = 29823, upload-time = "2024-02-02T16:31:18.247Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/9bc18da763496b055d8e98ce476c8e718dcfd78157e17f555ce6dd7d0895/MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", size = 16658, upload-time = "2024-02-02T16:31:19.583Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f8/4da07de16f10551ca1f640c92b5f316f9394088b183c6a57183df6de5ae4/MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", size = 17211, upload-time = "2024-02-02T16:31:20.96Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, + { url = "https://files.pythonhosted.org/packages/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" }, + { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkautodoc" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/a4/aa32093fb96c4d657cce0fbd7fa4ce28e54eb6df05ef24caf0c0c57acd2b/mkautodoc-0.2.0.tar.gz", hash = "sha256:b6e0c89804ba39d453ce5795eb040e5615756f90438ac04f47e97893a86a10cd", size = 4827, upload-time = "2022-08-03T09:10:37.084Z" } + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "jinja2" }, + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec", version = "0.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pathspec", version = "1.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag", version = "0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pyyaml-env-tag", version = "1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "watchdog", version = "4.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "watchdog", version = "6.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mkdocs", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/ae/0f1154c614d6a8b8a36fff084e5b82af3a15f7d2060cf0dcdb1c53297a71/mkdocs_autorefs-1.2.0.tar.gz", hash = "sha256:a86b93abff653521bda71cf3fc5596342b7a23982093915cb74273f67522190f", size = 40262, upload-time = "2024-09-01T18:29:18.514Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/26/4d39d52ea2219604053a4d05b98e90d6a335511cc01806436ec4886b1028/mkdocs_autorefs-1.2.0-py3-none-any.whl", hash = "sha256:d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f", size = 16522, upload-time = "2024-09-01T18:29:16.605Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markupsafe", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "mkdocs", marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/fa/9124cd63d822e2bcbea1450ae68cdc3faf3655c69b455f3a7ed36ce6c628/mkdocs_autorefs-1.4.3.tar.gz", hash = "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75", size = 55425, upload-time = "2025-08-26T14:23:17.223Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/4d/7123b6fa2278000688ebd338e2a06d16870aaf9eceae6ba047ea05f92df1/mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9", size = 25034, upload-time = "2025-08-26T14:23:15.906Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "mergedeep" }, + { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs", version = "5.7.post1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "backrefs", version = "6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions", version = "10.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pymdown-extensions", version = "10.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "requests", version = "2.32.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/e2/2ffc356cd72f1473d07c7719d82a8f2cbd261666828614ecb95b12169f41/mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8", size = 4094392, upload-time = "2025-12-18T09:49:00.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c", size = 9297166, upload-time = "2025-12-18T09:48:56.664Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "0.26.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "jinja2", marker = "python_full_version < '3.9'" }, + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mkdocs", marker = "python_full_version < '3.9'" }, + { name = "mkdocs-autorefs", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pymdown-extensions", version = "10.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/170ff04de72227f715d67da32950c7b8434449f3805b2ec3dd1085db4d7c/mkdocstrings-0.26.1.tar.gz", hash = "sha256:bb8b8854d6713d5348ad05b069a09f3b79edbc6a0f33a34c6821141adb03fe33", size = 92677, upload-time = "2024-09-06T10:26:06.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/cc/8ba127aaee5d1e9046b0d33fa5b3d17da95a9d705d44902792e0569257fd/mkdocstrings-0.26.1-py3-none-any.whl", hash = "sha256:29738bfb72b4608e8e55cc50fb8a54f325dc7ebd2014e4e3881a49892d5983cf", size = 29643, upload-time = "2024-09-06T10:26:04.498Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python", version = "1.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] + +[[package]] +name = "mkdocstrings" +version = "0.30.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "jinja2", marker = "python_full_version == '3.9.*'" }, + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "markupsafe", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "mkdocs", marker = "python_full_version == '3.9.*'" }, + { name = "mkdocs-autorefs", version = "1.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pymdown-extensions", version = "10.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/33/2fa3243439f794e685d3e694590d28469a9b8ea733af4b48c250a3ffc9a0/mkdocstrings-0.30.1.tar.gz", hash = "sha256:84a007aae9b707fb0aebfc9da23db4b26fc9ab562eb56e335e9ec480cb19744f", size = 106350, upload-time = "2025-09-19T10:49:26.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/2c/f0dc4e1ee7f618f5bff7e05898d20bf8b6e7fa612038f768bfa295f136a4/mkdocstrings-0.30.1-py3-none-any.whl", hash = "sha256:41bd71f284ca4d44a668816193e4025c950b002252081e387433656ae9a70a82", size = 36704, upload-time = "2025-09-19T10:49:24.805Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python", version = "1.18.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "jinja2", marker = "python_full_version >= '3.10'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markupsafe", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mkdocs", marker = "python_full_version >= '3.10'" }, + { name = "mkdocs-autorefs", version = "1.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pymdown-extensions", version = "10.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/ec/680e3bc7c88704d3fb9c658a517ec10f2f2aed3b9340136978675e581688/mkdocstrings-1.0.1.tar.gz", hash = "sha256:caa7d311c85ac0a0674831725ecfdeee4348e3b8a2c91ab193ee319a41dbeb3d", size = 100794, upload-time = "2026-01-19T11:36:24.429Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/f9/ecd3e5cf258d63eddc13e354bd090df3aa458b64be50d737d52a8ad9df22/mkdocstrings-1.0.1-py3-none-any.whl", hash = "sha256:10deb908e310e6d427a5b8f69026361dac06b77de860f46043043e26f121db02", size = 35245, upload-time = "2026-01-19T11:36:23.067Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.11.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "griffe", version = "1.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mkdocs-autorefs", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mkdocstrings", version = "0.26.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/ba/534c934cd0a809f51c91332d6ed278782ee4126b8ba8db02c2003f162b47/mkdocstrings_python-1.11.1.tar.gz", hash = "sha256:8824b115c5359304ab0b5378a91f6202324a849e1da907a3485b59208b797322", size = 166890, upload-time = "2024-09-03T17:20:54.904Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/f2/2a2c48fda645ac6bbe73bcc974587a579092b6868e6ff8bc6d177f4db38a/mkdocstrings_python-1.11.1-py3-none-any.whl", hash = "sha256:a21a1c05acef129a618517bb5aae3e33114f569b11588b1e7af3e9d4061a71af", size = 109297, upload-time = "2024-09-03T17:20:52.621Z" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "1.18.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "griffe", version = "1.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "mkdocs-autorefs", version = "1.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "mkdocstrings", version = "0.30.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/ae/58ab2bfbee2792e92a98b97e872f7c003deb903071f75d8d83aa55db28fa/mkdocstrings_python-1.18.2.tar.gz", hash = "sha256:4ad536920a07b6336f50d4c6d5603316fafb1172c5c882370cbbc954770ad323", size = 207972, upload-time = "2025-08-28T16:11:19.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/8f/ce008599d9adebf33ed144e7736914385e8537f5fc686fdb7cceb8c22431/mkdocstrings_python-1.18.2-py3-none-any.whl", hash = "sha256:944fe6deb8f08f33fa936d538233c4036e9f53e840994f6146e8e94eb71b600d", size = 138215, upload-time = "2025-08-28T16:11:18.176Z" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "griffe", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mkdocs-autorefs", version = "1.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mkdocstrings", version = "1.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/75/d30af27a2906f00eb90143470272376d728521997800f5dce5b340ba35bc/mkdocstrings_python-2.0.1.tar.gz", hash = "sha256:843a562221e6a471fefdd4b45cc6c22d2607ccbad632879234fa9692e9cf7732", size = 199345, upload-time = "2025-12-03T14:26:11.755Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/06/c5f8deba7d2cbdfa7967a716ae801aa9ca5f734b8f54fd473ef77a088dbe/mkdocstrings_python-2.0.1-py3-none-any.whl", hash = "sha256:66ecff45c5f8b71bf174e11d49afc845c2dfc7fc0ab17a86b6b337e0f24d8d90", size = 105055, upload-time = "2025-12-03T14:26:10.184Z" }, +] + +[[package]] +name = "mypy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "mypy-extensions", marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload-time = "2024-12-30T16:37:22.435Z" }, + { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload-time = "2024-12-30T16:37:53.526Z" }, + { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload-time = "2024-12-30T16:37:50.332Z" }, + { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload-time = "2024-12-30T16:37:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload-time = "2024-12-30T16:38:05.037Z" }, + { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload-time = "2024-12-30T16:37:19.726Z" }, + { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload-time = "2024-12-30T16:37:11.533Z" }, + { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload-time = "2024-12-30T16:37:40.724Z" }, + { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload-time = "2024-12-30T16:36:58.73Z" }, + { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload-time = "2024-12-30T16:37:03.741Z" }, + { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload-time = "2024-12-30T16:37:57.948Z" }, + { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload-time = "2024-12-30T16:37:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload-time = "2024-12-30T16:38:02.211Z" }, + { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload-time = "2024-12-30T16:37:46.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload-time = "2024-12-30T16:37:43.534Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload-time = "2024-12-30T16:37:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload-time = "2024-12-30T16:37:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload-time = "2024-12-30T16:37:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload-time = "2024-12-30T16:37:25.144Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload-time = "2024-12-30T16:38:08.634Z" }, + { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload-time = "2024-12-30T16:38:12.132Z" }, + { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload-time = "2024-12-30T16:38:17.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload-time = "2024-12-30T16:38:32.839Z" }, + { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload-time = "2024-12-30T16:38:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/39/02/1817328c1372be57c16148ce7d2bfcfa4a796bedaed897381b1aad9b267c/mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31", size = 11143050, upload-time = "2024-12-30T16:38:29.743Z" }, + { url = "https://files.pythonhosted.org/packages/b9/07/99db9a95ece5e58eee1dd87ca456a7e7b5ced6798fd78182c59c35a7587b/mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6", size = 10321087, upload-time = "2024-12-30T16:38:14.739Z" }, + { url = "https://files.pythonhosted.org/packages/9a/eb/85ea6086227b84bce79b3baf7f465b4732e0785830726ce4a51528173b71/mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319", size = 12066766, upload-time = "2024-12-30T16:38:47.038Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bb/f01bebf76811475d66359c259eabe40766d2f8ac8b8250d4e224bb6df379/mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac", size = 12787111, upload-time = "2024-12-30T16:39:02.444Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c9/84837ff891edcb6dcc3c27d85ea52aab0c4a34740ff5f0ccc0eb87c56139/mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b", size = 12974331, upload-time = "2024-12-30T16:38:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/84/5f/901e18464e6a13f8949b4909535be3fa7f823291b8ab4e4b36cfe57d6769/mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837", size = 9763210, upload-time = "2024-12-30T16:38:36.299Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493, upload-time = "2024-12-30T16:38:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702, upload-time = "2024-12-30T16:38:50.623Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104, upload-time = "2024-12-30T16:38:53.735Z" }, + { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167, upload-time = "2024-12-30T16:38:56.437Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834, upload-time = "2024-12-30T16:38:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231, upload-time = "2024-12-30T16:39:05.124Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload-time = "2024-12-30T16:38:42.021Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "librt", marker = "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions", marker = "python_full_version >= '3.9'" }, + { name = "pathspec", version = "1.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/88436084550ca9af5e610fa45286be04c3b63374df3e021c762fe8c4369f/mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3", size = 13102606, upload-time = "2025-12-15T05:02:46.833Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a5/43dfad311a734b48a752790571fd9e12d61893849a01bff346a54011957f/mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a", size = 12164496, upload-time = "2025-12-15T05:03:41.947Z" }, + { url = "https://files.pythonhosted.org/packages/88/f0/efbfa391395cce2f2771f937e0620cfd185ec88f2b9cd88711028a768e96/mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67", size = 12772068, upload-time = "2025-12-15T05:02:53.689Z" }, + { url = "https://files.pythonhosted.org/packages/25/05/58b3ba28f5aed10479e899a12d2120d582ba9fa6288851b20bf1c32cbb4f/mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e", size = 13520385, upload-time = "2025-12-15T05:02:38.328Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a0/c006ccaff50b31e542ae69b92fe7e2f55d99fba3a55e01067dd564325f85/mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376", size = 13796221, upload-time = "2025-12-15T05:03:22.147Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ff/8bdb051cd710f01b880472241bd36b3f817a8e1c5d5540d0b761675b6de2/mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24", size = 10055456, upload-time = "2025-12-15T05:03:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[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 = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "orjson" +version = "3.10.15" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/5dea21763eeff8c1590076918a446ea3d6140743e0e36f58f369928ed0f4/orjson-3.10.15.tar.gz", hash = "sha256:05ca7fe452a2e9d8d9d706a2984c95b9c2ebc5db417ce0b7a49b91d50642a23e", size = 5282482, upload-time = "2025-01-18T15:55:28.817Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/09/e5ff18ad009e6f97eb7edc5f67ef98b3ce0c189da9c3eaca1f9587cd4c61/orjson-3.10.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:552c883d03ad185f720d0c09583ebde257e41b9521b74ff40e08b7dec4559c04", size = 249532, upload-time = "2025-01-18T15:53:17.717Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b8/a75883301fe332bd433d9b0ded7d2bb706ccac679602c3516984f8814fb5/orjson-3.10.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e3e8d438d02e4854f70bfdc03a6bcdb697358dbaa6bcd19cbe24d24ece1f8", size = 125229, upload-time = "2025-01-18T18:11:48.708Z" }, + { url = "https://files.pythonhosted.org/packages/83/4b/22f053e7a364cc9c685be203b1e40fc5f2b3f164a9b2284547504eec682e/orjson-3.10.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c2c79fa308e6edb0ffab0a31fd75a7841bf2a79a20ef08a3c6e3b26814c8ca8", size = 150148, upload-time = "2025-01-18T15:53:21.254Z" }, + { url = "https://files.pythonhosted.org/packages/63/64/1b54fc75ca328b57dd810541a4035fe48c12a161d466e3cf5b11a8c25649/orjson-3.10.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cb85490aa6bf98abd20607ab5c8324c0acb48d6da7863a51be48505646c814", size = 139748, upload-time = "2025-01-18T15:53:23.629Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ff/ff0c5da781807bb0a5acd789d9a7fbcb57f7b0c6e1916595da1f5ce69f3c/orjson-3.10.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763dadac05e4e9d2bc14938a45a2d0560549561287d41c465d3c58aec818b164", size = 154559, upload-time = "2025-01-18T15:53:25.904Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9a/11e2974383384ace8495810d4a2ebef5f55aacfc97b333b65e789c9d362d/orjson-3.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a330b9b4734f09a623f74a7490db713695e13b67c959713b78369f26b3dee6bf", size = 130349, upload-time = "2025-01-18T18:11:52.164Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c4/dd9583aea6aefee1b64d3aed13f51d2aadb014028bc929fe52936ec5091f/orjson-3.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a61a4622b7ff861f019974f73d8165be1bd9a0855e1cad18ee167acacabeb061", size = 138514, upload-time = "2025-01-18T15:53:28.092Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/dcf1729230654f5c5594fc752de1f43dcf67e055ac0d300c8cdb1309269a/orjson-3.10.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd271247691574416b3228db667b84775c497b245fa275c6ab90dc1ffbbd2b3", size = 130940, upload-time = "2025-01-18T15:53:30.403Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2b/b9759fe704789937705c8a56a03f6c03e50dff7df87d65cba9a20fec5282/orjson-3.10.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4759b109c37f635aa5c5cc93a1b26927bfde24b254bcc0e1149a9fada253d2d", size = 414713, upload-time = "2025-01-18T15:53:32.779Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/b9dfdbd4b6e20a59238319eb203ae07c3f6abf07eef909169b7a37ae3bba/orjson-3.10.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e992fd5cfb8b9f00bfad2fd7a05a4299db2bbe92e6440d9dd2fab27655b3182", size = 141028, upload-time = "2025-01-18T15:53:35.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b5/40f5bbea619c7caf75eb4d652a9821875a8ed04acc45fe3d3ef054ca69fb/orjson-3.10.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f95fb363d79366af56c3f26b71df40b9a583b07bbaaf5b317407c4d58497852e", size = 129715, upload-time = "2025-01-18T15:53:36.665Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/2272514061cbdf4d672edbca6e59c7e01cd1c706e881427d88f3c3e79761/orjson-3.10.15-cp310-cp310-win32.whl", hash = "sha256:f9875f5fea7492da8ec2444839dcc439b0ef298978f311103d0b7dfd775898ab", size = 142473, upload-time = "2025-01-18T15:53:38.855Z" }, + { url = "https://files.pythonhosted.org/packages/11/5d/be1490ff7eafe7fef890eb4527cf5bcd8cfd6117f3efe42a3249ec847b60/orjson-3.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:17085a6aa91e1cd70ca8533989a18b5433e15d29c574582f76f821737c8d5806", size = 133564, upload-time = "2025-01-18T15:53:40.257Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a2/21b25ce4a2c71dbb90948ee81bd7a42b4fbfc63162e57faf83157d5540ae/orjson-3.10.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c4cc83960ab79a4031f3119cc4b1a1c627a3dc09df125b27c4201dff2af7eaa6", size = 249533, upload-time = "2025-01-18T15:53:41.572Z" }, + { url = "https://files.pythonhosted.org/packages/b2/85/2076fc12d8225698a51278009726750c9c65c846eda741e77e1761cfef33/orjson-3.10.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddbeef2481d895ab8be5185f2432c334d6dec1f5d1933a9c83014d188e102cef", size = 125230, upload-time = "2025-01-18T18:11:54.582Z" }, + { url = "https://files.pythonhosted.org/packages/06/df/a85a7955f11274191eccf559e8481b2be74a7c6d43075d0a9506aa80284d/orjson-3.10.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e590a0477b23ecd5b0ac865b1b907b01b3c5535f5e8a8f6ab0e503efb896334", size = 150148, upload-time = "2025-01-18T15:53:44.062Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/94c55625a29b8767c0eed194cb000b3787e3c23b4cdd13be17bae6ccbb4b/orjson-3.10.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6be38bd103d2fd9bdfa31c2720b23b5d47c6796bcb1d1b598e3924441b4298d", size = 139749, upload-time = "2025-01-18T15:53:45.526Z" }, + { url = "https://files.pythonhosted.org/packages/53/ba/c608b1e719971e8ddac2379f290404c2e914cf8e976369bae3cad88768b1/orjson-3.10.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ff4f6edb1578960ed628a3b998fa54d78d9bb3e2eb2cfc5c2a09732431c678d0", size = 154558, upload-time = "2025-01-18T15:53:47.712Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c4/c1fb835bb23ad788a39aa9ebb8821d51b1c03588d9a9e4ca7de5b354fdd5/orjson-3.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0482b21d0462eddd67e7fce10b89e0b6ac56570424662b685a0d6fccf581e13", size = 130349, upload-time = "2025-01-18T18:11:56.885Z" }, + { url = "https://files.pythonhosted.org/packages/78/14/bb2b48b26ab3c570b284eb2157d98c1ef331a8397f6c8bd983b270467f5c/orjson-3.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb5cc3527036ae3d98b65e37b7986a918955f85332c1ee07f9d3f82f3a6899b5", size = 138513, upload-time = "2025-01-18T15:53:50.52Z" }, + { url = "https://files.pythonhosted.org/packages/4a/97/d5b353a5fe532e92c46467aa37e637f81af8468aa894cd77d2ec8a12f99e/orjson-3.10.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d569c1c462912acdd119ccbf719cf7102ea2c67dd03b99edcb1a3048651ac96b", size = 130942, upload-time = "2025-01-18T15:53:51.894Z" }, + { url = "https://files.pythonhosted.org/packages/b5/5d/a067bec55293cca48fea8b9928cfa84c623be0cce8141d47690e64a6ca12/orjson-3.10.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1e6d33efab6b71d67f22bf2962895d3dc6f82a6273a965fab762e64fa90dc399", size = 414717, upload-time = "2025-01-18T15:53:53.215Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/1485b8b05c6b4c4db172c438cf5db5dcfd10e72a9bc23c151a1137e763e0/orjson-3.10.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c33be3795e299f565681d69852ac8c1bc5c84863c0b0030b2b3468843be90388", size = 141033, upload-time = "2025-01-18T15:53:54.664Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d2/fc67523656e43a0c7eaeae9007c8b02e86076b15d591e9be11554d3d3138/orjson-3.10.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eea80037b9fae5339b214f59308ef0589fc06dc870578b7cce6d71eb2096764c", size = 129720, upload-time = "2025-01-18T15:53:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/f58c7bd4e5b54da2ce2ef0331a39ccbbaa7699b7f70206fbf06737c9ed7d/orjson-3.10.15-cp311-cp311-win32.whl", hash = "sha256:d5ac11b659fd798228a7adba3e37c010e0152b78b1982897020a8e019a94882e", size = 142473, upload-time = "2025-01-18T15:53:58.796Z" }, + { url = "https://files.pythonhosted.org/packages/00/f8/bb60a4644287a544ec81df1699d5b965776bc9848d9029d9f9b3402ac8bb/orjson-3.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:cf45e0214c593660339ef63e875f32ddd5aa3b4adc15e662cdb80dc49e194f8e", size = 133570, upload-time = "2025-01-18T15:54:00.98Z" }, + { url = "https://files.pythonhosted.org/packages/66/85/22fe737188905a71afcc4bf7cc4c79cd7f5bbe9ed1fe0aac4ce4c33edc30/orjson-3.10.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d11c0714fc85bfcf36ada1179400862da3288fc785c30e8297844c867d7505a", size = 249504, upload-time = "2025-01-18T15:54:02.28Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/2622b29f3afebe938a0a9037e184660379797d5fd5234e5998345d7a5b43/orjson-3.10.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dba5a1e85d554e3897fa9fe6fbcff2ed32d55008973ec9a2b992bd9a65d2352d", size = 125080, upload-time = "2025-01-18T18:11:59.21Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8f/0b72a48f4403d0b88b2a41450c535b3e8989e8a2d7800659a967efc7c115/orjson-3.10.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7723ad949a0ea502df656948ddd8b392780a5beaa4c3b5f97e525191b102fff0", size = 150121, upload-time = "2025-01-18T15:54:03.998Z" }, + { url = "https://files.pythonhosted.org/packages/06/ec/acb1a20cd49edb2000be5a0404cd43e3c8aad219f376ac8c60b870518c03/orjson-3.10.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fd9bc64421e9fe9bd88039e7ce8e58d4fead67ca88e3a4014b143cec7684fd4", size = 139796, upload-time = "2025-01-18T15:54:06.551Z" }, + { url = "https://files.pythonhosted.org/packages/33/e1/f7840a2ea852114b23a52a1c0b2bea0a1ea22236efbcdb876402d799c423/orjson-3.10.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dadba0e7b6594216c214ef7894c4bd5f08d7c0135f4dd0145600be4fbcc16767", size = 154636, upload-time = "2025-01-18T15:54:08.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/da/31543337febd043b8fa80a3b67de627669b88c7b128d9ad4cc2ece005b7a/orjson-3.10.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48f59114fe318f33bbaee8ebeda696d8ccc94c9e90bc27dbe72153094e26f41", size = 130621, upload-time = "2025-01-18T18:12:00.843Z" }, + { url = "https://files.pythonhosted.org/packages/ed/78/66115dc9afbc22496530d2139f2f4455698be444c7c2475cb48f657cefc9/orjson-3.10.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:035fb83585e0f15e076759b6fedaf0abb460d1765b6a36f48018a52858443514", size = 138516, upload-time = "2025-01-18T15:54:09.413Z" }, + { url = "https://files.pythonhosted.org/packages/22/84/cd4f5fb5427ffcf823140957a47503076184cb1ce15bcc1165125c26c46c/orjson-3.10.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d13b7fe322d75bf84464b075eafd8e7dd9eae05649aa2a5354cfa32f43c59f17", size = 130762, upload-time = "2025-01-18T15:54:11.777Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/67596b711ba9f56dd75d73b60089c5c92057f1130bb3a25a0f53fb9a583b/orjson-3.10.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7066b74f9f259849629e0d04db6609db4cf5b973248f455ba5d3bd58a4daaa5b", size = 414700, upload-time = "2025-01-18T15:54:14.026Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0c/6a3b3271b46443d90efb713c3e4fe83fa8cd71cda0d11a0f69a03f437c6e/orjson-3.10.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88dc3f65a026bd3175eb157fea994fca6ac7c4c8579fc5a86fc2114ad05705b7", size = 141077, upload-time = "2025-01-18T15:54:15.612Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9b/33c58e0bfc788995eccd0d525ecd6b84b40d7ed182dd0751cd4c1322ac62/orjson-3.10.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b342567e5465bd99faa559507fe45e33fc76b9fb868a63f1642c6bc0735ad02a", size = 129898, upload-time = "2025-01-18T15:54:17.049Z" }, + { url = "https://files.pythonhosted.org/packages/01/c1/d577ecd2e9fa393366a1ea0a9267f6510d86e6c4bb1cdfb9877104cac44c/orjson-3.10.15-cp312-cp312-win32.whl", hash = "sha256:0a4f27ea5617828e6b58922fdbec67b0aa4bb844e2d363b9244c47fa2180e665", size = 142566, upload-time = "2025-01-18T15:54:18.507Z" }, + { url = "https://files.pythonhosted.org/packages/ed/eb/a85317ee1732d1034b92d56f89f1de4d7bf7904f5c8fb9dcdd5b1c83917f/orjson-3.10.15-cp312-cp312-win_amd64.whl", hash = "sha256:ef5b87e7aa9545ddadd2309efe6824bd3dd64ac101c15dae0f2f597911d46eaa", size = 133732, upload-time = "2025-01-18T15:54:20.027Z" }, + { url = "https://files.pythonhosted.org/packages/06/10/fe7d60b8da538e8d3d3721f08c1b7bff0491e8fa4dd3bf11a17e34f4730e/orjson-3.10.15-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bae0e6ec2b7ba6895198cd981b7cca95d1487d0147c8ed751e5632ad16f031a6", size = 249399, upload-time = "2025-01-18T15:54:22.46Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/52c356fd3a61abd829ae7e4366a6fe8e8863c825a60d7ac5156067516edf/orjson-3.10.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f93ce145b2db1252dd86af37d4165b6faa83072b46e3995ecc95d4b2301b725a", size = 125044, upload-time = "2025-01-18T18:12:02.747Z" }, + { url = "https://files.pythonhosted.org/packages/55/b2/d06d5901408e7ded1a74c7c20d70e3a127057a6d21355f50c90c0f337913/orjson-3.10.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c203f6f969210128af3acae0ef9ea6aab9782939f45f6fe02d05958fe761ef9", size = 150066, upload-time = "2025-01-18T15:54:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/75/8c/60c3106e08dc593a861755781c7c675a566445cc39558677d505878d879f/orjson-3.10.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8918719572d662e18b8af66aef699d8c21072e54b6c82a3f8f6404c1f5ccd5e0", size = 139737, upload-time = "2025-01-18T15:54:26.236Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8c/ae00d7d0ab8a4490b1efeb01ad4ab2f1982e69cc82490bf8093407718ff5/orjson-3.10.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f71eae9651465dff70aa80db92586ad5b92df46a9373ee55252109bb6b703307", size = 154804, upload-time = "2025-01-18T15:54:28.275Z" }, + { url = "https://files.pythonhosted.org/packages/22/86/65dc69bd88b6dd254535310e97bc518aa50a39ef9c5a2a5d518e7a223710/orjson-3.10.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e117eb299a35f2634e25ed120c37c641398826c2f5a3d3cc39f5993b96171b9e", size = 130583, upload-time = "2025-01-18T18:12:04.343Z" }, + { url = "https://files.pythonhosted.org/packages/bb/00/6fe01ededb05d52be42fabb13d93a36e51f1fd9be173bd95707d11a8a860/orjson-3.10.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13242f12d295e83c2955756a574ddd6741c81e5b99f2bef8ed8d53e47a01e4b7", size = 138465, upload-time = "2025-01-18T15:54:29.808Z" }, + { url = "https://files.pythonhosted.org/packages/db/2f/4cc151c4b471b0cdc8cb29d3eadbce5007eb0475d26fa26ed123dca93b33/orjson-3.10.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7946922ada8f3e0b7b958cc3eb22cfcf6c0df83d1fe5521b4a100103e3fa84c8", size = 130742, upload-time = "2025-01-18T15:54:31.289Z" }, + { url = "https://files.pythonhosted.org/packages/9f/13/8a6109e4b477c518498ca37963d9c0eb1508b259725553fb53d53b20e2ea/orjson-3.10.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b7155eb1623347f0f22c38c9abdd738b287e39b9982e1da227503387b81b34ca", size = 414669, upload-time = "2025-01-18T15:54:33.687Z" }, + { url = "https://files.pythonhosted.org/packages/22/7b/1d229d6d24644ed4d0a803de1b0e2df832032d5beda7346831c78191b5b2/orjson-3.10.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:208beedfa807c922da4e81061dafa9c8489c6328934ca2a562efa707e049e561", size = 141043, upload-time = "2025-01-18T15:54:35.482Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d3/6dc91156cf12ed86bed383bcb942d84d23304a1e57b7ab030bf60ea130d6/orjson-3.10.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eca81f83b1b8c07449e1d6ff7074e82e3fd6777e588f1a6632127f286a968825", size = 129826, upload-time = "2025-01-18T15:54:37.906Z" }, + { url = "https://files.pythonhosted.org/packages/b3/38/c47c25b86f6996f1343be721b6ea4367bc1c8bc0fc3f6bbcd995d18cb19d/orjson-3.10.15-cp313-cp313-win32.whl", hash = "sha256:c03cd6eea1bd3b949d0d007c8d57049aa2b39bd49f58b4b2af571a5d3833d890", size = 142542, upload-time = "2025-01-18T15:54:40.181Z" }, + { url = "https://files.pythonhosted.org/packages/27/f1/1d7ec15b20f8ce9300bc850de1e059132b88990e46cd0ccac29cbf11e4f9/orjson-3.10.15-cp313-cp313-win_amd64.whl", hash = "sha256:fd56a26a04f6ba5fb2045b0acc487a63162a958ed837648c5781e1fe3316cfbf", size = 133444, upload-time = "2025-01-18T15:54:42.076Z" }, + { url = "https://files.pythonhosted.org/packages/e8/93/7e826e2fe347bba393c60c3554a6966c09dc17613d7af2b6686348171ba9/orjson-3.10.15-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5e8afd6200e12771467a1a44e5ad780614b86abb4b11862ec54861a82d677746", size = 249866, upload-time = "2025-01-18T15:54:44.383Z" }, + { url = "https://files.pythonhosted.org/packages/6e/71/2d31ebc2f2da9249ce77dea6c31f2a7df2735fe6ec9a326096cbcc0448e9/orjson-3.10.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da9a18c500f19273e9e104cca8c1f0b40a6470bcccfc33afcc088045d0bf5ea6", size = 124917, upload-time = "2025-01-18T18:12:06.786Z" }, + { url = "https://files.pythonhosted.org/packages/32/9d/5fabd50e13580aedf22c90b888d3c4f5d86f285d5e580f0b1b91801f0c68/orjson-3.10.15-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb00b7bfbdf5d34a13180e4805d76b4567025da19a197645ca746fc2fb536586", size = 149921, upload-time = "2025-01-18T15:54:46.853Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6a/bd4226116560ab43cd439fa432d9ac1407efc7af80d1b70c36701818ff8b/orjson-3.10.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33aedc3d903378e257047fee506f11e0833146ca3e57a1a1fb0ddb789876c1e1", size = 139171, upload-time = "2025-01-18T15:54:49.224Z" }, + { url = "https://files.pythonhosted.org/packages/3f/55/587ceaaaefd8d3faec3c4d0b2acdae1761b3a9e3ec928d836374b5a05c13/orjson-3.10.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0099ae6aed5eb1fc84c9eb72b95505a3df4267e6962eb93cdd5af03be71c98", size = 154203, upload-time = "2025-01-18T15:54:50.891Z" }, + { url = "https://files.pythonhosted.org/packages/72/3c/2e26157d69d127c5663cdaa53a31860ca0df0a9a89ece81c81800ef99490/orjson-3.10.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c864a80a2d467d7786274fce0e4f93ef2a7ca4ff31f7fc5634225aaa4e9e98c", size = 130068, upload-time = "2025-01-18T18:12:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/a7/93/37590ace084c984e127c7910e76d08ef34af558eee48e75765c0c99104a2/orjson-3.10.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c25774c9e88a3e0013d7d1a6c8056926b607a61edd423b50eb5c88fd7f2823ae", size = 138099, upload-time = "2025-01-18T15:54:52.457Z" }, + { url = "https://files.pythonhosted.org/packages/17/37/719d7f2d545aac188aa1f4d90d1de2d5d8e48bec39134b6b226ac7cc5d94/orjson-3.10.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:e78c211d0074e783d824ce7bb85bf459f93a233eb67a5b5003498232ddfb0e8a", size = 130599, upload-time = "2025-01-18T15:54:54.051Z" }, + { url = "https://files.pythonhosted.org/packages/ef/82/e6697f15f1c2303b575837904d25d3faf86d83fa3e3fabd113b4b8dff39a/orjson-3.10.15-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:43e17289ffdbbac8f39243916c893d2ae41a2ea1a9cbb060a56a4d75286351ae", size = 414435, upload-time = "2025-01-18T15:54:56.539Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f1/51a2ec98822c474d0a24d0a9f490c94f22c9ced35665e106c8b4c89916ad/orjson-3.10.15-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:781d54657063f361e89714293c095f506c533582ee40a426cb6489c48a637b81", size = 140676, upload-time = "2025-01-18T15:54:58.073Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fb/1d868dd8b364a7709cc15aa073bfa9727183a2c800bf07343baa00dd3d15/orjson-3.10.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6875210307d36c94873f553786a808af2788e362bd0cf4c8e66d976791e7b528", size = 129478, upload-time = "2025-01-18T15:54:59.694Z" }, + { url = "https://files.pythonhosted.org/packages/13/1f/7f01afc23a7dee108baa31fbaa8e8d3f6b56b915201bf079c9586b37680a/orjson-3.10.15-cp38-cp38-win32.whl", hash = "sha256:305b38b2b8f8083cc3d618927d7f424349afce5975b316d33075ef0f73576b60", size = 142231, upload-time = "2025-01-18T15:55:01.281Z" }, + { url = "https://files.pythonhosted.org/packages/72/78/11d6afa317d3c7ee3c35b3a70e91776bf60ea9f010b629cc40d4a00edde8/orjson-3.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:5dd9ef1639878cc3efffed349543cbf9372bdbd79f478615a1c633fe4e4180d1", size = 133278, upload-time = "2025-01-18T15:55:03.156Z" }, + { url = "https://files.pythonhosted.org/packages/56/39/b2123d8d98a62ee89626dc7ecb782d9b60a5edb0b5721bc894ee3470df5a/orjson-3.10.15-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ffe19f3e8d68111e8644d4f4e267a069ca427926855582ff01fc012496d19969", size = 250031, upload-time = "2025-01-18T15:55:05.697Z" }, + { url = "https://files.pythonhosted.org/packages/65/4d/a058dc6476713cbd5647e5fd0be8d40c27e9ed77d37a788b594c424caa0e/orjson-3.10.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d433bf32a363823863a96561a555227c18a522a8217a6f9400f00ddc70139ae2", size = 125021, upload-time = "2025-01-18T18:12:11.807Z" }, + { url = "https://files.pythonhosted.org/packages/3d/cb/4d1450bb2c3276f8bf9524df6b01af4d01f55e9a9772555cf119275eb1d0/orjson-3.10.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da03392674f59a95d03fa5fb9fe3a160b0511ad84b7a3914699ea5a1b3a38da2", size = 149957, upload-time = "2025-01-18T15:55:08.843Z" }, + { url = "https://files.pythonhosted.org/packages/93/7b/d1fae6d4393a9fa8f5d3fb173f0a9c778135569c50e5390811b74c45b4b3/orjson-3.10.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a63bb41559b05360ded9132032239e47983a39b151af1201f07ec9370715c82", size = 139515, upload-time = "2025-01-18T15:55:10.567Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b2/e0c0b8197c709983093700f9a59aa64478d80edc55fe620bceadb92004e3/orjson-3.10.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3766ac4702f8f795ff3fa067968e806b4344af257011858cc3d6d8721588b53f", size = 154314, upload-time = "2025-01-18T15:55:12.196Z" }, + { url = "https://files.pythonhosted.org/packages/db/94/eeb94ca3aa7564f753fe352101bcfc8179febaa1888f55ba3cad25b05f71/orjson-3.10.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a1c73dcc8fadbd7c55802d9aa093b36878d34a3b3222c41052ce6b0fc65f8e8", size = 130145, upload-time = "2025-01-18T18:12:13.477Z" }, + { url = "https://files.pythonhosted.org/packages/ca/10/54c0118a38eaa5ae832c27306834bdc13954bd0a443b80da63faebf17ffe/orjson-3.10.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b299383825eafe642cbab34be762ccff9fd3408d72726a6b2a4506d410a71ab3", size = 138344, upload-time = "2025-01-18T15:55:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/3c15eeb315171aa27f96bcca87ed54ee292b72d755973a66e3a6800e8ae9/orjson-3.10.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:abc7abecdbf67a173ef1316036ebbf54ce400ef2300b4e26a7b843bd446c2480", size = 130730, upload-time = "2025-01-18T15:55:15.402Z" }, + { url = "https://files.pythonhosted.org/packages/8a/dc/522430fb24445b9cc8301a5954f80ce8ee244c5159ba913578acc36b078f/orjson-3.10.15-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:3614ea508d522a621384c1d6639016a5a2e4f027f3e4a1c93a51867615d28829", size = 414482, upload-time = "2025-01-18T15:55:16.989Z" }, + { url = "https://files.pythonhosted.org/packages/c8/01/83b2e80b9c96ca9753d06e01d325037b2f3e404b14c7a8e875b2f2b7c171/orjson-3.10.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:295c70f9dc154307777ba30fe29ff15c1bcc9dfc5c48632f37d20a607e9ba85a", size = 140792, upload-time = "2025-01-18T15:55:18.731Z" }, + { url = "https://files.pythonhosted.org/packages/96/40/f211084b0e0267b6b515f05967048d8957839d80ff534bde0dc7f9df9ae0/orjson-3.10.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:63309e3ff924c62404923c80b9e2048c1f74ba4b615e7584584389ada50ed428", size = 129536, upload-time = "2025-01-18T15:55:21.306Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8c/014d96f5c6446adcd2403fe2d4007ff582f8867f5028b0cd994f0174d61c/orjson-3.10.15-cp39-cp39-win32.whl", hash = "sha256:a2f708c62d026fb5340788ba94a55c23df4e1869fec74be455e0b2f5363b8507", size = 142302, upload-time = "2025-01-18T15:55:25.094Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/81da73ef8e66434c51a4ea7db45e3a0b62bff2c3e7ebc723aa4eeead2feb/orjson-3.10.15-cp39-cp39-win_amd64.whl", hash = "sha256:efcf6c735c3d22ef60c4aa27a5238f1a477df85e9b15f2142f9d669beb2d13fd", size = 133401, upload-time = "2025-01-18T15:55:26.953Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5", size = 5972347, upload-time = "2025-12-06T15:55:39.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/19/b22cf9dad4db20c8737041046054cbd4f38bb5a2d0e4bb60487832ce3d76/orjson-3.11.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:df9eadb2a6386d5ea2bfd81309c505e125cfc9ba2b1b99a97e60985b0b3665d1", size = 245719, upload-time = "2025-12-06T15:53:43.877Z" }, + { url = "https://files.pythonhosted.org/packages/03/2e/b136dd6bf30ef5143fbe76a4c142828b55ccc618be490201e9073ad954a1/orjson-3.11.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc70da619744467d8f1f49a8cadae5ec7bbe054e5232d95f92ed8737f8c5870", size = 132467, upload-time = "2025-12-06T15:53:45.379Z" }, + { url = "https://files.pythonhosted.org/packages/ae/fc/ae99bfc1e1887d20a0268f0e2686eb5b13d0ea7bbe01de2b566febcd2130/orjson-3.11.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:073aab025294c2f6fc0807201c76fdaed86f8fc4be52c440fb78fbb759a1ac09", size = 130702, upload-time = "2025-12-06T15:53:46.659Z" }, + { url = "https://files.pythonhosted.org/packages/6e/43/ef7912144097765997170aca59249725c3ab8ef6079f93f9d708dd058df5/orjson-3.11.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:835f26fa24ba0bb8c53ae2a9328d1706135b74ec653ed933869b74b6909e63fd", size = 135907, upload-time = "2025-12-06T15:53:48.487Z" }, + { url = "https://files.pythonhosted.org/packages/3f/da/24d50e2d7f4092ddd4d784e37a3fa41f22ce8ed97abc9edd222901a96e74/orjson-3.11.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667c132f1f3651c14522a119e4dd631fad98761fa960c55e8e7430bb2a1ba4ac", size = 139935, upload-time = "2025-12-06T15:53:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/02/4a/b4cb6fcbfff5b95a3a019a8648255a0fac9b221fbf6b6e72be8df2361feb/orjson-3.11.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42e8961196af655bb5e63ce6c60d25e8798cd4dfbc04f4203457fa3869322c2e", size = 137541, upload-time = "2025-12-06T15:53:51.226Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/a11bd129f18c2377c27b2846a9d9be04acec981f770d711ba0aaea563984/orjson-3.11.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75412ca06e20904c19170f8a24486c4e6c7887dea591ba18a1ab572f1300ee9f", size = 139031, upload-time = "2025-12-06T15:53:52.309Z" }, + { url = "https://files.pythonhosted.org/packages/64/29/d7b77d7911574733a036bb3e8ad7053ceb2b7d6ea42208b9dbc55b23b9ed/orjson-3.11.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6af8680328c69e15324b5af3ae38abbfcf9cbec37b5346ebfd52339c3d7e8a18", size = 141622, upload-time = "2025-12-06T15:53:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/93/41/332db96c1de76b2feda4f453e91c27202cd092835936ce2b70828212f726/orjson-3.11.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a86fe4ff4ea523eac8f4b57fdac319faf037d3c1be12405e6a7e86b3fbc4756a", size = 413800, upload-time = "2025-12-06T15:53:54.866Z" }, + { url = "https://files.pythonhosted.org/packages/76/e1/5a0d148dd1f89ad2f9651df67835b209ab7fcb1118658cf353425d7563e9/orjson-3.11.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e607b49b1a106ee2086633167033afbd63f76f2999e9236f638b06b112b24ea7", size = 151198, upload-time = "2025-12-06T15:53:56.383Z" }, + { url = "https://files.pythonhosted.org/packages/0d/96/8db67430d317a01ae5cf7971914f6775affdcfe99f5bff9ef3da32492ecc/orjson-3.11.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7339f41c244d0eea251637727f016b3d20050636695bc78345cce9029b189401", size = 141984, upload-time = "2025-12-06T15:53:57.746Z" }, + { url = "https://files.pythonhosted.org/packages/71/49/40d21e1aa1ac569e521069228bb29c9b5a350344ccf922a0227d93c2ed44/orjson-3.11.5-cp310-cp310-win32.whl", hash = "sha256:8be318da8413cdbbce77b8c5fac8d13f6eb0f0db41b30bb598631412619572e8", size = 135272, upload-time = "2025-12-06T15:53:59.769Z" }, + { url = "https://files.pythonhosted.org/packages/c4/7e/d0e31e78be0c100e08be64f48d2850b23bcb4d4c70d114f4e43b39f6895a/orjson-3.11.5-cp310-cp310-win_amd64.whl", hash = "sha256:b9f86d69ae822cabc2a0f6c099b43e8733dda788405cba2665595b7e8dd8d167", size = 133360, upload-time = "2025-12-06T15:54:01.25Z" }, + { url = "https://files.pythonhosted.org/packages/fd/68/6b3659daec3a81aed5ab47700adb1a577c76a5452d35b91c88efee89987f/orjson-3.11.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9c8494625ad60a923af6b2b0bd74107146efe9b55099e20d7740d995f338fcd8", size = 245318, upload-time = "2025-12-06T15:54:02.355Z" }, + { url = "https://files.pythonhosted.org/packages/e9/00/92db122261425f61803ccf0830699ea5567439d966cbc35856fe711bfe6b/orjson-3.11.5-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:7bb2ce0b82bc9fd1168a513ddae7a857994b780b2945a8c51db4ab1c4b751ebc", size = 129491, upload-time = "2025-12-06T15:54:03.877Z" }, + { url = "https://files.pythonhosted.org/packages/94/4f/ffdcb18356518809d944e1e1f77589845c278a1ebbb5a8297dfefcc4b4cb/orjson-3.11.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67394d3becd50b954c4ecd24ac90b5051ee7c903d167459f93e77fc6f5b4c968", size = 132167, upload-time = "2025-12-06T15:54:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/97/c6/0a8caff96f4503f4f7dd44e40e90f4d14acf80d3b7a97cb88747bb712d3e/orjson-3.11.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:298d2451f375e5f17b897794bcc3e7b821c0f32b4788b9bcae47ada24d7f3cf7", size = 130516, upload-time = "2025-12-06T15:54:06.274Z" }, + { url = "https://files.pythonhosted.org/packages/4d/63/43d4dc9bd9954bff7052f700fdb501067f6fb134a003ddcea2a0bb3854ed/orjson-3.11.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa5e4244063db8e1d87e0f54c3f7522f14b2dc937e65d5241ef0076a096409fd", size = 135695, upload-time = "2025-12-06T15:54:07.702Z" }, + { url = "https://files.pythonhosted.org/packages/87/6f/27e2e76d110919cb7fcb72b26166ee676480a701bcf8fc53ac5d0edce32f/orjson-3.11.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1db2088b490761976c1b2e956d5d4e6409f3732e9d79cfa69f876c5248d1baf9", size = 139664, upload-time = "2025-12-06T15:54:08.828Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/5966153a5f1be49b5fbb8ca619a529fde7bc71aa0a376f2bb83fed248bcd/orjson-3.11.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2ed66358f32c24e10ceea518e16eb3549e34f33a9d51f99ce23b0251776a1ef", size = 137289, upload-time = "2025-12-06T15:54:09.898Z" }, + { url = "https://files.pythonhosted.org/packages/a7/34/8acb12ff0299385c8bbcbb19fbe40030f23f15a6de57a9c587ebf71483fb/orjson-3.11.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2021afda46c1ed64d74b555065dbd4c2558d510d8cec5ea6a53001b3e5e82a9", size = 138784, upload-time = "2025-12-06T15:54:11.022Z" }, + { url = "https://files.pythonhosted.org/packages/ee/27/910421ea6e34a527f73d8f4ee7bdffa48357ff79c7b8d6eb6f7b82dd1176/orjson-3.11.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b42ffbed9128e547a1647a3e50bc88ab28ae9daa61713962e0d3dd35e820c125", size = 141322, upload-time = "2025-12-06T15:54:12.427Z" }, + { url = "https://files.pythonhosted.org/packages/87/a3/4b703edd1a05555d4bb1753d6ce44e1a05b7a6d7c164d5b332c795c63d70/orjson-3.11.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8d5f16195bb671a5dd3d1dbea758918bada8f6cc27de72bd64adfbd748770814", size = 413612, upload-time = "2025-12-06T15:54:13.858Z" }, + { url = "https://files.pythonhosted.org/packages/1b/36/034177f11d7eeea16d3d2c42a1883b0373978e08bc9dad387f5074c786d8/orjson-3.11.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c0e5d9f7a0227df2927d343a6e3859bebf9208b427c79bd31949abcc2fa32fa5", size = 150993, upload-time = "2025-12-06T15:54:15.189Z" }, + { url = "https://files.pythonhosted.org/packages/44/2f/ea8b24ee046a50a7d141c0227c4496b1180b215e728e3b640684f0ea448d/orjson-3.11.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:23d04c4543e78f724c4dfe656b3791b5f98e4c9253e13b2636f1af5d90e4a880", size = 141774, upload-time = "2025-12-06T15:54:16.451Z" }, + { url = "https://files.pythonhosted.org/packages/8a/12/cc440554bf8200eb23348a5744a575a342497b65261cd65ef3b28332510a/orjson-3.11.5-cp311-cp311-win32.whl", hash = "sha256:c404603df4865f8e0afe981aa3c4b62b406e6d06049564d58934860b62b7f91d", size = 135109, upload-time = "2025-12-06T15:54:17.73Z" }, + { url = "https://files.pythonhosted.org/packages/a3/83/e0c5aa06ba73a6760134b169f11fb970caa1525fa4461f94d76e692299d9/orjson-3.11.5-cp311-cp311-win_amd64.whl", hash = "sha256:9645ef655735a74da4990c24ffbd6894828fbfa117bc97c1edd98c282ecb52e1", size = 133193, upload-time = "2025-12-06T15:54:19.426Z" }, + { url = "https://files.pythonhosted.org/packages/cb/35/5b77eaebc60d735e832c5b1a20b155667645d123f09d471db0a78280fb49/orjson-3.11.5-cp311-cp311-win_arm64.whl", hash = "sha256:1cbf2735722623fcdee8e712cbaaab9e372bbcb0c7924ad711b261c2eccf4a5c", size = 126830, upload-time = "2025-12-06T15:54:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a4/8052a029029b096a78955eadd68ab594ce2197e24ec50e6b6d2ab3f4e33b/orjson-3.11.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d", size = 245347, upload-time = "2025-12-06T15:54:22.061Z" }, + { url = "https://files.pythonhosted.org/packages/64/67/574a7732bd9d9d79ac620c8790b4cfe0717a3d5a6eb2b539e6e8995e24a0/orjson-3.11.5-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626", size = 129435, upload-time = "2025-12-06T15:54:23.615Z" }, + { url = "https://files.pythonhosted.org/packages/52/8d/544e77d7a29d90cf4d9eecd0ae801c688e7f3d1adfa2ebae5e1e94d38ab9/orjson-3.11.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f", size = 132074, upload-time = "2025-12-06T15:54:24.694Z" }, + { url = "https://files.pythonhosted.org/packages/6e/57/b9f5b5b6fbff9c26f77e785baf56ae8460ef74acdb3eae4931c25b8f5ba9/orjson-3.11.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85", size = 130520, upload-time = "2025-12-06T15:54:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6d/d34970bf9eb33f9ec7c979a262cad86076814859e54eb9a059a52f6dc13d/orjson-3.11.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9", size = 136209, upload-time = "2025-12-06T15:54:27.264Z" }, + { url = "https://files.pythonhosted.org/packages/e7/39/bc373b63cc0e117a105ea12e57280f83ae52fdee426890d57412432d63b3/orjson-3.11.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626", size = 139837, upload-time = "2025-12-06T15:54:28.75Z" }, + { url = "https://files.pythonhosted.org/packages/cb/aa/7c4818c8d7d324da220f4f1af55c343956003aa4d1ce1857bdc1d396ba69/orjson-3.11.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa", size = 137307, upload-time = "2025-12-06T15:54:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/46/bf/0993b5a056759ba65145effe3a79dd5a939d4a070eaa5da2ee3180fbb13f/orjson-3.11.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477", size = 139020, upload-time = "2025-12-06T15:54:31.024Z" }, + { url = "https://files.pythonhosted.org/packages/65/e8/83a6c95db3039e504eda60fc388f9faedbb4f6472f5aba7084e06552d9aa/orjson-3.11.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e", size = 141099, upload-time = "2025-12-06T15:54:32.196Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b4/24fdc024abfce31c2f6812973b0a693688037ece5dc64b7a60c1ce69e2f2/orjson-3.11.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69", size = 413540, upload-time = "2025-12-06T15:54:33.361Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/01c0ec95d55ed0c11e4cae3e10427e479bba40c77312b63e1f9665e0737d/orjson-3.11.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3", size = 151530, upload-time = "2025-12-06T15:54:34.6Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d4/f9ebc57182705bb4bbe63f5bbe14af43722a2533135e1d2fb7affa0c355d/orjson-3.11.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca", size = 141863, upload-time = "2025-12-06T15:54:35.801Z" }, + { url = "https://files.pythonhosted.org/packages/0d/04/02102b8d19fdcb009d72d622bb5781e8f3fae1646bf3e18c53d1bc8115b5/orjson-3.11.5-cp312-cp312-win32.whl", hash = "sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98", size = 135255, upload-time = "2025-12-06T15:54:37.209Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fb/f05646c43d5450492cb387de5549f6de90a71001682c17882d9f66476af5/orjson-3.11.5-cp312-cp312-win_amd64.whl", hash = "sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875", size = 133252, upload-time = "2025-12-06T15:54:38.401Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/7b8c0b26ba18c793533ac1cd145e131e46fcf43952aa94c109b5b913c1f0/orjson-3.11.5-cp312-cp312-win_arm64.whl", hash = "sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe", size = 126777, upload-time = "2025-12-06T15:54:39.515Z" }, + { url = "https://files.pythonhosted.org/packages/10/43/61a77040ce59f1569edf38f0b9faadc90c8cf7e9bec2e0df51d0132c6bb7/orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629", size = 245271, upload-time = "2025-12-06T15:54:40.878Z" }, + { url = "https://files.pythonhosted.org/packages/55/f9/0f79be617388227866d50edd2fd320cb8fb94dc1501184bb1620981a0aba/orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3", size = 129422, upload-time = "2025-12-06T15:54:42.403Z" }, + { url = "https://files.pythonhosted.org/packages/77/42/f1bf1549b432d4a78bfa95735b79b5dac75b65b5bb815bba86ad406ead0a/orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39", size = 132060, upload-time = "2025-12-06T15:54:43.531Z" }, + { url = "https://files.pythonhosted.org/packages/25/49/825aa6b929f1a6ed244c78acd7b22c1481fd7e5fda047dc8bf4c1a807eb6/orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f", size = 130391, upload-time = "2025-12-06T15:54:45.059Z" }, + { url = "https://files.pythonhosted.org/packages/42/ec/de55391858b49e16e1aa8f0bbbb7e5997b7345d8e984a2dec3746d13065b/orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51", size = 135964, upload-time = "2025-12-06T15:54:46.576Z" }, + { url = "https://files.pythonhosted.org/packages/1c/40/820bc63121d2d28818556a2d0a09384a9f0262407cf9fa305e091a8048df/orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8", size = 139817, upload-time = "2025-12-06T15:54:48.084Z" }, + { url = "https://files.pythonhosted.org/packages/09/c7/3a445ca9a84a0d59d26365fd8898ff52bdfcdcb825bcc6519830371d2364/orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706", size = 137336, upload-time = "2025-12-06T15:54:49.426Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b3/dc0d3771f2e5d1f13368f56b339c6782f955c6a20b50465a91acb79fe961/orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f", size = 138993, upload-time = "2025-12-06T15:54:50.939Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a2/65267e959de6abe23444659b6e19c888f242bf7725ff927e2292776f6b89/orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863", size = 141070, upload-time = "2025-12-06T15:54:52.414Z" }, + { url = "https://files.pythonhosted.org/packages/63/c9/da44a321b288727a322c6ab17e1754195708786a04f4f9d2220a5076a649/orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228", size = 413505, upload-time = "2025-12-06T15:54:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/7f/17/68dc14fa7000eefb3d4d6d7326a190c99bb65e319f02747ef3ebf2452f12/orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2", size = 151342, upload-time = "2025-12-06T15:54:55.113Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c5/ccee774b67225bed630a57478529fc026eda33d94fe4c0eac8fe58d4aa52/orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05", size = 141823, upload-time = "2025-12-06T15:54:56.331Z" }, + { url = "https://files.pythonhosted.org/packages/67/80/5d00e4155d0cd7390ae2087130637671da713959bb558db9bac5e6f6b042/orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef", size = 135236, upload-time = "2025-12-06T15:54:57.507Z" }, + { url = "https://files.pythonhosted.org/packages/95/fe/792cc06a84808dbdc20ac6eab6811c53091b42f8e51ecebf14b540e9cfe4/orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583", size = 133167, upload-time = "2025-12-06T15:54:58.71Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/d158bd8b50e3b1cfdcf406a7e463f6ffe3f0d167b99634717acdaf5e299f/orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287", size = 126712, upload-time = "2025-12-06T15:54:59.892Z" }, + { url = "https://files.pythonhosted.org/packages/c2/60/77d7b839e317ead7bb225d55bb50f7ea75f47afc489c81199befc5435b50/orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0", size = 245252, upload-time = "2025-12-06T15:55:01.127Z" }, + { url = "https://files.pythonhosted.org/packages/f1/aa/d4639163b400f8044cef0fb9aa51b0337be0da3a27187a20d1166e742370/orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81", size = 129419, upload-time = "2025-12-06T15:55:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/30/94/9eabf94f2e11c671111139edf5ec410d2f21e6feee717804f7e8872d883f/orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f", size = 132050, upload-time = "2025-12-06T15:55:03.918Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c8/ca10f5c5322f341ea9a9f1097e140be17a88f88d1cfdd29df522970d9744/orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e", size = 130370, upload-time = "2025-12-06T15:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/25/d4/e96824476d361ee2edd5c6290ceb8d7edf88d81148a6ce172fc00278ca7f/orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7", size = 136012, upload-time = "2025-12-06T15:55:06.402Z" }, + { url = "https://files.pythonhosted.org/packages/85/8e/9bc3423308c425c588903f2d103cfcfe2539e07a25d6522900645a6f257f/orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb", size = 139809, upload-time = "2025-12-06T15:55:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/b404e94e0b02a232b957c54643ce68d0268dacb67ac33ffdee24008c8b27/orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4", size = 137332, upload-time = "2025-12-06T15:55:08.961Z" }, + { url = "https://files.pythonhosted.org/packages/51/30/cc2d69d5ce0ad9b84811cdf4a0cd5362ac27205a921da524ff42f26d65e0/orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad", size = 138983, upload-time = "2025-12-06T15:55:10.595Z" }, + { url = "https://files.pythonhosted.org/packages/0e/87/de3223944a3e297d4707d2fe3b1ffb71437550e165eaf0ca8bbe43ccbcb1/orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829", size = 141069, upload-time = "2025-12-06T15:55:11.832Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/81d5087ae74be33bcae3ff2d80f5ccaa4a8fedc6d39bf65a427a95b8977f/orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac", size = 413491, upload-time = "2025-12-06T15:55:13.314Z" }, + { url = "https://files.pythonhosted.org/packages/d0/6f/f6058c21e2fc1efaf918986dbc2da5cd38044f1a2d4b7b91ad17c4acf786/orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d", size = 151375, upload-time = "2025-12-06T15:55:14.715Z" }, + { url = "https://files.pythonhosted.org/packages/54/92/c6921f17d45e110892899a7a563a925b2273d929959ce2ad89e2525b885b/orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439", size = 141850, upload-time = "2025-12-06T15:55:15.94Z" }, + { url = "https://files.pythonhosted.org/packages/88/86/cdecb0140a05e1a477b81f24739da93b25070ee01ce7f7242f44a6437594/orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499", size = 135278, upload-time = "2025-12-06T15:55:17.202Z" }, + { url = "https://files.pythonhosted.org/packages/e4/97/b638d69b1e947d24f6109216997e38922d54dcdcdb1b11c18d7efd2d3c59/orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310", size = 133170, upload-time = "2025-12-06T15:55:18.468Z" }, + { url = "https://files.pythonhosted.org/packages/8f/dd/f4fff4a6fe601b4f8f3ba3aa6da8ac33d17d124491a3b804c662a70e1636/orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5", size = 126713, upload-time = "2025-12-06T15:55:19.738Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/7b682849dd4c9fb701a981669b964ea700516ecbd8e88f62aae07c6852bd/orjson-3.11.5-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1b280e2d2d284a6713b0cfec7b08918ebe57df23e3f76b27586197afca3cb1e9", size = 245298, upload-time = "2025-12-06T15:55:20.984Z" }, + { url = "https://files.pythonhosted.org/packages/1b/3f/194355a9335707a15fdc79ddc670148987b43d04712dd26898a694539ce6/orjson-3.11.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c8d8a112b274fae8c5f0f01954cb0480137072c271f3f4958127b010dfefaec", size = 132150, upload-time = "2025-12-06T15:55:22.364Z" }, + { url = "https://files.pythonhosted.org/packages/e9/08/d74b3a986d37e6c2e04b8821c62927620c9a1924bb49ea51519a87751b86/orjson-3.11.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0a2ae6f09ac7bd47d2d5a5305c1d9ed08ac057cda55bb0a49fa506f0d2da00", size = 130490, upload-time = "2025-12-06T15:55:23.619Z" }, + { url = "https://files.pythonhosted.org/packages/b2/16/ebd04c38c1db01e493a68eee442efdffc505a43112eccd481e0146c6acc2/orjson-3.11.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0d87bd1896faac0d10b4f849016db81a63e4ec5df38757ffae84d45ab38aa71", size = 135726, upload-time = "2025-12-06T15:55:24.912Z" }, + { url = "https://files.pythonhosted.org/packages/06/64/2ce4b2c09a099403081c37639c224bdcdfe401138bd66fed5c96d4f8dbd3/orjson-3.11.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:801a821e8e6099b8c459ac7540b3c32dba6013437c57fdcaec205b169754f38c", size = 139640, upload-time = "2025-12-06T15:55:26.535Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e2/425796df8ee1d7cea3a7edf868920121dd09162859dbb76fffc9a5c37fd3/orjson-3.11.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a0f6ac618c98c74b7fbc8c0172ba86f9e01dbf9f62aa0b1776c2231a7bffe5", size = 137289, upload-time = "2025-12-06T15:55:27.78Z" }, + { url = "https://files.pythonhosted.org/packages/32/a2/88e482eb8e899a037dcc9eff85ef117a568e6ca1ffa1a2b2be3fcb51b7bb/orjson-3.11.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea7339bdd22e6f1060c55ac31b6a755d86a5b2ad3657f2669ec243f8e3b2bdb", size = 138761, upload-time = "2025-12-06T15:55:29.388Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fd/131dd6d32eeb74c513bfa487f434a2150811d0fbd9cb06689284f2f21b34/orjson-3.11.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4dad582bc93cef8f26513e12771e76385a7e6187fd713157e971c784112aad56", size = 141357, upload-time = "2025-12-06T15:55:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/7a/90/e4a0abbcca7b53e9098ac854f27f5ed9949c796f3c760bc04af997da0eb2/orjson-3.11.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:0522003e9f7fba91982e83a97fec0708f5a714c96c4209db7104e6b9d132f111", size = 413638, upload-time = "2025-12-06T15:55:32.344Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c2/df91e385514924120001ade9cd52d6295251023d3bfa2c0a01f38cfc485a/orjson-3.11.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:7403851e430a478440ecc1258bcbacbfbd8175f9ac1e39031a7121dd0de05ff8", size = 150972, upload-time = "2025-12-06T15:55:33.725Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ff/c76cc5a30a4451191ff1b868a331ad1354433335277fc40931f5fc3cab9d/orjson-3.11.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5f691263425d3177977c8d1dd896cde7b98d93cbf390b2544a090675e83a6a0a", size = 141729, upload-time = "2025-12-06T15:55:35.317Z" }, + { url = "https://files.pythonhosted.org/packages/27/c3/7830bf74389ea1eaab2b017d8b15d1cab2bb0737d9412dfa7fb8644f7d78/orjson-3.11.5-cp39-cp39-win32.whl", hash = "sha256:61026196a1c4b968e1b1e540563e277843082e9e97d78afa03eb89315af531f1", size = 135100, upload-time = "2025-12-06T15:55:36.57Z" }, + { url = "https://files.pythonhosted.org/packages/69/e6/babf31154e047e465bc194eb72d1326d7c52ad4d7f50bf92b02b3cacda5c/orjson-3.11.5-cp39-cp39-win_amd64.whl", hash = "sha256:09b94b947ac08586af635ef922d69dc9bc63321527a3a04647f4986a73f4bd30", size = 133189, upload-time = "2025-12-06T15:55:38.143Z" }, +] + +[[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 = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +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 = "pathspec" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302, upload-time = "2024-09-17T19:06:50.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439, upload-time = "2024-09-17T19:06:49.212Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +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 = "pre-commit" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "cfgv", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "identify", version = "2.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "nodeenv", marker = "python_full_version < '3.9'" }, + { name = "pyyaml", marker = "python_full_version < '3.9'" }, + { name = "virtualenv", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/b3/4ae08d21eb097162f5aad37f4585f8069a86402ed7f5362cc9ae097f9572/pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32", size = 177079, upload-time = "2023-10-13T15:57:48.334Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/75/526915fedf462e05eeb1c75ceaf7e3f9cde7b5ce6f62740fe5f7f19a0050/pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660", size = 203698, upload-time = "2023-10-13T15:57:46.378Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "cfgv", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "identify", version = "2.6.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "nodeenv", marker = "python_full_version == '3.9.*'" }, + { name = "pyyaml", marker = "python_full_version == '3.9.*'" }, + { name = "virtualenv", marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "cfgv", version = "3.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "identify", version = "2.6.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "nodeenv", marker = "python_full_version >= '3.10'" }, + { name = "pyyaml", marker = "python_full_version >= '3.10'" }, + { name = "virtualenv", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[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 = "pymdown-extensions" +version = "10.15" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "markdown", version = "3.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pyyaml", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/92/a7296491dbf5585b3a987f3f3fc87af0e632121ff3e490c14b5f2d2b4eb5/pymdown_extensions-10.15.tar.gz", hash = "sha256:0e5994e32155f4b03504f939e501b981d306daf7ec2aa1cd2eb6bd300784f8f7", size = 852320, upload-time = "2025-04-27T23:48:29.183Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/d1/c54e608505776ce4e7966d03358ae635cfd51dff1da6ee421c090dbc797b/pymdown_extensions-10.15-py3-none-any.whl", hash = "sha256:46e99bb272612b0de3b7e7caf6da8dd5f4ca5212c0b273feb9304e236c484e5f", size = 265845, upload-time = "2025-04-27T23:48:27.359Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.20" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "markdown", version = "3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "markdown", version = "3.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyyaml", marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/35/e3814a5b7df295df69d035cfb8aab78b2967cdf11fcfae7faed726b66664/pymdown_extensions-10.20.tar.gz", hash = "sha256:5c73566ab0cf38c6ba084cb7c5ea64a119ae0500cce754ccb682761dfea13a52", size = 852774, upload-time = "2025-12-31T19:59:42.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/10/47caf89cbb52e5bb764696fd52a8c591a2f0e851a93270c05a17f36000b5/pymdown_extensions-10.20-py3-none-any.whl", hash = "sha256:ea9e62add865da80a271d00bfa1c0fa085b20d133fb3fc97afdc88e682f60b2f", size = 268733, upload-time = "2025-12-31T19:59:40.652Z" }, +] + +[[package]] +name = "pyproject-api" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "packaging", marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/19/441e0624a8afedd15bbcce96df1b80479dd0ff0d965f5ce8fde4f2f6ffad/pyproject_api-1.8.0.tar.gz", hash = "sha256:77b8049f2feb5d33eefcc21b57f1e279636277a8ac8ad6b5871037b243778496", size = 22340, upload-time = "2024-09-18T23:18:37.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/f4/3c4ddfcc0c19c217c6de513842d286de8021af2f2ab79bbb86c00342d778/pyproject_api-1.8.0-py3-none-any.whl", hash = "sha256:3d7d347a047afe796fd5d1885b1e391ba29be7169bd2f102fcd378f04273d228", size = 13100, upload-time = "2024-09-18T23:18:35.927Z" }, +] + +[[package]] +name = "pyproject-api" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "packaging", marker = "python_full_version == '3.9.*'" }, + { name = "tomli", marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/fd/437901c891f58a7b9096511750247535e891d2d5a5a6eefbc9386a2b41d5/pyproject_api-1.9.1.tar.gz", hash = "sha256:43c9918f49daab37e302038fc1aed54a8c7a91a9fa935d00b9a485f37e0f5335", size = 22710, upload-time = "2025-05-12T14:41:58.025Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e6/c293c06695d4a3ab0260ef124a74ebadba5f4c511ce3a4259e976902c00b/pyproject_api-1.9.1-py3-none-any.whl", hash = "sha256:7d6238d92f8962773dd75b5f0c4a6a27cce092a14b623b811dba656f3b628948", size = 13158, upload-time = "2025-05-12T14:41:56.217Z" }, +] + +[[package]] +name = "pyproject-api" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/7b/c0e1333b61d41c69e59e5366e727b18c4992688caf0de1be10b3e5265f6b/pyproject_api-1.10.0.tar.gz", hash = "sha256:40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330", size = 22785, upload-time = "2025-10-09T19:12:27.21Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/cc/cecf97be298bee2b2a37dd360618c819a2a7fd95251d8e480c1f0eb88f3b/pyproject_api-1.10.0-py3-none-any.whl", hash = "sha256:8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09", size = 13218, upload-time = "2025-10-09T19:12:24.428Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "packaging", marker = "python_full_version < '3.9'" }, + { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version == '3.9.*' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.9.*'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "packaging", marker = "python_full_version == '3.9.*'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pygments", marker = "python_full_version == '3.9.*'" }, + { name = "tomli", marker = "python_full_version == '3.9.*'" }, +] +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" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", 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", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/c6cf50ce320cf8611df7a1254d86233b3df7cc07f9b5f5cbcb82e08aa534/pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276", size = 49855, upload-time = "2024-08-22T08:03:18.145Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/31/6607dab48616902f76885dfcf62c08d929796fc3b2d2318faf9fd54dbed9/pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", size = 18024, upload-time = "2024-08-22T08:03:15.536Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version == '3.9.*'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +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-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version == '3.10.*'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "coverage", version = "7.6.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.9'" }, + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042, upload-time = "2024-03-24T20:16:34.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990, upload-time = "2024-03-24T20:16:32.444Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version == '3.9.*'" }, + { name = "coverage", version = "7.13.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +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 = "pytest-httpserver" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "werkzeug", version = "3.0.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/8a/98d9f2dc6a7f486bc3467935db766255509ffd47efc6b47bd2231b2bf009/pytest_httpserver-1.1.1.tar.gz", hash = "sha256:e5c46c62c0aa65e5d4331228cb2cb7db846c36e429c3e74ca806f284806bf7c6", size = 68190, upload-time = "2025-01-21T19:43:36.092Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/52/b9148e455166fba9bc2f6a5293d7d01e305155231d9863f10cb4e968c4fb/pytest_httpserver-1.1.1-py3-none-any.whl", hash = "sha256:aadc744bfac773a2ea93d05c2ef51fa23c087e3cc5dace3ea9d45cdd4bfe1fe8", size = 20723, upload-time = "2025-01-21T19:43:33.801Z" }, +] + +[[package]] +name = "pytest-httpserver" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "werkzeug", version = "3.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/d8/def15ba33bd696dd72dd4562a5287c0cba4d18a591eeb82e0b08ab385afc/pytest_httpserver-1.1.3.tar.gz", hash = "sha256:af819d6b533f84b4680b9416a5b3f67f1df3701f1da54924afd4d6e4ba5917ec", size = 68870, upload-time = "2025-04-10T08:17:15.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/d2/dfc2f25f3905921c2743c300a48d9494d29032f1389fc142e718d6978fb2/pytest_httpserver-1.1.3-py3-none-any.whl", hash = "sha256:5f84757810233e19e2bb5287f3826a71c97a3740abe3a363af9155c0f82fdbb9", size = 21000, upload-time = "2025-04-10T08:17:13.906Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/a2/09f67a3589cb4320fb5ce90d3fd4c9752636b8b6ad8f34b54d76c5a54693/PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f", size = 186824, upload-time = "2025-09-29T20:27:35.918Z" }, + { url = "https://files.pythonhosted.org/packages/02/72/d972384252432d57f248767556ac083793292a4adf4e2d85dfe785ec2659/PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4", size = 795069, upload-time = "2025-09-29T20:27:38.15Z" }, + { url = "https://files.pythonhosted.org/packages/a7/3b/6c58ac0fa7c4e1b35e48024eb03d00817438310447f93ef4431673c24138/PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3", size = 862585, upload-time = "2025-09-29T20:27:39.715Z" }, + { url = "https://files.pythonhosted.org/packages/25/a2/b725b61ac76a75583ae7104b3209f75ea44b13cfd026aa535ece22b7f22e/PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6", size = 806018, upload-time = "2025-09-29T20:27:41.444Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b0/b2227677b2d1036d84f5ee95eb948e7af53d59fe3e4328784e4d290607e0/PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369", size = 802822, upload-time = "2025-09-29T20:27:42.885Z" }, + { url = "https://files.pythonhosted.org/packages/99/a5/718a8ea22521e06ef19f91945766a892c5ceb1855df6adbde67d997ea7ed/PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295", size = 143744, upload-time = "2025-09-29T20:27:44.487Z" }, + { url = "https://files.pythonhosted.org/packages/76/b2/2b69cee94c9eb215216fc05778675c393e3aa541131dc910df8e52c83776/PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b", size = 160082, upload-time = "2025-09-29T20:27:46.049Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" }, + { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "pyyaml", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631, upload-time = "2020-11-12T02:38:26.239Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911, upload-time = "2020-11-12T02:38:24.638Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "pyyaml", marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version < '3.9'" }, + { name = "charset-normalizer", marker = "python_full_version < '3.9'" }, + { name = "idna", marker = "python_full_version < '3.9'" }, + { name = "urllib3", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "certifi", marker = "python_full_version >= '3.9'" }, + { name = "charset-normalizer", marker = "python_full_version >= '3.9'" }, + { name = "idna", marker = "python_full_version >= '3.9'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" }, + { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" }, + { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" }, + { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" }, + { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" }, + { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" }, + { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" }, + { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "tox" +version = "4.25.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "cachetools", version = "5.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "chardet", marker = "python_full_version < '3.9'" }, + { name = "colorama", marker = "python_full_version < '3.9'" }, + { name = "filelock", version = "3.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "packaging", marker = "python_full_version < '3.9'" }, + { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pyproject-api", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "virtualenv", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/87/692478f0a194f1cad64803692642bd88c12c5b64eee16bf178e4a32e979c/tox-4.25.0.tar.gz", hash = "sha256:dd67f030317b80722cf52b246ff42aafd3ed27ddf331c415612d084304cf5e52", size = 196255, upload-time = "2025-03-27T15:13:37.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/38/33348de6fc4b1afb3d76d8485c8aecbdabcfb3af8da53d40c792332e2b37/tox-4.25.0-py3-none-any.whl", hash = "sha256:4dfdc7ba2cc6fdc6688dde1b21e7b46ff6c41795fb54586c91a3533317b5255c", size = 172420, upload-time = "2025-03-27T15:13:35.703Z" }, +] + +[[package]] +name = "tox" +version = "4.30.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "cachetools", version = "6.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "chardet", marker = "python_full_version == '3.9.*'" }, + { name = "colorama", marker = "python_full_version == '3.9.*'" }, + { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "packaging", marker = "python_full_version == '3.9.*'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pyproject-api", version = "1.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "tomli", marker = "python_full_version == '3.9.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "virtualenv", marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/b2/cee55172e5e10ce030b087cd3ac06641e47d08a3dc8d76c17b157dba7558/tox-4.30.3.tar.gz", hash = "sha256:f3dd0735f1cd4e8fbea5a3661b77f517456b5f0031a6256432533900e34b90bf", size = 202799, upload-time = "2025-10-02T16:24:39.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/e4/8bb9ce952820df4165eb34610af347665d6cb436898a234db9d84d093ce6/tox-4.30.3-py3-none-any.whl", hash = "sha256:a9f17b4b2d0f74fe0d76207236925a119095011e5c2e661a133115a8061178c9", size = 175512, upload-time = "2025-10-02T16:24:38.209Z" }, +] + +[[package]] +name = "tox" +version = "4.34.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "cachetools", version = "6.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "chardet", marker = "python_full_version >= '3.10'" }, + { name = "colorama", marker = "python_full_version >= '3.10'" }, + { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pyproject-api", version = "1.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "virtualenv", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/9b/5909f40b281ebd37c2f83de5087b9cb8a9a64c33745f334be0aeaedadbbc/tox-4.34.1.tar.gz", hash = "sha256:ef1e82974c2f5ea02954d590ee0b967fad500c3879b264ea19efb9a554f3cc60", size = 205306, upload-time = "2026-01-09T17:42:59.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/0f/fe6629e277ce615e53d0a0b65dc23c88b15a402bb7dbf771f17bbd18f1c4/tox-4.34.1-py3-none-any.whl", hash = "sha256:5610d69708bab578d618959b023f8d7d5d3386ed14a2392aeebf9c583615af60", size = 176812, upload-time = "2026-01-09T17:42:58.629Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +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 = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677, upload-time = "2024-09-12T10:52:18.401Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338, upload-time = "2024-09-12T10:52:16.589Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.36.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock", version = "3.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, +] + +[[package]] +name = "watchdog" +version = "4.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/38/764baaa25eb5e35c9a043d4c4588f9836edfe52a708950f4b6d5f714fd42/watchdog-4.0.2.tar.gz", hash = "sha256:b4dfbb6c49221be4535623ea4474a4d6ee0a9cef4a80b20c28db4d858b64e270", size = 126587, upload-time = "2024-08-11T07:38:01.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/b0/219893d41c16d74d0793363bf86df07d50357b81f64bba4cb94fe76e7af4/watchdog-4.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ede7f010f2239b97cc79e6cb3c249e72962404ae3865860855d5cbe708b0fd22", size = 100257, upload-time = "2024-08-11T07:37:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c6/8e90c65693e87d98310b2e1e5fd7e313266990853b489e85ce8396cc26e3/watchdog-4.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2cffa171445b0efa0726c561eca9a27d00a1f2b83846dbd5a4f639c4f8ca8e1", size = 92249, upload-time = "2024-08-11T07:37:06.364Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cd/2e306756364a934532ff8388d90eb2dc8bb21fe575cd2b33d791ce05a02f/watchdog-4.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c50f148b31b03fbadd6d0b5980e38b558046b127dc483e5e4505fcef250f9503", size = 92888, upload-time = "2024-08-11T07:37:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/de/78/027ad372d62f97642349a16015394a7680530460b1c70c368c506cb60c09/watchdog-4.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c7d4bf585ad501c5f6c980e7be9c4f15604c7cc150e942d82083b31a7548930", size = 100256, upload-time = "2024-08-11T07:37:11.017Z" }, + { url = "https://files.pythonhosted.org/packages/59/a9/412b808568c1814d693b4ff1cec0055dc791780b9dc947807978fab86bc1/watchdog-4.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:914285126ad0b6eb2258bbbcb7b288d9dfd655ae88fa28945be05a7b475a800b", size = 92252, upload-time = "2024-08-11T07:37:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/04/57/179d76076cff264982bc335dd4c7da6d636bd3e9860bbc896a665c3447b6/watchdog-4.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:984306dc4720da5498b16fc037b36ac443816125a3705dfde4fd90652d8028ef", size = 92888, upload-time = "2024-08-11T07:37:15.077Z" }, + { url = "https://files.pythonhosted.org/packages/92/f5/ea22b095340545faea37ad9a42353b265ca751f543da3fb43f5d00cdcd21/watchdog-4.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1cdcfd8142f604630deef34722d695fb455d04ab7cfe9963055df1fc69e6727a", size = 100342, upload-time = "2024-08-11T07:37:16.393Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d2/8ce97dff5e465db1222951434e3115189ae54a9863aef99c6987890cc9ef/watchdog-4.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7ab624ff2f663f98cd03c8b7eedc09375a911794dfea6bf2a359fcc266bff29", size = 92306, upload-time = "2024-08-11T07:37:17.997Z" }, + { url = "https://files.pythonhosted.org/packages/49/c4/1aeba2c31b25f79b03b15918155bc8c0b08101054fc727900f1a577d0d54/watchdog-4.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:132937547a716027bd5714383dfc40dc66c26769f1ce8a72a859d6a48f371f3a", size = 92915, upload-time = "2024-08-11T07:37:19.967Z" }, + { url = "https://files.pythonhosted.org/packages/79/63/eb8994a182672c042d85a33507475c50c2ee930577524dd97aea05251527/watchdog-4.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd67c7df93eb58f360c43802acc945fa8da70c675b6fa37a241e17ca698ca49b", size = 100343, upload-time = "2024-08-11T07:37:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/ce/82/027c0c65c2245769580605bcd20a1dc7dfd6c6683c8c4e2ef43920e38d27/watchdog-4.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcfd02377be80ef3b6bc4ce481ef3959640458d6feaae0bd43dd90a43da90a7d", size = 92313, upload-time = "2024-08-11T07:37:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/2a/89/ad4715cbbd3440cb0d336b78970aba243a33a24b1a79d66f8d16b4590d6a/watchdog-4.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:980b71510f59c884d684b3663d46e7a14b457c9611c481e5cef08f4dd022eed7", size = 92919, upload-time = "2024-08-11T07:37:24.715Z" }, + { url = "https://files.pythonhosted.org/packages/55/08/1a9086a3380e8828f65b0c835b86baf29ebb85e5e94a2811a2eb4f889cfd/watchdog-4.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:aa160781cafff2719b663c8a506156e9289d111d80f3387cf3af49cedee1f040", size = 100255, upload-time = "2024-08-11T07:37:26.862Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3e/064974628cf305831f3f78264800bd03b3358ec181e3e9380a36ff156b93/watchdog-4.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f6ee8dedd255087bc7fe82adf046f0b75479b989185fb0bdf9a98b612170eac7", size = 92257, upload-time = "2024-08-11T07:37:28.253Z" }, + { url = "https://files.pythonhosted.org/packages/23/69/1d2ad9c12d93bc1e445baa40db46bc74757f3ffc3a3be592ba8dbc51b6e5/watchdog-4.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0b4359067d30d5b864e09c8597b112fe0a0a59321a0f331498b013fb097406b4", size = 92886, upload-time = "2024-08-11T07:37:29.52Z" }, + { url = "https://files.pythonhosted.org/packages/68/eb/34d3173eceab490d4d1815ba9a821e10abe1da7a7264a224e30689b1450c/watchdog-4.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:770eef5372f146997638d737c9a3c597a3b41037cfbc5c41538fc27c09c3a3f9", size = 100254, upload-time = "2024-08-11T07:37:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/18/a1/4bbafe7ace414904c2cc9bd93e472133e8ec11eab0b4625017f0e34caad8/watchdog-4.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eeea812f38536a0aa859972d50c76e37f4456474b02bd93674d1947cf1e39578", size = 92249, upload-time = "2024-08-11T07:37:32.193Z" }, + { url = "https://files.pythonhosted.org/packages/f3/11/ec5684e0ca692950826af0de862e5db167523c30c9cbf9b3f4ce7ec9cc05/watchdog-4.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b2c45f6e1e57ebb4687690c05bc3a2c1fb6ab260550c4290b8abb1335e0fd08b", size = 92891, upload-time = "2024-08-11T07:37:34.212Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9a/6f30f023324de7bad8a3eb02b0afb06bd0726003a3550e9964321315df5a/watchdog-4.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:10b6683df70d340ac3279eff0b2766813f00f35a1d37515d2c99959ada8f05fa", size = 91775, upload-time = "2024-08-11T07:37:35.567Z" }, + { url = "https://files.pythonhosted.org/packages/87/62/8be55e605d378a154037b9ba484e00a5478e627b69c53d0f63e3ef413ba6/watchdog-4.0.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f7c739888c20f99824f7aa9d31ac8a97353e22d0c0e54703a547a218f6637eb3", size = 92255, upload-time = "2024-08-11T07:37:37.596Z" }, + { url = "https://files.pythonhosted.org/packages/6b/59/12e03e675d28f450bade6da6bc79ad6616080b317c472b9ae688d2495a03/watchdog-4.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c100d09ac72a8a08ddbf0629ddfa0b8ee41740f9051429baa8e31bb903ad7508", size = 91682, upload-time = "2024-08-11T07:37:38.901Z" }, + { url = "https://files.pythonhosted.org/packages/ef/69/241998de9b8e024f5c2fbdf4324ea628b4231925305011ca8b7e1c3329f6/watchdog-4.0.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f5315a8c8dd6dd9425b974515081fc0aadca1d1d61e078d2246509fd756141ee", size = 92249, upload-time = "2024-08-11T07:37:40.143Z" }, + { url = "https://files.pythonhosted.org/packages/70/3f/2173b4d9581bc9b5df4d7f2041b6c58b5e5448407856f68d4be9981000d0/watchdog-4.0.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2d468028a77b42cc685ed694a7a550a8d1771bb05193ba7b24006b8241a571a1", size = 91773, upload-time = "2024-08-11T07:37:42.095Z" }, + { url = "https://files.pythonhosted.org/packages/f0/de/6fff29161d5789048f06ef24d94d3ddcc25795f347202b7ea503c3356acb/watchdog-4.0.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f15edcae3830ff20e55d1f4e743e92970c847bcddc8b7509bcd172aa04de506e", size = 92250, upload-time = "2024-08-11T07:37:44.052Z" }, + { url = "https://files.pythonhosted.org/packages/8a/b1/25acf6767af6f7e44e0086309825bd8c098e301eed5868dc5350642124b9/watchdog-4.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:936acba76d636f70db8f3c66e76aa6cb5136a936fc2a5088b9ce1c7a3508fc83", size = 82947, upload-time = "2024-08-11T07:37:45.388Z" }, + { url = "https://files.pythonhosted.org/packages/e8/90/aebac95d6f954bd4901f5d46dcd83d68e682bfd21798fd125a95ae1c9dbf/watchdog-4.0.2-py3-none-manylinux2014_armv7l.whl", hash = "sha256:e252f8ca942a870f38cf785aef420285431311652d871409a64e2a0a52a2174c", size = 82942, upload-time = "2024-08-11T07:37:46.722Z" }, + { url = "https://files.pythonhosted.org/packages/15/3a/a4bd8f3b9381824995787488b9282aff1ed4667e1110f31a87b871ea851c/watchdog-4.0.2-py3-none-manylinux2014_i686.whl", hash = "sha256:0e83619a2d5d436a7e58a1aea957a3c1ccbf9782c43c0b4fed80580e5e4acd1a", size = 82947, upload-time = "2024-08-11T07:37:48.941Z" }, + { url = "https://files.pythonhosted.org/packages/09/cc/238998fc08e292a4a18a852ed8274159019ee7a66be14441325bcd811dfd/watchdog-4.0.2-py3-none-manylinux2014_ppc64.whl", hash = "sha256:88456d65f207b39f1981bf772e473799fcdc10801062c36fd5ad9f9d1d463a73", size = 82946, upload-time = "2024-08-11T07:37:50.279Z" }, + { url = "https://files.pythonhosted.org/packages/80/f1/d4b915160c9d677174aa5fae4537ae1f5acb23b3745ab0873071ef671f0a/watchdog-4.0.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:32be97f3b75693a93c683787a87a0dc8db98bb84701539954eef991fb35f5fbc", size = 82947, upload-time = "2024-08-11T07:37:51.55Z" }, + { url = "https://files.pythonhosted.org/packages/db/02/56ebe2cf33b352fe3309588eb03f020d4d1c061563d9858a9216ba004259/watchdog-4.0.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:c82253cfc9be68e3e49282831afad2c1f6593af80c0daf1287f6a92657986757", size = 82944, upload-time = "2024-08-11T07:37:52.855Z" }, + { url = "https://files.pythonhosted.org/packages/01/d2/c8931ff840a7e5bd5dcb93f2bb2a1fd18faf8312e9f7f53ff1cf76ecc8ed/watchdog-4.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c0b14488bd336c5b1845cee83d3e631a1f8b4e9c5091ec539406e4a324f882d8", size = 82947, upload-time = "2024-08-11T07:37:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d8/cdb0c21a4a988669d7c210c75c6a2c9a0e16a3b08d9f7e633df0d9a16ad8/watchdog-4.0.2-py3-none-win32.whl", hash = "sha256:0d8a7e523ef03757a5aa29f591437d64d0d894635f8a50f370fe37f913ce4e19", size = 82935, upload-time = "2024-08-11T07:37:56.668Z" }, + { url = "https://files.pythonhosted.org/packages/99/2e/b69dfaae7a83ea64ce36538cc103a3065e12c447963797793d5c0a1d5130/watchdog-4.0.2-py3-none-win_amd64.whl", hash = "sha256:c344453ef3bf875a535b0488e3ad28e341adbd5a9ffb0f7d62cefacc8824ef2b", size = 82934, upload-time = "2024-08-11T07:37:57.991Z" }, + { url = "https://files.pythonhosted.org/packages/b0/0b/43b96a9ecdd65ff5545b1b13b687ca486da5c6249475b1a45f24d63a1858/watchdog-4.0.2-py3-none-win_ia64.whl", hash = "sha256:baececaa8edff42cd16558a639a9b0ddf425f93d892e8392a56bf904f5eff22c", size = 82933, upload-time = "2024-08-11T07:37:59.573Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/7223011bb760fce8ddc53416beb65b83a3ea6d7d13738dde75eeb2c89679/watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", size = 96390, upload-time = "2024-11-01T14:06:49.325Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/d2b21bc4e706d3a9d467561f487c2938cbd881c69f3808c43ac1ec242391/watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", size = 88386, upload-time = "2024-11-01T14:06:50.536Z" }, + { url = "https://files.pythonhosted.org/packages/ea/22/1c90b20eda9f4132e4603a26296108728a8bfe9584b006bd05dd94548853/watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", size = 89017, upload-time = "2024-11-01T14:06:51.717Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/5b/79/69f2b0e8d3f2afd462029031baafb1b75d11bb62703f0e1022b2e54d49ee/watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", size = 87903, upload-time = "2024-11-01T14:06:57.052Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2b/dc048dd71c2e5f0f7ebc04dd7912981ec45793a03c0dc462438e0591ba5d/watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", size = 88381, upload-time = "2024-11-01T14:06:58.193Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.0.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/f9/0ba83eaa0df9b9e9d1efeb2ea351d0677c37d41ee5d0f91e98423c7281c9/werkzeug-3.0.6.tar.gz", hash = "sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d", size = 805170, upload-time = "2024-10-25T18:52:31.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/69/05837f91dfe42109203ffa3e488214ff86a6d68b2ed6c167da6cdc42349b/werkzeug-3.0.6-py3-none-any.whl", hash = "sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17", size = 227979, upload-time = "2024-10-25T18:52:30.129Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "markupsafe", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, +] + +[[package]] +name = "wheel" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545, upload-time = "2024-11-23T00:18:23.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494, upload-time = "2024-11-23T00:18:21.207Z" }, +] + +[[package]] +name = "wrapper-tls-requests" +source = { editable = "." } +dependencies = [ + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "orjson", version = "3.10.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "orjson", version = "3.11.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mkautodoc" }, + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", version = "0.26.1", source = { registry = "https://pypi.org/simple" }, extra = ["python"], marker = "python_full_version < '3.9'" }, + { name = "mkdocstrings", version = "0.30.1", source = { registry = "https://pypi.org/simple" }, extra = ["python"], marker = "python_full_version == '3.9.*'" }, + { name = "mkdocstrings", version = "1.0.1", source = { registry = "https://pypi.org/simple" }, extra = ["python"], marker = "python_full_version >= '3.10'" }, + { name = "mypy", version = "1.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mypy", version = "1.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pre-commit", version = "3.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pre-commit", version = "4.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pre-commit", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest-asyncio", version = "0.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pytest-asyncio", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest-cov", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pytest-cov", version = "7.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest-httpserver", version = "1.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pytest-httpserver", version = "1.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "ruff" }, + { name = "tox", version = "4.25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "tox", version = "4.30.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "tox", version = "4.34.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "werkzeug", version = "3.0.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "werkzeug", version = "3.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] + +[package.metadata] +requires-dist = [ + { name = "charset-normalizer" }, + { name = "idna", specifier = "~=3.10" }, + { name = "orjson" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mkautodoc" }, + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extras = ["python"] }, + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-httpserver" }, + { name = "ruff" }, + { name = "tox" }, + { name = "werkzeug" }, +] + +[[package]] +name = "zipp" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", size = 24199, upload-time = "2024-09-13T13:44:16.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", size = 9200, upload-time = "2024-09-13T13:44:14.38Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]