diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index a32afcf3..b147886a 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -16,16 +16,12 @@ jobs: uses: actions/setup-python@v2 with: python-version: "3.13" + - name: Install uv + uses: astral-sh/setup-uv@v4 - name: Install dependencies - run: | - python -m pip install --upgrade pip poetry - poetry config virtualenvs.create false - poetry install - - - name: build release distributions - run: | - # NOTE: put your own distribution build steps here. - poetry build + run: uv sync + - name: Build release distributions + run: uv build - name: upload dists uses: actions/upload-artifact@v4 @@ -48,4 +44,4 @@ jobs: path: dist/ - name: Publish release distributions to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@release/v1 \ No newline at end of file diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 858d4edb..896f3e4b 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -4,7 +4,6 @@ on: paths: - "**.py" - "pyproject.toml" - - "poetry.lock" pull_request: branches: - master @@ -12,7 +11,6 @@ on: paths: - "**.py" - "pyproject.toml" - - "poetry.lock" workflow_dispatch: jobs: @@ -23,21 +21,21 @@ jobs: - name: Setup Python uses: actions/setup-python@v3 with: - python-version: "3.9" + python-version: "3.11" - name: Checkout uses: actions/checkout@v3 with: ref: ${{ github.event.pull_request.head.sha }} + - name: Install uv + uses: astral-sh/setup-uv@v4 - name: Install Dependencies - run: | - pip install poetry - poetry install --with styling + run: uv sync --group dev - name: Run Ruff format - run: poetry run ruff format homeassistant_api + run: uv run ruff format homeassistant_api - name: Run Ruff linting - run: poetry run ruff check homeassistant_api - - name: Run MyPy - run: poetry run mypy homeassistant_api --show-error-codes + run: uv run ruff check homeassistant_api + - name: Run Zuban + run: uv run zuban check homeassistant_api code_functionality: name: "Code Functionality" diff --git a/.gitignore b/.gitignore index 3c53f063..90cf0fbc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +CHANGELOG.draft.md + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -133,6 +135,8 @@ venv/ ENV/ env.bak/ venv.bak/ +uv.lock +poetry.lock # Spyder project settings .spyderproject diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 674fe974..2e816619 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,12 @@ repos: - - repo: https://github.com/pre-commit/mirrors-mypy + - repo: local hooks: - - id: mypy - additional_dependencies: - - types-requests - - types-simplejson - rev: "v1.17.1" + - id: zuban + name: zuban + entry: uv run zuban check homeassistant_api + language: system + types: [python] + pass_filenames: false - repo: https://github.com/pre-commit/pre-commit-hooks hooks: - id: trailing-whitespace diff --git a/.readthedocs.yml b/.readthedocs.yml index 14f286ed..7af745bb 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,15 +4,11 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.10" + python: "3.11" jobs: - pre_create_environment: - - asdf plugin add poetry - - asdf install poetry 1.8.3 - - asdf global poetry 1.8.3 - - poetry export -f requirements.txt --output requirements.txt --with docs post_install: - - pip install -r requirements.txt + - pip install uv + - uv export --group docs --no-hashes | uv pip install -r - # Build documentation in the docs/ directory with Sphinx sphinx: diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..5e49284e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,57 @@ +# HomeAssistant-API v6.0 Changelog + +## Breaking Changes +A lot has changed in this version. A lot went into modernising and standardizing the interfaces of the clients. +As such, many of the previous paradigms have changed. If you run into any bugs or issues, please let us know. +### Minimum Python version raised to 3.11 +Python 3.9 and 3.10 are no longer supported. + +### Client classes restructured +The old `Client` class (which combined sync and async via a `use_async` flag) has been removed. There are now four distinct client classes: + +| Old | New | +|---|---| +| `Client(use_async=False)` | `Client` | +| `Client(use_async=True)` | `AsyncClient` | +| `WebsocketClient` (sync) | `WebsocketClient` | +| — | `AsyncWebsocketClient` (new) | + +Async client methods no longer have the `async_` prefix — e.g. `async_get_states()` is now just `get_states()` on `AsyncClient`. + +### Models split into Base/Sync/Async variants +`Domain`, `Entity`, `Group`, `Service`, and `Event` now have three variants each: +- **`Base*`** — plain data, no client reference +- **`*`** (e.g. `Domain`) — sync, bound to `Client` +- **`Async*`** (e.g. `AsyncDomain`) — async, bound to `AsyncClient` + +### Processing module rewritten +The decorator-based `@Processing.processor(mimetype)` registry has been replaced with simple dict-based MIME dispatch and separate `sync`/`async` entry points. Custom processor registration and the `decode_bytes` parameter have been removed. + +### Build system moved from Poetry to uv + hatchling +`poetry.lock` is removed. The project now uses `uv` for dependency management and `hatchling` as the build backend. + +### Type checker changed from mypy to zuban + +## New Features + +### WebSocket API support (sync & async) +Full WebSocket client implementation for both sync (`WebsocketClient`) and async (`AsyncWebsocketClient`) with support for: +- **Config entries** — get, disable, enable, ignore flow, subentries, subscribe +- **Entity registry** — list, get, update, remove entries +- **Events** — subscribe, fire, and listen +- **Services** — trigger with full domain/service routing +- **Templates** — render and subscribe to template updates + +### Entity registry models +New models: `EntityRegistryEntry`, `EntityRegistryEntryExtended`, `EntityRegistryUpdateResult`. + +### Configurable WebSocket max message size +WS clients accept a `max_size` parameter (default 16 MB) to handle large responses like full entity registry lists. + +## Improvements + +- Unified method signatures across all four client classes for consistency +- Expanded ruff lint rules to `ALL` (from just E, F, W) +- Test coverage improved to ~99% +- Modernized type annotations throughout +- Response content is now read lazily, eliminating internal `_buffer` access hacks \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index be299b74..9586dfbe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,13 @@ -ARG BUILD_FROM +FROM ghcr.io/astral-sh/uv:python3.13-bookworm AS dependencies +WORKDIR /app +COPY pyproject.toml README.md ./ +RUN uv sync --group dev -FROM ${BUILD_FROM} AS base +FROM python:3.13-bookworm ENV PYTHONPATH=. WORKDIR /app -COPY ./ /app/ - -FROM base AS dependencies -RUN pip install --upgrade pip wheel -RUN pip install poetry -RUN python3 -m venv .venv && \ - . .venv/bin/activate && \ - poetry install --with testing && \ - deactivate - -FROM base AS final COPY --from=dependencies /app/.venv /app/.venv +ENV PATH="/app/.venv/bin:$PATH" +COPY ./ /app/ -ENTRYPOINT [ "sh", "entrypoint.sh" ] - +ENTRYPOINT [ "sh", "entrypoint.sh" ] \ No newline at end of file diff --git a/README.md b/README.md index cd5bcafa..7d0da8e2 100644 --- a/README.md +++ b/README.md @@ -25,25 +25,22 @@ with Client( '', # i.e. 'http://homeassistant.local:8123/api/' '' ) as client: - light = client.trigger_service('light', 'turn_on', entity_id="light.living_room") + client.trigger_service('light', 'turn_on', entity_id="light.living_room") ``` -All the methods also support async/await! -Just prefix the method with `async_` and pass the `use_async=True` argument to the `Client` constructor. -Then you can use the methods as coroutines -(i.e. `await light.async_turn_on(...)`). +All four client classes share the same method names. +The async clients (`AsyncClient`, `AsyncWebsocketClient`) use `async def` methods that you `await`. ```py import asyncio -from homeassistant_api import Client +from homeassistant_api import AsyncClient async def main(): - with Client( + async with AsyncClient( '', # i.e. 'http://homeassistant.local:8123/api/' '', - use_async=True ) as client: - light = await client.async_trigger_service('light', 'turn_on', entity_id="light.living_room") + await client.trigger_service('light', 'turn_on', entity_id="light.living_room") asyncio.run(main()) ``` @@ -57,11 +54,9 @@ with WebsocketClient( '', # i.e. 'ws://homeassistant.local:8123/api/websocket' '' ) as ws_client: - light = ws_client.trigger_service('light', 'turn_on', entity_id="light.living_room") + ws_client.trigger_service('light', 'turn_on', entity_id="light.living_room") ``` -> Note: The Websocket API is not yet supported in async/await mode. - ## Documentation All documentation, API reference, contribution guidelines and pretty much everything else diff --git a/compose.yml b/compose.yml index bd502f0f..b8364f51 100644 --- a/compose.yml +++ b/compose.yml @@ -8,8 +8,6 @@ services: tests: build: context: . - args: - BUILD_FROM: "python:3.13" image: homeassistant-tests:latest volumes: - ./volumes/coverage:/app/coverage:rw diff --git a/docs/CONTRIBUTING.rst b/docs/CONTRIBUTING.rst index 2639d155..6f8d0931 100644 --- a/docs/CONTRIBUTING.rst +++ b/docs/CONTRIBUTING.rst @@ -37,12 +37,12 @@ Next run in your terminal. Step Three: Installing Dependencies ====================================== -Firstly, you need to have Python 3.7 or newer with Pip installed. +Firstly, you need to have Python 3.11 or newer installed. Download the latest Python Version from `here `__. -Then you need to install the very popular Python Package Manager, :code:`poetry`. -Checkout the `Poetry Docs `__. -You can install that with :code:`pip` by running :code:`pip install poetry`. -Now you can install the project's dependencies by running :code:`cd HomeAssistantAPI && poetry install` +Then you need to install :code:`uv`, a fast Python package manager. +Checkout the `uv Docs `__. +You can install it with :code:`pip` by running :code:`pip install uv`, or see the uv docs for other installation methods. +Now you can install the project's dependencies by running :code:`cd HomeAssistantAPI && uv sync` Step Four: [Optional] Setting Up a Home Assistant Development Environment. ============================================================================= @@ -69,7 +69,7 @@ Code Styling Guidelines In order to make sure that our code is easy to read, and navigate. As well as to stop stupid mistakes like typos, undefined variables, etc. We enforce code standards. -Using the tools, :code:`ruff`, :code:`pytest`, and :code:`docker`, we make make sure that our code quality is top notch, and that are changes work everywhere. +Using the tools, :code:`ruff`, :code:`zuban`, :code:`pytest`, and :code:`docker`, we make make sure that our code quality is top notch, and that are changes work everywhere. You can those tools manually yourself, but they also run automatically when you open a PR. Merging Your Contributions diff --git a/docs/advanced.rst b/docs/advanced.rst index 978045ed..3103adf5 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -2,123 +2,74 @@ Advanced Section ******************* -Persistent Caching -******************** +Caching +********** + +By default, caching is **disabled**. You can enable the built-in in-memory cache by passing :code:`use_cache=True`: + +.. code-block:: python -Persistent caching is exactly what it means. It makes your requests cache persist or stay around between :py:class:`Client` objects, and between runs, and contexts (:code:`with client:` statements). -Rather than the default behavior, which is saving the cache to memory or not at all and erasing it after each context and run. + from homeassistant_api import Client + client = Client("", "", use_cache=True) -If you want to persist your requests cache you can pass your own custom cached session to :py:class:`Client`'s init method. -You can pass a variety of options to your cached session like how fast to expire the cache, where to cache it (the cache backend), and what to do when the cache is expired. +This creates an in-memory cache that expires after 300 seconds. -Depending on whether you are using this in an async or sync project you will want to use either :py:class:`aiohttp_client_cache.backends.CachedSession` or :py:class:`requests_cache.CachedSession` respectively. -See the docs for `requests_cache `__ and `aiohttp_client_cache `__ for how to implement these backends, options, and much more. +Persistent Caching +******************** -You can simply pass them to your client like so. +If you want your cache to persist between runs (e.g. to a filesystem), you can pass your own custom cached session via the :code:`session` parameter. + +Depending on whether you are using a sync or async client you will want to use either :py:class:`requests_cache.CachedSession` or :py:class:`aiohttp_client_cache.session.CachedSession` respectively. +See the docs for `requests_cache `__ and `aiohttp_client_cache `__ for backend options and more. .. code-block:: python + from datetime import timedelta from homeassistant_api import Client from requests_cache import CachedSession client = Client( "", "", - cache_session=CachedSession( + session=CachedSession( backend="filesystem", - expire_after=timedelta(minutes=5) - ) + expire_after=timedelta(minutes=5), + ), ) - # CachedSession is activated by the `with` statement. with client: # Grab and update some cool entities and services inside your installation. ... +.. code-block:: python + # Or an example for async import asyncio - from homeassistant_api import Client + from datetime import timedelta + from homeassistant_api import AsyncClient from aiohttp_client_cache import CachedSession, FileBackend - client = Client( - "", + client = AsyncClient( + "", "", - cache_session=CachedSession( + session=CachedSession( cache=FileBackend( - expire_after=timedelta(minutes=5) - ) + expire_after=timedelta(minutes=5), + ), ), - use_async=True ) + async def main(): async with client: # Grab and update some cool entities and services inside your installation. ... + asyncio.run(main()) Why the heck is :py:class:`Client` a context manager? ******************************************************** -The :py:class:`Client` is a context manager because it activates the cache session and pings Home Assistant to make sure its running. -You might not want this behavior, if you don't then don't use the :code:`with` or :code:`async with` statement. -You can still use the client without it, but you will have to manually activate the cache session before you use it. - -Disabling Caching -****************** - -To explicitly disable the default cache you can pass :code:`cache_session=False` or :code:`async_cache_session=False` to :py:class:`Client`'s init method depending on your use case. -Otherwise the default cache will be used by default when you use :code:`with client:` or :code:`async with client:`. - - -Response Processing -********************** -Home Assistant API uses functions called processors. -These functions take a Response object as a parameter and return the python data type associated with the content-type header. - -How To Register Response Processors (Converters) -================================================== - -To register a response processor you need to import the :py:class:`Processing` class and then implement the decorator. - - -.. code-block:: python - - from homeassistant_api import Processing, Client - from homeassistant_api.processing import process_json - - - @Processing.processor("application/octet-stream") - def text_processor(response): - return response.text.lower() - - @Processing.processor("text/csv") - async def async_text_processor(response): - text = await response.text() - return [line.split(",") for line in text.splitlines()] - - @Processing.processor("application/json") - def json_processor(response): - print("I processed a json response!) - return process_json(response) - - - client = Client(url, token) - print(client.get_entities()) - - -In this example. -The first processor (a function wrapped with the processor decorator) is going to be called when we receive a response that has that as its :code:`Content-Type` header. -:code:`homeassistant_api` provides processors for :code:`application/octet-stream` and :code:`application/json` by default, -But :code:`@Processing.processor` gives the most recently registered processor the highest precedence when choosing a processor for a response. -So our processor here will be chosen over the default processors. - -The second processor is an async processor that only gets called when Client receives an async response that has :code:`text/csv` as its :code:`Content-Type` header. -If you wanted, you could not use :code:`homeassistant_api`'s default json processing using the :code:`json` module, -and use instead the :code:`ujson` module (which is faster but more restrictive). - -The third processor function implements the default processor function for the :code:`application/json` mimetype after printing a string. -If you wanted to run some intermediate processing. - -Most likely the only processors you will ever use are :code:`application/json` and :code:`application/octet-stream` +The :py:class:`Client` is a context manager because it manages the underlying HTTP session and pings Home Assistant to make sure it's running. +You don't have to use the context manager — the client works without it — but you'll need to manage the session lifecycle yourself. diff --git a/docs/extensions/resourcelinks.py b/docs/extensions/resourcelinks.py index d37af4c9..9c807c93 100644 --- a/docs/extensions/resourcelinks.py +++ b/docs/extensions/resourcelinks.py @@ -2,28 +2,34 @@ # Copyright 2007-2020 by the Sphinx team # Licensed under BSD. -from typing import Any, Dict, List, Tuple +from typing import Any import sphinx -from docutils import nodes, utils -from docutils.nodes import Node, system_message +from docutils import nodes +from docutils import utils +from docutils.nodes import Node +from docutils.nodes import system_message from docutils.parsers.rst.states import Inliner from sphinx.application import Sphinx from sphinx.util.nodes import split_explicit_title from sphinx.util.typing import RoleFunction -def make_link_role(resource_links: Dict[str, str]) -> RoleFunction: +def make_link_role(resource_links: dict[str, str]) -> RoleFunction: def role( typ: str, rawtext: str, text: str, lineno: int, inliner: Inliner, - options: Dict = {}, - content: List[str] = [], - ) -> Tuple[List[Node], List[system_message]]: - + options: dict | None = None, + content: list[str] | None = None, + ) -> tuple[list[Node], list[system_message]]: + + if content is None: + content = [] + if options is None: + options = {} text = utils.unescape(text) has_explicit_title, title, key = split_explicit_title(text) full_url = resource_links[key] @@ -39,7 +45,7 @@ def add_link_role(app: Sphinx) -> None: app.add_role("resource", make_link_role(app.config.resource_links)) -def setup(app: Sphinx) -> Dict[str, Any]: +def setup(app: Sphinx) -> dict[str, Any]: app.add_config_value("resource_links", {}, "env") app.connect("builder-inited", add_link_role) return {"version": sphinx.__display_version__, "parallel_read_safe": True} diff --git a/docs/index.rst b/docs/index.rst index 742970e8..643204d6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,11 +30,11 @@ Features ---------- - Full consumption of the Home Assistant REST API endpoints. -- Full consumption of the Home Assistant Websocket API (all of the documented commands and some undocumented ones) +- Full consumption of the Home Assistant Websocket API (all of the documented commands and some undocumented ones). - Convenient Pydantic Models for data validation. -- Syncrononous and Asynchronous support for integrating with all applications and/or libraries. +- Synchronous and asynchronous support for both REST and WebSocket clients. - Modular design for intuitive readability. -- Request caching for more efficient repeative requests. +- Request caching for more efficient repetitive requests. Getting Started ------------------- diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 8ae128ca..c93b2273 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -55,14 +55,14 @@ Installation with pip is really easy and will install the dependencies this proj # To install the latest stable version from PyPI $ pip install homeassistant_api - # To install the latest dev version (you'll need to use poetry because pip, by itself, does not understand poetry dependencies.) - $ poetry add git+https://github.com/GrandMoff100/HomeassistantAPI + # To install the latest dev version + $ pip install git+https://github.com/GrandMoff100/HomeassistantAPI Installing with :code:`git` ---------------------------------- -To install with git we're going to clone the repository and then run :code:`$ poetry install` like so. +To install with git we're going to clone the repository and then run :code:`$ uv sync` like so. .. code-block:: bash @@ -70,13 +70,13 @@ To install with git we're going to clone the repository and then run :code:`$ po git clone https://github.com/GrandMoff100/HomeassistantAPI # CD into your project - cd + cd HomeAssistantAPI - # Install poetry - python -m pip install poetry + # Install uv (see https://docs.astral.sh/uv/ for other methods) + python -m pip install uv - # Run poetry install - python -m poetry install ~/HomeAssistantAPI + # Install dependencies + uv sync Then you should be all set to start using the library! diff --git a/docs/usage.rst b/docs/usage.rst index d879f36d..cb8f71c5 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -6,9 +6,15 @@ Usage The Basics... ################# -This library is centered around the :py:class:`Client` and :py:class:`WebsocketClient` classes. -Once you have have your api base url and Long Lived Access Token from Home Assistant we can start to do stuff. -The rest of this guide assumes you have the :py:class:`Client` saved to a :code:`client` variable or a :py:class:`WebsocketClient` saved to a :code:`ws_client` variable. +This library provides four client classes: + +- :py:class:`Client` — sync REST API client +- :py:class:`AsyncClient` — async REST API client +- :py:class:`WebsocketClient` — sync WebSocket client +- :py:class:`AsyncWebsocketClient` — async WebSocket client + +Once you have your API base URL and Long Lived Access Token from Home Assistant we can start to do stuff. +The rest of this guide assumes you have a client saved to a variable as shown below. Most of these examples require some integrations to be setup inside Home Assistant for the examples to actually work. The most commonly used features of this library include triggering services and getting and modifying entity states. @@ -18,7 +24,7 @@ The most commonly used features of this library include triggering services and import os from homeassistant_api import Client - URL = '' # Example: 'https://foobarhomeassistant.duckdns.org:8123/api' + URL = '' # Example: 'http://homeassistant.local:8123/api' TOKEN = '' # Assigns the Client object to a variable and checks if it's running. @@ -35,11 +41,11 @@ The most commonly used features of this library include triggering services and from homeassistant_api import WebsocketClient - WS_URL = '' # Example: 'https://foobarhomeassistant.duckdns.org:8123/api/websocket' + WS_URL = '' # Example: 'ws://homeassistant.local:8123/api/websocket' TOKEN = '' with WebsocketClient(WS_URL, TOKEN) as ws_client: # opens a websocket connection to Home Assistant - print(ws_client.render_template("{{ states('sensor.my_sensor') }}")) + print(ws_client.get_rendered_template("{{ states('sensor.my_sensor') }}")) .. code-block:: python @@ -50,9 +56,9 @@ The most commonly used features of this library include triggering services and # You can also initialize Client before you use it. - client = Client("https://foobarhomeassistant.duckdns.org:8123/api", "mylongtokenpasswordthingyfoobar") + client = Client("http://homeassistant.local:8123/api", "mylongtokenpasswordthingyfoobar") - # In order to activate the requests session you to use the Client context manager like so. + # In order to activate the requests session you need to use the Client context manager like so. # Using it as a context manager will automatically close the session when you're done with it. # But also will *ping* your Home Assistant instance to make sure it's running. with client: @@ -108,63 +114,58 @@ Entities ) # - # Alternatively you can set state from the entity class itself - from homeassistant_api import State - - # If you are wondering where door came from its about 15 lines up. - door.set_state(State(state="My new state", attributes={"open_height": "5ft"})) + # Alternatively you can update state from the entity class itself. + # Modify the entity's local state object, then push it to Home Assistant. + door.state.state = 'My new state' + door.state.attributes.update({'open_height': '5ft'}) + door.update_state() # - ## All of these methods can be used with the WebsocketClient as well [except for set_state because the WS API doesn't support it :((( ]. - -Using Client with :code:`async`/:code:`await` -************************************************* -Are you wondering if you can use :code:`homeassistant_api` using Python's :code:`async`/:code:`await` syntax? -Good news! You can! + # All of these methods work with the WebsocketClient as well. -(You can't use the WebsocketClient with :code:`async`/:code:`await` yet because we haven't implemented it yet.) +Using :py:class:`AsyncClient` +********************************* +All four client classes share the same method names. +The async clients (:py:class:`AsyncClient` and :py:class:`AsyncWebsocketClient`) simply use :code:`async def` methods that you :code:`await`. Async Services ******************** .. code-block:: python import asyncio - from homeassistant_api import Client - - # Initialize client like usual, except with the :code:`use_async` keyword. - client = Client(url, token, use_async=True) + from homeassistant_api import AsyncClient async def main(): + async with AsyncClient(url, token) as client: + domains = await client.get_domains() + print(domains) + # {'homeassistant': , 'notify': } - domains = await client.async_get_domains() - print(domains) - # {'homeassistant': , 'notify': } - - cover = await client.async_get_domain("cover") + cover = await client.get_domain("cover") - changed_states = await cover.close_cover(entity_id='cover.garage_door') - # [] + changed_states = await cover.close_cover(entity_id='cover.garage_door') + # (,) - asyncio.get_event_loop().run_until_complete(main()) + asyncio.run(main()) Async Entities ***************** .. code-block:: python - entity_groups = await client.async_get_entities() - # {'person': , 'zone': , ...} + entity_groups = await client.get_entities() + # {'person': , 'zone': , ...} - door = await client.async_get_entity(entity_id='cover.garage_door') - # "> + door = await client.get_entity(entity_id='cover.garage_door') + # "> - states = await client.async_get_states() - # [, ,...] + states = await client.get_states() + # (, ,...) - state = await client.async_get_state('sun.sun') + state = await client.get_state('sun.sun') # - new_state = await client.async_set_state( + new_state = await client.set_state( State( state='my ToaTallY Whatever vAlUe 12t87932', entity_id='my_favorite_colors.number_one' @@ -172,15 +173,29 @@ Async Entities ) # - # Alternatively you can set state from the entity class itself - from homeassistant_api import State - - # If you are wondering where door came from its about 15 lines up. + # Alternatively you can update state from the entity class itself. door.state.state = 'My new state' door.state.attributes.update({'open_height': '5ft'}) - await door.async_set_state(door.state) + await door.update_state() # +Async WebSocket +****************** + +The :py:class:`AsyncWebsocketClient` works the same way: + +.. code-block:: python + + import asyncio + from homeassistant_api import AsyncWebsocketClient + + async def main(): + async with AsyncWebsocketClient(ws_url, token) as ws_client: + template = await ws_client.get_rendered_template("{{ states('sensor.my_sensor') }}") + print(template) + + asyncio.run(main()) + Using Events (Listening and Firing) ***************************************** @@ -189,14 +204,14 @@ Using Events (Listening and Firing) from homeassistant_api import WebsocketClient - WS_URL = '' # Example: 'https://foobarhomeassistant.duckdns.org:8123/api/websocket' + WS_URL = '' # Example: 'ws://homeassistant.local:8123/api/websocket' TOKEN = '' with WebsocketClient(WS_URL, TOKEN) as ws_client: with ws_client.listen_events() as events: for event in events: print(event) - + # Or if you want to listen for a specific event type until dinner time. with ws_client.listen_events('state_changed') as events: for event in events: @@ -208,7 +223,7 @@ Using Events (Listening and Firing) with ws_client.listen_events("my_event") as events: for _, event in zip(range(10), events): print(event) - + # Alternatively for just one event. with ws_client.listen_events("my_event") as events: event = next(events) @@ -226,12 +241,12 @@ Listening for Triggers from homeassistant_api import WebsocketClient with WebsocketClient(WS_URL, TOKEN) as ws_client: - with ws_client.listen_triggers() as triggers: # see WebsocketClient.listen_triggers for more info. + with ws_client.listen_trigger() as triggers: # see WebsocketClient.listen_trigger for more info. for trigger in triggers: print(trigger) - + # Another more specific example, listening for event triggers. - with ws_client.listen_triggers("event", event_type="my_event") as triggers: + with ws_client.listen_trigger("event", event_type="my_event") as triggers: ws_client.fire_event("my_event", my_arg="my_value") for trigger in triggers: diff --git a/examples/async_get_entities.py b/examples/async_get_entities.py index d2d5933a..9e047fa0 100644 --- a/examples/async_get_entities.py +++ b/examples/async_get_entities.py @@ -1,20 +1,19 @@ import asyncio import os -from homeassistant_api import Client +from homeassistant_api import AsyncClient url = os.getenv("HOMEASSISTANT_API_ENDPOINT") token = os.getenv("HOMEASSISTANT_API_TOKEN") -async def main(): +async def main() -> None: # Initialize main object - client = Client(url, token, use_async=True) + client = AsyncClient(url, token) # Uses async context manager to ping the server and initialize caching. async with client: # All async methods are prefixed with `async_`. - data = await client.async_get_entities() - print(data) + await client.get_entities() loop = asyncio.get_event_loop() diff --git a/examples/basic.py b/examples/basic.py index 4ffefd9f..d6a7e946 100644 --- a/examples/basic.py +++ b/examples/basic.py @@ -4,17 +4,23 @@ api_url = "https://homeassistant.duckdns.org:8123/api" # Something like http://localhost:8123/api token = os.getenv( - "HOMEASSISTANT_TOKEN" + "HOMEASSISTANT_TOKEN", ) # Used to aunthenticate yourself with homeassistant # See the documentation on how to obtain a Long Lived Access Token -assert token is not None -with Client( - api_url, - token, -) as client: # Create Client object and check that its running. - cover = client.get_domain("cover") +def main() -> None: + with Client( + api_url, + token, + ) as client: # Create Client object and check that its running. + cover = client.get_domain("cover") + if cover is None: + return - # Tells Home Assistant to trigger the toggle service on the given entity_id - cover.toggle(entity_id="cover.garage_door") + # Tells Home Assistant to trigger the toggle service on the given entity_id + cover.toggle(entity_id="cover.garage_door") + + +if __name__ == "__main__": + main() diff --git a/examples/set_sensors.py b/examples/set_sensors.py index 494f2e34..9bc4031d 100644 --- a/examples/set_sensors.py +++ b/examples/set_sensors.py @@ -1,10 +1,10 @@ from homeassistant_api import Client +from homeassistant_api import State with Client( "http://homeassistant.local:8123/api", "myfabulousapikey", ) as client: new_state = client.set_state( - entity_id="sensor.some_variable", state="42 the answer to everything" + state=State(entity_id="some_entity", state="42 the answer to everything"), ) - print(new_state) diff --git a/examples/toggle_light.py b/examples/toggle_light.py index 8ae9f0ec..e647c2fd 100644 --- a/examples/toggle_light.py +++ b/examples/toggle_light.py @@ -6,13 +6,16 @@ token = os.getenv("TOKEN") -if api_url is not None and token is not None: +def main() -> None: # Intitializes the main Client client = Client(api_url, token) # Verifies the extistence of the specified server and opens efficient ClientSessions. with client: - # Gets the cover service domain light = client.get_domain("light") - assert light is not None - # Triggers the service with a specific garage door - print(light.toggle(entity_id="light.light_bulb_1")) + if light is None: + return + print(light.model_dump()) # noqa: T201 + + +if __name__ == "__main__": + main() diff --git a/homeassistant_api/__init__.py b/homeassistant_api/__init__.py index cc637912..caeff0e3 100644 --- a/homeassistant_api/__init__.py +++ b/homeassistant_api/__init__.py @@ -1,48 +1,127 @@ """Interact with your Homeassistant Instance remotely.""" __all__ = ( + "AsyncClient", + "AsyncDomain", + "AsyncEntity", + "AsyncEvent", + "AsyncGroup", + "AsyncService", + "AsyncWebsocketClient", + "AuthInvalid", + "AuthOk", + "AuthRequired", + "BaseDomain", + "BaseEntity", + "BaseEvent", + "BaseGroup", + "BaseService", "Client", - "State", + "ConfigEntry", + "ConfigEntryChange", + "ConfigEntryDisabler", + "ConfigEntryEvent", + "ConfigEntryState", + "ConfigFlowContext", + "ConfigSubEntry", "Context", + "DisableEnableResult", + "DiscoveryKey", "Domain", - "Service", - "Group", "Entity", - "History", + "EntityCategory", + "EntityDisabledBy", + "EntityHiddenBy", + "EntityRegistryEntry", + "EntityRegistryEntryExtended", + "EntityRegistryUpdateResult", + "ErrorResponse", "Event", + "EventResponse", + "FlowContext", + "FlowResult", + "FlowResultType", + "Group", + "History", + "IntegrationTypes", "LogbookEntry", - "WebsocketClient", - "AuthInvalid", - "AuthOk", - "AuthRequired", - "ResultResponse", - "ErrorResponse", "PingResponse", - "EventResponse", + "ResultResponse", + "Service", + "ServiceField", + "State", + "WebsocketClient", ) +from .asyncclient import AsyncClient +from .asyncwebsocket import AsyncWebsocketClient from .client import Client -from .models.domains import Domain, Service -from .models.entity import Entity, Group +from .models.config_entries import ConfigEntry +from .models.config_entries import ConfigEntryChange +from .models.config_entries import ConfigEntryDisabler +from .models.config_entries import ConfigEntryEvent +from .models.config_entries import ConfigEntryState +from .models.config_entries import ConfigFlowContext +from .models.config_entries import ConfigSubEntry +from .models.config_entries import DisableEnableResult +from .models.config_entries import DiscoveryKey +from .models.config_entries import FlowContext +from .models.config_entries import FlowResult +from .models.config_entries import FlowResultType +from .models.config_entries import IntegrationTypes +from .models.domains import AsyncDomain +from .models.domains import AsyncService +from .models.domains import BaseDomain +from .models.domains import BaseService +from .models.domains import Domain +from .models.domains import Service +from .models.domains import ServiceField +from .models.domains import ServiceFieldSelector +from .models.domains import ServiceFieldSelectorObjectField +from .models.entity import AsyncEntity +from .models.entity import AsyncGroup +from .models.entity import BaseEntity +from .models.entity import BaseGroup +from .models.entity import Entity +from .models.entity import Group +from .models.entity_registry import EntityCategory +from .models.entity_registry import EntityDisabledBy +from .models.entity_registry import EntityHiddenBy +from .models.entity_registry import EntityRegistryEntry +from .models.entity_registry import EntityRegistryEntryExtended +from .models.entity_registry import EntityRegistryUpdateResult +from .models.events import AsyncEvent +from .models.events import BaseEvent from .models.events import Event from .models.history import History from .models.logbook import LogbookEntry -from .models.states import Context, State -from .models.websocket import ( - AuthInvalid, - AuthOk, - AuthRequired, - ErrorResponse, - EventResponse, - PingResponse, - ResultResponse, -) +from .models.states import Context +from .models.states import State +from .models.websocket import AuthInvalid +from .models.websocket import AuthOk +from .models.websocket import AuthRequired +from .models.websocket import ErrorResponse +from .models.websocket import EventResponse +from .models.websocket import PingResponse +from .models.websocket import ResultResponse from .websocket import WebsocketClient +AsyncDomain.model_rebuild() +AsyncEntity.model_rebuild() +AsyncEvent.model_rebuild() +AsyncGroup.model_rebuild() +AsyncService.model_rebuild() +BaseDomain.model_rebuild() +BaseEntity.model_rebuild() +BaseEvent.model_rebuild() +BaseGroup.model_rebuild() +BaseService.model_rebuild() Domain.model_rebuild() Entity.model_rebuild() Event.model_rebuild() Group.model_rebuild() History.model_rebuild() Service.model_rebuild() +ServiceFieldSelector.model_rebuild() +ServiceFieldSelectorObjectField.model_rebuild() State.model_rebuild() diff --git a/homeassistant_api/asyncclient.py b/homeassistant_api/asyncclient.py new file mode 100644 index 00000000..11ef0f59 --- /dev/null +++ b/homeassistant_api/asyncclient.py @@ -0,0 +1,411 @@ +"""Module for interacting with Home Assistant asynchronously.""" + +from __future__ import annotations + +import asyncio +import json +import logging +from http import HTTPMethod +from posixpath import join +from typing import TYPE_CHECKING +from typing import Any + +from niquests import AsyncSession + +from .baseclient import BaseClient +from .errors import BadTemplateError +from .errors import RequestError +from .errors import RequestTimeoutError +from .models import AsyncDomain +from .models import AsyncEntity +from .models import AsyncEvent +from .models import AsyncGroup +from .models import History +from .models import LogbookEntry +from .models import State +from .processing import ResponseType +from .processing import async_process_response +from .utils import prepare_entity_id + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator + from datetime import datetime + from types import TracebackType + + from typing_extensions import Self + +logger = logging.getLogger(__name__) + + +class AsyncClient(BaseClient): + """ + The async client for interacting with Home Assistant via the REST API. + + :param api_url: The location of the api endpoint. e.g. :code:`http://localhost:8123/api` Required. + :param token: The refresh or long lived access token to authenticate your requests. Required. + :param session: A custom :py:class:`niquests.AsyncSession` instance. Optional. + :param verify_ssl: Whether to verify SSL certificates. Default :code:`True`. + :param global_request_kwargs: Kwargs to pass to :meth:`niquests.AsyncSession.request`. Optional. + """ # pylint: disable=line-too-long + + _session: AsyncSession + + def __init__( + self, + *args: Any, + session: AsyncSession | None = None, + verify_ssl: bool = True, + **kwargs: Any, + ) -> None: + super().__init__(*args, **kwargs) + self.global_request_kwargs["verify"] = verify_ssl + if session is not None: + self._session = session + else: + self._session = AsyncSession() + + async def __aenter__(self) -> Self: + logger.debug("Entering cached async requests session %r", self._session) + await self._session.__aenter__() + await self.check_api_running() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + logger.debug("Exiting async requests session %r", self._session) + await self._session.close() + + # Very important request function + async def request( + self, + path: str, + *, + params: dict[str, Any] | None = None, + method: HTTPMethod = HTTPMethod.GET, + headers: dict[str, str] | None = None, + **kwargs: Any, + ) -> Any: + """Base method for making requests to the api""" + path = self.endpoint(path) + if params: + path = f"{path}?{self.construct_params(params)}" + if self.global_request_kwargs is not None: + kwargs.update(self.global_request_kwargs) + try: + resp = await self._session.request( + method, + path, + headers=self.prepare_headers(headers), + **kwargs, + ) + except asyncio.exceptions.TimeoutError as err: + msg = f"Home Assistant did not respond in time (timeout: {kwargs.get('timeout', 300)} sec)" + raise RequestTimeoutError(msg, path) from err + return await self.response_logic(resp) + + async def _dict_request(self, *args: Any, **kwargs: Any) -> dict[str, Any]: + data = await self.request(*args, **kwargs) + if not isinstance(data, dict): + msg = f"Expected dict response, got {type(data).__name__}" + raise TypeError(msg) + return data + + async def _list_request(self, *args: Any, **kwargs: Any) -> list: + data = await self.request(*args, **kwargs) + if not isinstance(data, list): + msg = f"Expected list response, got {type(data).__name__}" + raise TypeError(msg) + return data + + async def _str_request(self, *args: Any, **kwargs: Any) -> str: + data = await self.request(*args, **kwargs) + if not isinstance(data, str): + msg = f"Expected str response, got {type(data).__name__}" + raise TypeError(msg) + return data + + @staticmethod + async def response_logic(response: ResponseType) -> Any: + """Processes custom mimetype content asynchronously.""" + return await async_process_response(response) + + # API information methods + async def get_error_log(self) -> str: + """ + Returns the server error log as a string. + :code:`GET /api/error_log` + """ + return await self._str_request("error_log") + + async def get_config(self) -> dict[str, Any]: + """ + Returns the configuration of Home Assistant. + :code:`GET /api/config` + """ + return await self._dict_request("config") + + async def get_logbook_entries( + self, + *args: Any, + **kwargs: Any, + ) -> AsyncGenerator[LogbookEntry, None]: + """ + Returns a list of logbook entries from Home Assistant. + :code:`GET /api/logbook/` + """ + params, url = self.prepare_get_logbook_entry_params(*args, **kwargs) + data = await self._list_request(url, params=params) + for entry in data: + yield LogbookEntry.model_validate(entry) + + async def get_entity_histories( + self, + entities: tuple[AsyncEntity, ...] | None = None, + start_timestamp: datetime | None = None, + # Defaults to 1 day before. https://developers.home-assistant.io/docs/api/rest/ + end_timestamp: datetime | None = None, + *, + significant_changes_only: bool = False, + ) -> AsyncGenerator[History, None]: + """ + Yields entity state histories. See docs on the :py:class:`History` model. + :code:`GET /api/history/period/` + """ + params, url = self.prepare_get_entity_histories_params( + entities=entities, + start_timestamp=start_timestamp, + end_timestamp=end_timestamp, + significant_changes_only=significant_changes_only, + ) + data = await self._list_request(url, params=params) + for states in data: + yield History.model_validate({"states": states}) + + async def get_rendered_template(self, template: str) -> str: + """ + Renders a given Jinja2 template string with Home Assistant context data. + :code:`POST /api/template` + """ + try: + return await self._str_request( + "template", + json={"template": template}, + method=HTTPMethod.POST, + ) + except RequestError as err: + msg = ( + "Your template is invalid. " + "Try debugging it in the developer tools page of Home Assistant." + ) + raise BadTemplateError(msg) from err + + # API check methods + async def check_api_config(self) -> bool: + """ + Asks Home Assistant to validate its configuration file. + :code:`POST /api/config/core/check_config` + """ + res = await self._dict_request( + "config/core/check_config", + method=HTTPMethod.POST, + ) + return {"valid": True, "invalid": False}.get(res["result"], False) + + async def check_api_running(self) -> bool: + """ + Asks Home Assistant if it is running. + :code:`GET /api/` + """ + res = await self._dict_request("") + return res.get("message") == "API running." + + # Entity methods + async def get_entities(self) -> dict[str, AsyncGroup]: + """ + Fetches all entities from the api and returns them as a dictionary of :py:class:`AsyncGroup`'s. + :code:`GET /api/states` + """ + entities: dict[str, AsyncGroup] = {} + for state in await self.get_states(): + group_id, entity_slug = state.entity_id.split(".") + if group_id not in entities: + entities[group_id] = AsyncGroup(group_id=group_id, client=self) + entities[group_id]._add_entity(entity_slug, state) # noqa: SLF001 + return entities + + async def get_entity( + self, + group_id: str | None = None, + slug: str | None = None, + entity_id: str | None = None, + ) -> AsyncEntity | None: + """ + Returns an :py:class:`AsyncEntity` model for an :code:`entity_id`. + :code:`GET /api/states/` + """ + if group_id is not None and slug is not None: + state = await self.get_state(group_id=group_id, slug=slug) + elif entity_id is not None: + state = await self.get_state(entity_id=entity_id) + else: + help_msg = ( + "Use keyword arguments to pass entity_id. " + "Or you can pass the group_id and slug instead." + ) + msg = f"Neither group_id and slug or entity_id provided. {help_msg}" + raise ValueError(msg) + group_id, entity_slug = state.entity_id.split(".") + group = AsyncGroup(group_id=group_id, client=self) + group._add_entity(entity_slug, state) # noqa: SLF001 + return group.get_entity(entity_slug) + + # Services and domain methods + async def get_domains(self) -> dict[str, AsyncDomain]: + """ + Fetches all service :py:class:`AsyncDomain`'s from the API. + :code:`GET /api/services` + """ + data = await self._list_request("services") + domains = ( + AsyncDomain.from_json_with_client(json, client=self) for json in data + ) + return {domain.domain_id: domain for domain in domains} + + async def get_domain(self, domain_id: str) -> AsyncDomain | None: + """ + Fetches all :py:class:`AsyncService`'s under a particular service :py:class:`AsyncDomain`. + Uses cached data from :py:meth:`get_domains` if available. + """ + domains = await self.get_domains() + return domains.get(domain_id) + + async def trigger_service( + self, + domain: str, + service: str, + **service_data: Any, + ) -> tuple[State, ...]: + """ + Tells Home Assistant to trigger a service, returns all states changed while in the process of being called. + :code:`POST /api/services//` + """ + data = await self._list_request( + f"services/{domain}/{service}", + method=HTTPMethod.POST, + json=service_data, + ) + return tuple(map(State.from_json, data)) + + async def trigger_service_with_response( + self, + domain: str, + service: str, + **service_data: Any, + ) -> tuple[tuple[State, ...], dict[str, Any]]: + """ + Tells Home Assistant to trigger a service, returns the response from the service call. + :code:`POST /api/services//` + + Returns a list of the states changed and the response from the service call. + """ + data = await self._dict_request( + join("services", domain, service) + "?return_response", + method=HTTPMethod.POST, + json=service_data, + ) + states = tuple( + map( + State.from_json, + data.get("changed_states", []), + ), + ) + return states, data.get("service_response", {}) + + # EntityState methods + async def get_state( # pylint: disable=duplicate-code + self, + *, + entity_id: str | None = None, + group_id: str | None = None, + slug: str | None = None, + ) -> State: + """ + Fetches the state of the entity specified. + :code:`GET /api/states/` + """ + target_entity_id = prepare_entity_id( + group_id=group_id, + slug=slug, + entity_id=entity_id, + ) + data = await self._dict_request(join("states", target_entity_id)) + return State.from_json(data) + + async def set_state( # pylint: disable=duplicate-code + self, + state: State, + ) -> State: + """ + This method sets the representation of a device within Home Assistant and will not communicate with the actual device. + To communicate with the device, use :py:meth:`AsyncService.trigger`. + :code:`POST /api/states/` + """ + data = await self._dict_request( + join("states", state.entity_id), + method=HTTPMethod.POST, + json=json.loads(state.model_dump_json()), + ) + return State.from_json(data) + + async def get_states(self) -> tuple[State, ...]: + """ + Gets the states of all entities within Home Assistant. + :code:`GET /api/states` + """ + data = await self._list_request("states") + return tuple(map(State.from_json, data)) + + # Event methods + async def get_events(self) -> tuple[AsyncEvent, ...]: + """ + Gets the Events that happen within Home Assistant. + :code:`GET /api/events` + """ + data = await self._list_request("events") + return tuple( + AsyncEvent.from_json_with_client(json, client=self) for json in data + ) + + async def get_event(self, name: str) -> AsyncEvent | None: + """ + Gets the :py:class:`AsyncEvent` with the specified name if it has at least one listener. + Uses cached data from :py:meth:`get_events` if available. + """ + for event in await self.get_events(): + if event.event == name.strip().lower(): + return event + return None + + async def fire_event(self, event_type: str, **event_data: Any) -> str: + """ + Fires a given event_type within Home Assistant. + :code:`POST /api/events/` + """ + data = await self._dict_request( + join("events", event_type), + method=HTTPMethod.POST, + json=event_data, + ) + return data.get("message", "No message provided") + + async def get_components(self) -> tuple[str, ...]: + """ + Returns a tuple of all registered components. + :code:`GET /api/components` + """ + data = await self._list_request("components") + return tuple(data) diff --git a/homeassistant_api/asyncwebsocket.py b/homeassistant_api/asyncwebsocket.py new file mode 100644 index 00000000..1d7a6d38 --- /dev/null +++ b/homeassistant_api/asyncwebsocket.py @@ -0,0 +1,779 @@ +from __future__ import annotations + +import contextlib +import json +import logging +import time +from typing import TYPE_CHECKING +from typing import Any + +from niquests import AsyncSession +from pydantic import ValidationError +from typing_extensions import Self + +from homeassistant_api.basewebsocket import BaseWebsocketClient +from homeassistant_api.errors import ReceivingError +from homeassistant_api.errors import ResponseError +from homeassistant_api.errors import UnauthorizedError +from homeassistant_api.models import AsyncDomain +from homeassistant_api.models import AsyncEntity +from homeassistant_api.models import AsyncGroup +from homeassistant_api.models import ConfigEntry +from homeassistant_api.models import ConfigEntryEvent +from homeassistant_api.models import ConfigSubEntry +from homeassistant_api.models import History +from homeassistant_api.models import State +from homeassistant_api.models.config_entries import DisableEnableResult +from homeassistant_api.models.config_entries import FlowResult +from homeassistant_api.models.entity_registry import EntityRegistryEntry +from homeassistant_api.models.entity_registry import EntityRegistryEntryExtended +from homeassistant_api.models.entity_registry import EntityRegistryUpdateParams +from homeassistant_api.models.entity_registry import EntityRegistryUpdateResult +from homeassistant_api.models.states import Context +from homeassistant_api.models.websocket import AuthInvalid +from homeassistant_api.models.websocket import AuthOk +from homeassistant_api.models.websocket import AuthRequired +from homeassistant_api.models.websocket import EventResponse +from homeassistant_api.models.websocket import FiredEvent +from homeassistant_api.models.websocket import FiredTrigger +from homeassistant_api.models.websocket import PingResponse +from homeassistant_api.models.websocket import ResultResponse +from homeassistant_api.models.websocket import TemplateEvent +from homeassistant_api.utils import prepare_entity_id + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator + from datetime import datetime + from types import TracebackType + + from urllib3.contrib.webextensions._async.protocol import AsyncExtensionFromHTTP + +logger = logging.getLogger(__name__) + + +class AsyncWebsocketClient(BaseWebsocketClient): + _session: AsyncSession + _ws: AsyncExtensionFromHTTP + + def __init__( + self, + api_url: str, + token: str, + *, + max_size: int = 2**24, + session: AsyncSession | None = None, + ) -> None: + super().__init__(api_url, token, max_size=max_size) + self._session = session if session is not None else AsyncSession() + + async def __aenter__(self) -> Self: + await self._session.__aenter__() + resp = await self._session.get(self.api_url) + resp.raise_for_status() + if resp.extension is None: + msg = "Server did not upgrade to WebSocket" + raise ReceivingError(msg) + self._ws = resp.extension + okay = await self.authentication_phase() + logger.info("Authenticated with Home Assistant (%s)", okay.ha_version) + await self.supported_features_phase() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + await self._ws.close() + del self._ws + await self._session.__aexit__(exc_type, exc_value, traceback) + + async def _async_send(self, data: dict[str, Any]) -> None: + """Send a message to the websocket server.""" + if data.get("type") != "auth": + logger.debug(f"Sending message: {data}") + await self._ws.send_payload(json.dumps(data)) + + async def _async_recv(self) -> dict[str, Any]: + """Receive a complete JSON message from the websocket server, buffering fragments.""" + buf = "" + while True: + chunk = await self._ws.next_payload() + if chunk is None: + msg = "WebSocket connection closed" + raise ReceivingError(msg) + buf += chunk if isinstance(chunk, str) else chunk.decode() + try: + r = json.loads(buf) + except json.JSONDecodeError: # pragma: no cover + continue + logger.debug("Received message: %s", buf) + if not isinstance(r, dict): + msg = f"Expected dict, got {type(r).__name__}" + raise TypeError(msg) + return r + + async def send(self, msg_type: str, *, include_id: bool = True, **data: Any) -> int: + """ + Send a command message to the websocket server and wait for a "result" response. + + Returns the id of the message sent. + """ + if include_id: # auth messages don't have an id + data["id"] = self._request_id() + + data["type"] = msg_type + await self._async_send(data) + + if "id" in data: + if not isinstance(data["id"], int): + msg = f"Expected int for message id, got {type(data['id'])}" + raise TypeError(msg) + if data["type"] == "ping": + self._ping_responses[data["id"]] = PingResponse( + start=time.perf_counter_ns(), + id=data["id"], + type="pong", + ) + else: + self._event_responses[data["id"]] = [] + self._result_responses[data["id"]] = None + return data["id"] + return -1 # non-command messages don't have an id + + async def recv( + self, + msg_id: int, + ) -> EventResponse | ResultResponse | PingResponse | None: + """Receive a response to a message from the websocket server.""" + while True: + ## have we received a message with the id we're looking for? + if self._result_responses.get(msg_id) is not None: + return self._result_responses.pop(msg_id) + if self._event_responses.get(msg_id, []): + return self._event_responses[msg_id].pop(0) + if ( + self._ping_responses.get(msg_id) is not None + and self._ping_responses[msg_id].end is not None + ): + return self._ping_responses.pop(msg_id) + + ## if not, keep receiving messages until we do + self.handle_recv(await self._async_recv()) + + async def recv_result(self, msg_id: int) -> ResultResponse: + """Receive a ResultResponse, raising TypeError if the response is not a ResultResponse.""" + resp = await self.recv(msg_id) + if not isinstance(resp, ResultResponse): + msg = f"Expected ResultResponse, got {type(resp).__name__}" + raise TypeError(msg) + return resp + + async def recv_result_dict(self, msg_id: int) -> dict[str, Any]: + """Receive a ResultResponse and return its result as a dict.""" + resp = await self.recv_result(msg_id) + if not isinstance(resp.result, dict): + msg = f"Expected dict result, got {type(resp.result).__name__}" + raise TypeError(msg) + return resp.result + + async def recv_result_list(self, msg_id: int) -> list[dict[str, Any]]: + """Receive a ResultResponse and return its result as a list.""" + resp = await self.recv_result(msg_id) + if not isinstance(resp.result, list): + msg = f"Expected list result, got {type(resp.result).__name__}" + raise TypeError(msg) + return resp.result + + async def recv_event(self, msg_id: int) -> EventResponse: + """Receive an EventResponse, raising TypeError if the response is not an EventResponse.""" + resp = await self.recv(msg_id) + if not isinstance(resp, EventResponse): + msg = f"Expected EventResponse, got {type(resp).__name__}" + raise TypeError(msg) + return resp + + async def recv_ping(self, msg_id: int) -> PingResponse: + """Receive a PingResponse, raising TypeError if the response is not a PingResponse.""" + resp = await self.recv(msg_id) + if not isinstance(resp, PingResponse): + msg = f"Expected PingResponse, got {type(resp).__name__}" + raise TypeError(msg) + return resp + + async def authentication_phase(self) -> AuthOk: + """Authenticate with the websocket server.""" + # Capture the first message from the server saying we need to authenticate + try: + welcome = AuthRequired.model_validate(await self._async_recv()) + logger.debug(f"Received welcome message: {welcome}") + except ValidationError as e: + msg = "Unexpected response during authentication" + raise ResponseError(msg) from e + + # Send our authentication token + await self.send("auth", access_token=self.token, include_id=False) + logger.debug("Sent auth message") + + # Check the response + resp = await self._async_recv() + try: + return AuthOk.model_validate(resp) + except ValidationError: + try: + error_resp = AuthInvalid.model_validate(resp) + raise UnauthorizedError(error_resp.message) from None + except ValidationError: + msg = f"Unexpected response during authentication: {resp}" + raise ResponseError(msg) from None + except Exception as e: + msg = f"Unexpected response during authentication: {resp}" + raise ResponseError(msg) from e + + async def supported_features_phase(self) -> None: + """Get the supported features from the websocket server.""" + resp = await self.recv_result( + await self.send( + "supported_features", + features={ + # "coalesce_messages": 42, # including this key sets it to True # noqa: ERA001 + }, + ), + ) + if resp.result is not None: + msg = "Expected None result for unsubscribe" + raise ValueError(msg) + + async def ping_latency(self) -> float: + """Get the latency (in milliseconds) of the connection by sending a ping message.""" + pong = await self.recv_ping(await self.send("ping")) + if pong.end is None: + msg = "Pong response missing end timestamp" + raise ValueError(msg) + return (pong.end - pong.start) / 1_000_000 + + async def get_rendered_template(self, template: str) -> str: + """ + Renders a Jinja2 template with Home Assistant context data. + See https://www.home-assistant.io/docs/configuration/templating. + + Sends command :code:`{"type": "render_template", ...}`. + """ + msg_id = await self.send( + "render_template", + template=template, + report_errors=True, + ) + await self.recv_result(msg_id) + second = await self.recv_event(msg_id) + await self._async_unsubscribe(msg_id) + if not isinstance(second.event, TemplateEvent): + msg = f"Expected TemplateEvent, got {type(second.event).__name__}" + raise TypeError(msg) + return str(second.event.result) + + async def get_config(self) -> dict[str, Any]: + """ + Returns the configuration of Home Assistant. + + Sends command :code:`{"type": "get_config", ...}`. + """ + return await self.recv_result_dict(await self.send("get_config")) + + async def get_states(self) -> tuple[State, ...]: + """ + Gets the states of all entities within Home Assistant. + + Sends command :code:`{"type": "get_states", ...}`. + """ + return tuple( + State.from_json(state) + for state in await self.recv_result_list(await self.send("get_states")) + ) + + async def get_state( # pylint: disable=duplicate-code + self, + *, + entity_id: str | None = None, + group_id: str | None = None, + slug: str | None = None, + ) -> State: + """ + Fetches the state of the entity specified. + + Note: The WebSocket API has no single-entity state command, so this fetches all states and filters. + + Sends command :code:`{"type": "get_states", ...}`. + """ + entity_id = prepare_entity_id( + group_id=group_id, + slug=slug, + entity_id=entity_id, + ) + + for state in await self.get_states(): + if state.entity_id == entity_id: + return state + msg = f"Entity {entity_id} not found!" + raise ValueError(msg) + + async def get_entities(self) -> dict[str, AsyncGroup]: + """ + Fetches all entities from the Websocket API and returns them as a dictionary of :py:class:`AsyncGroup`'s. + For example :code:`light.living_room` would be in the group :code:`light` (i.e. :code:`get_entities()["light"].living_room`). + """ + entities: dict[str, AsyncGroup] = {} + for state in await self.get_states(): + group_id, entity_slug = state.entity_id.split(".") + if group_id not in entities: + entities[group_id] = AsyncGroup( + group_id=group_id, + client=self, + ) + entities[group_id]._add_entity(entity_slug, state) # noqa: SLF001 + return entities + + async def get_entity( + self, + group_id: str | None = None, + slug: str | None = None, + entity_id: str | None = None, + ) -> AsyncEntity | None: + """ + Returns an :py:class:`AsyncEntity` model for an :code:`entity_id`. + + Note: The WebSocket API has no single-entity state command, so this fetches all states and filters. + """ + if group_id is not None and slug is not None: + state = await self.get_state(group_id=group_id, slug=slug) + elif entity_id is not None: + state = await self.get_state(entity_id=entity_id) + else: + help_msg = ( + "Use keyword arguments to pass entity_id. " + "Or you can pass the group_id and slug instead" + ) + msg = f"Neither group_id and slug or entity_id provided. {help_msg}" + raise ValueError(msg) + split_group_id, split_slug = state.entity_id.split(".") + group = AsyncGroup( + group_id=split_group_id, + client=self, + ) + group._add_entity(split_slug, state) # noqa: SLF001 + return group.get_entity(split_slug) + + async def set_state(self, state: State) -> State: + """Not supported over WebSocket. Use the REST :py:class:`AsyncClient` instead.""" + msg = "set_state is not supported over the WebSocket API. Use the REST AsyncClient." + raise NotImplementedError(msg) + + async def get_entity_histories( + self, + entities: tuple[AsyncEntity, ...] | None = None, # noqa: ARG002 + start_timestamp: datetime | None = None, # noqa: ARG002 + end_timestamp: datetime | None = None, # noqa: ARG002 + significant_changes_only: bool = False, # noqa: ARG002 + ) -> AsyncGenerator[History, None]: + """Not supported over WebSocket. Use the REST :py:class:`AsyncClient` instead.""" + msg = "get_entity_histories is not supported over the WebSocket API. Use the REST AsyncClient." + raise NotImplementedError(msg) + yield # unreachable: makes this a true AsyncGenerator for type checkers # type: ignore[unreachable] + + async def get_domains(self) -> dict[str, AsyncDomain]: + """ + Fetches all service :py:class:`AsyncDomain`'s from the API. + + Sends command :code:`{"type": "get_services", ...}`. + """ + result = await self.recv_result_dict(await self.send("get_services")) + domains = ( + AsyncDomain.from_json_with_client( + {"domain": item[0], "services": item[1]}, + client=self, + ) + for item in result.items() + ) + return {domain.domain_id: domain for domain in domains} + + async def get_domain(self, domain_id: str) -> AsyncDomain | None: + """ + Fetches all :py:class:`AsyncService`'s under a particular service :py:class:`AsyncDomain`. + + Note: The WebSocket API has no single-domain command, so this fetches all domains and filters. + """ + return (await self.get_domains()).get(domain_id) + + async def trigger_service( + self, + domain: str, + service: str, + **service_data: Any, + ) -> None: + """ + Trigger a service (that doesn't return a response). + + Sends command :code:`{"type": "call_service", ...}`. + + Note: Unlike the REST API, the WebSocket API does not return changed states. + Subscribe to ``state_changed`` events via :py:meth:`listen_events` to track changes. + """ + params = { + "domain": domain, + "service": service, + "service_data": service_data, + "return_response": False, + } + + result = await self.recv_result_dict( + await self.send("call_service", include_id=True, **params), + ) + + # TODO: handle result["context"] ? + + if result.get("response") is not None: + msg = "Unexpected response from service without response support" + raise ValueError(msg) + + async def trigger_service_with_response( + self, + domain: str, + service: str, + **service_data: Any, + ) -> dict[str, Any]: + """ + Trigger a service (that returns a response) and return the response. + + Sends command :code:`{"type": "call_service", ...}`. + + Note: Unlike the REST API, the WebSocket API does not return changed states, + only the service response data. Subscribe to ``state_changed`` events via + :py:meth:`listen_events` to track changes. + """ + params = { + "domain": domain, + "service": service, + "service_data": service_data, + "return_response": True, + } + + result = await self.recv_result_dict( + await self.send("call_service", include_id=True, **params), + ) + + return result["response"] + + @contextlib.asynccontextmanager + async def listen_events( + self, + event_type: str | None = None, + ) -> AsyncGenerator[AsyncGenerator[FiredEvent | FiredTrigger, None], None]: + """ + Listen for all events of a certain type. + + For example, to listen for all events of type `test_event`: + + .. code-block:: python + + async with ws_client.listen_events("test_event") as events: + async for i, event in zip(range(2), events): # to only wait for two events to be received + print(event) + """ + subscription = await self._async_subscribe_events(event_type) + yield self._async_wait_for(subscription) + await self._async_unsubscribe(subscription) + + async def _async_subscribe_events(self, event_type: str | None) -> int: + """ + Subscribe to all events of a certain type. + + + Sends command :code:`{"type": "subscribe_events", ...}`. + """ + params = {"event_type": event_type} if event_type else {} + return ( + await self.recv_result( + await self.send("subscribe_events", include_id=True, **params), + ) + ).id + + @contextlib.asynccontextmanager + async def listen_trigger( + self, + trigger: str, + **trigger_fields: Any, + ) -> AsyncGenerator[AsyncGenerator[dict[str, Any], None], None]: + """ + Listen to a Home Assistant trigger. + Allows additional trigger keyword parameters with :code:`**kwargs` (i.e. passing :code:`tag_id=...` for NFC tag triggers). + + For example, in Home Assistant Automations we can subscribe to a state trigger for a light entity with YAML: + + .. code-block:: yaml + + triggers: + # ... + - trigger: state + entity_id: light.kitchen + + To subscribe to that same state trigger with :py:class:`AsyncWebsocketClient` instead + + .. code-block:: python + + async with ws_client.listen_trigger("state", entity_id="light.kitchen") as trigger: + async for event in trigger: # will iterate until we manually break out of the loop + print(event) + if : + break + # exiting the context manager unsubscribes from the trigger + + Woohoo! We can now listen to triggers in Python code! + """ + subscription = await self._async_subscribe_trigger(trigger, **trigger_fields) + yield ( + fired_trigger.variables + async for fired_trigger in self._async_wait_for(subscription) + if isinstance(fired_trigger, FiredTrigger) + ) + await self._async_unsubscribe(subscription) + + async def _async_subscribe_trigger( + self, + trigger: str, + **trigger_fields: Any, + ) -> int: + """ + Return the subscription id of the trigger we subscribe to. + + Sends command :code:`{"type": "subscribe_trigger", ...}`. + """ + return ( + await self.recv_result( + await self.send( + "subscribe_trigger", + trigger={"platform": trigger, **trigger_fields}, + ), + ) + ).id + + async def _async_wait_for( + self, + subscription_id: int, + ) -> AsyncGenerator[FiredEvent | FiredTrigger, None]: + """ + An iterator that waits for events of a certain type. + """ + while True: + event_resp = await self.recv_event(subscription_id) + # TODO: DISCUSS Change of behavior here. before anything received would get yielded + if isinstance(event_resp.event, FiredEvent | FiredTrigger): + yield event_resp.event + + async def _async_unsubscribe(self, subcription_id: int) -> None: + """ + Unsubscribe from all events of a certain type. + + Sends command :code:`{"type": "unsubscribe_events", ...}`. + """ + try: + resp = await self.recv_result( + await self.send("unsubscribe_events", subscription=subcription_id), + ) + if resp.result is not None: + msg = "Expected None result for unsubscribe" + raise ValueError(msg) + finally: + self._event_responses.pop(subcription_id, None) + + async def get_config_entries(self) -> tuple[ConfigEntry, ...]: + """ + Get all config entries. + + Sends command :code:`{"type": "config_entries/get", ...}`. + """ + return tuple( + ConfigEntry.from_json(entry) + for entry in await self.recv_result_list( + await self.send("config_entries/get"), + ) + ) + + async def disable_config_entry(self, entry_id: str) -> DisableEnableResult: + """ + Disable a config entry. + + Sends command :code:`{"type": "config_entries/disable", ...}`. + """ + result = await self.recv_result_dict( + await self.send( + "config_entries/disable", + entry_id=entry_id, + disabled_by="user", + ), + ) + return DisableEnableResult.from_json(result) + + async def enable_config_entry(self, entry_id: str) -> DisableEnableResult: + """ + Enable a config entry. + + Sends command :code:`{"type": "config_entries/disable", ...}`. + """ + result = await self.recv_result_dict( + await self.send( + "config_entries/disable", + entry_id=entry_id, + disabled_by=None, + ), + ) + return DisableEnableResult.from_json(result) + + async def ignore_config_flow(self, flow_id: str, title: str) -> None: + """ + Ignore a config flow. + + Sends command :code:`{"type": "config_entries/ignore_flow", ...}`. + """ + await self.recv( + await self.send( + "config_entries/ignore_flow", + flow_id=flow_id, + title=title, + ), + ) + + async def get_nonuser_flows_in_progress(self) -> tuple[FlowResult, ...]: + """ + Get non-user config flows in progress. + + Sends command :code:`{"type": "config_entries/flow/progress", ...}`. + """ + return tuple( + FlowResult.from_json(flow) + for flow in await self.recv_result_list( + await self.send("config_entries/flow/progress"), + ) + ) + + async def get_entry_subentries(self, entry_id: str) -> tuple[ConfigSubEntry, ...]: + """ + Get subentries for a config entry. + + Sends command :code:`{"type": "config_entries/subentries/list", ...}`. + """ + return tuple( + ConfigSubEntry.from_json(subentry) + for subentry in await self.recv_result_list( + await self.send("config_entries/subentries/list", entry_id=entry_id), + ) + ) + + async def delete_entry_subentry(self, entry_id: str, subentry_id: str) -> None: + """ + Delete a subentry from a config entry. + + Sends command :code:`{"type": "config_entries/subentries/delete", ...}`. + """ + await self.recv( + await self.send( + "config_entries/subentries/delete", + entry_id=entry_id, + subentry_id=subentry_id, + ), + ) + + # ── Entity Registry ───────────────────────────────────────── + + async def list_entity_registry(self) -> tuple[EntityRegistryEntry, ...]: + """ + List all entity registry entries. + + Sends command :code:`{"type": "config/entity_registry/list", ...}`. + """ + return tuple( + EntityRegistryEntry.from_json(entry) + for entry in await self.recv_result_list( + await self.send("config/entity_registry/list"), + ) + ) + + async def get_entity_registry_entry( + self, + entity_id: str, + ) -> EntityRegistryEntryExtended: + """ + Get a single entity registry entry. + + Sends command :code:`{"type": "config/entity_registry/get", ...}`. + """ + result = await self.recv_result_dict( + await self.send("config/entity_registry/get", entity_id=entity_id), + ) + return EntityRegistryEntryExtended.from_json(result) + + async def update_entity_registry_entry( + self, + parameters: EntityRegistryUpdateParams, + ) -> EntityRegistryUpdateResult: + """ + Update an entity registry entry. + + Sends command :code:`{"type": "config/entity_registry/update", ...}`. + """ + result = await self.recv_result_dict( + await self.send( + "config/entity_registry/update", + **parameters, + ), + ) + return EntityRegistryUpdateResult.from_json(result) + + async def remove_entity_registry_entry(self, entity_id: str) -> None: + """ + Remove an entity from the entity registry. + + Sends command :code:`{"type": "config/entity_registry/remove", ...}`. + """ + await self.recv( + await self.send("config/entity_registry/remove", entity_id=entity_id), + ) + + @contextlib.asynccontextmanager + async def listen_config_entries( + self, + ) -> AsyncGenerator[AsyncGenerator[list[ConfigEntryEvent], None], None]: + """ + Listen for config entry changes. + + Sends command :code:`{"type": "config_entries/subscribe", ...}`. + """ + subscription = ( + await self.recv_result(await self.send("config_entries/subscribe")) + ).id + yield self._async_wait_for_config_entries(subscription) + await self._async_unsubscribe(subscription) + + async def _async_wait_for_config_entries( + self, + subscription_id: int, + ) -> AsyncGenerator[list[ConfigEntryEvent], None]: + """An async iterator that waits for config entry events.""" + while True: + event_resp = await self.recv_event(subscription_id) + if isinstance(event_resp.event, list): + yield [ConfigEntryEvent.from_json(entry) for entry in event_resp.event] + + async def fire_event(self, event_type: str, **event_data: Any) -> Context: + """ + Fires a given event_type within Home Assistant. + + Sends command :code:`{"type": "fire_event", ...}`. + """ + params: dict[str, Any] = {"event_type": event_type} + if event_data: + params["event_data"] = event_data + result = await self.recv_result_dict( + await self.send("fire_event", include_id=True, **params), + ) + return Context.from_json(result["context"]) diff --git a/homeassistant_api/rawbaseclient.py b/homeassistant_api/baseclient.py similarity index 73% rename from homeassistant_api/rawbaseclient.py rename to homeassistant_api/baseclient.py index a8a40d1c..5e282c31 100644 --- a/homeassistant_api/rawbaseclient.py +++ b/homeassistant_api/baseclient.py @@ -1,29 +1,36 @@ -"""Module for parent RawWrapper class""" +"""Module for parent BaseClient class""" -from datetime import datetime, timedelta +import urllib.parse as urlparse +from collections.abc import Iterable +from collections.abc import Mapping +from datetime import datetime +from datetime import timedelta from posixpath import join -from typing import Dict, Iterable, Mapping, Optional, Tuple, Union +from typing import Any from urllib.parse import quote_plus -from homeassistant_api.utils import JSONType - +from .models import AsyncEntity from .models import Entity -class RawBaseClient: +class BaseClient: """Builds, and makes requests to the API""" api_url: str token: str - global_request_kwargs: dict[str, JSONType] + global_request_kwargs: dict[str, Any] def __init__( self, api_url: str, token: str, *, - global_request_kwargs: Optional[Mapping[str, str]] = None, + global_request_kwargs: Mapping[str, str] | None = None, ) -> None: + parsed = urlparse.urlparse(api_url) + if parsed.scheme not in {"http", "https"}: + msg = f"Unknown scheme {parsed.scheme} in {api_url}" + raise ValueError(msg) if global_request_kwargs is None: global_request_kwargs = {} self.api_url = api_url @@ -41,7 +48,7 @@ def endpoint(self, *path: str) -> str: return join(self.api_url, *path) @property - def _headers(self) -> Dict[str, str]: + def _headers(self) -> dict[str, str]: """Constructs the headers to send to the api for every request""" return { "Authorization": f"Bearer {self.token}", @@ -50,21 +57,15 @@ def _headers(self) -> Dict[str, str]: def prepare_headers( self, - headers: Optional[Dict[str, str]] = None, - ) -> Dict[str, str]: + headers: dict[str, str] | None = None, + ) -> dict[str, str]: """Prepares and verifies dictionary headers.""" if headers is None: - headers = {} - if isinstance(headers, dict): - headers.update(self._headers) - else: - raise ValueError( - f"headers must be dict or dict subclass, not type {type(headers)!r}" - ) - return headers + return dict(self._headers) + return {**self._headers, **headers} @staticmethod - def construct_params(params: Dict[str, Optional[str]]) -> str: + def construct_params(params: dict[str, str | None]) -> str: """ Custom method for constructing non-standard query strings. @@ -73,19 +74,19 @@ def construct_params(params: Dict[str, Optional[str]]) -> str: To have an empty value use an empty string :code:`""` (i.e. :code:`?key1=&key2=value2`). """ return "&".join( - [k if v is None else f"{k}={quote_plus(v)}" for k, v in params.items()] + [k if v is None else f"{k}={quote_plus(v)}" for k, v in params.items()], ) @staticmethod def prepare_get_entity_histories_params( - entities: Optional[Tuple[Entity, ...]] = None, - start_timestamp: Optional[datetime] = None, + entities: tuple[Entity | AsyncEntity, ...] | None = None, + start_timestamp: datetime | None = None, # Defaults to 1 day before. https://developers.home-assistant.io/docs/api/rest/ - end_timestamp: Optional[datetime] = None, + end_timestamp: datetime | None = None, significant_changes_only: bool = False, - ) -> Tuple[Dict[str, Optional[str]], str]: + ) -> tuple[dict[str, str | None], str]: """ - Pre-logic for :py:meth:`Client.get_entity_histories` and :py:meth:`Client.async_get_entity_histories`. + Pre-logic for :py:meth:`Client.get_entity_histories` and :py:meth:`AsyncClient.get_entity_histories`. Ensure timestamps @@ -93,7 +94,7 @@ def prepare_get_entity_histories_params( * are timezone-aware * are URL-encoded (as :py:meth:`construct_params` is used instead of request's default parameter encoding) """ - params: Dict[str, Optional[str]] = {} + params: dict[str, str | None] = {} if entities is not None: params["filter_entity_id"] = ",".join([ent.entity_id for ent in entities]) if start_timestamp is not None: @@ -114,14 +115,12 @@ def prepare_get_entity_histories_params( @staticmethod def prepare_get_logbook_entry_params( - filter_entities: Optional[Union[str, Iterable[str]]] = None, - start_timestamp: Optional[ - Union[str, datetime] - ] = None, # Defaults to 1 day before - end_timestamp: Optional[Union[str, datetime]] = None, - ) -> Tuple[Dict[str, str], str]: + filter_entities: str | Iterable[str] | None = None, + start_timestamp: str | datetime | None = None, # Defaults to 1 day before + end_timestamp: str | datetime | None = None, + ) -> tuple[dict[str, str], str]: """Prepares the query string and url path for retrieving logbook entries.""" - params: Dict[str, str] = {} + params: dict[str, str] = {} if filter_entities is not None: params.update( { @@ -129,8 +128,8 @@ def prepare_get_logbook_entry_params( filter_entities if isinstance(filter_entities, str) else ",".join(filter_entities) - ) - } + ), + }, ) if end_timestamp is not None: if isinstance(end_timestamp, datetime): diff --git a/homeassistant_api/basewebsocket.py b/homeassistant_api/basewebsocket.py new file mode 100644 index 00000000..732a67c6 --- /dev/null +++ b/homeassistant_api/basewebsocket.py @@ -0,0 +1,78 @@ +import logging +import time +import urllib.parse as urlparse +from typing import Any +from typing import cast + +from homeassistant_api.errors import ReceivingError +from homeassistant_api.errors import ResponseError +from homeassistant_api.models.websocket import ErrorResponse +from homeassistant_api.models.websocket import EventResponse +from homeassistant_api.models.websocket import PingResponse +from homeassistant_api.models.websocket import ResultResponse + +logger = logging.getLogger(__name__) + + +class BaseWebsocketClient: + """Shared methods for Websocket clients.""" + + api_url: str + token: str + max_size: int + _id_counter: int + _result_responses: dict[int, ResultResponse | None] + _event_responses: dict[int, list[EventResponse]] + _ping_responses: dict[int, PingResponse] + + def __init__(self, api_url: str, token: str, *, max_size: int = 2**24) -> None: + parsed = urlparse.urlparse(api_url) + if parsed.scheme not in {"ws", "wss", "http", "https"}: + msg = f"Unknown scheme {parsed.scheme!r} in {api_url!r}" + raise ValueError(msg) + _scheme_map = {"ws": "ws", "wss": "wss", "http": "ws", "https": "wss"} + self.api_url = urlparse.urlunparse( + parsed._replace(scheme=_scheme_map[parsed.scheme]), + ) + self.token = token.strip() + self.max_size = max_size + + self._id_counter = 0 + self._result_responses = {} # id -> response + self._event_responses = {} # id -> [response, ...] + self._ping_responses = {} # id -> (sent, received) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.api_url!r})" + + def _request_id(self) -> int: + """Get a unique id for a message.""" + self._id_counter += 1 + return self._id_counter + + def handle_recv(self, data: dict[str, Any]) -> None: + """Handle a received message.""" + if "id" not in data: + msg = "Received a message without an id outside the auth phase." + raise ReceivingError(msg) + self.parse_response(data) + + def parse_response(self, data: dict[str, Any]) -> None: + data_id = cast("int", data["id"]) + if data.get("type") == "pong": + logger.info("Received pong message") + self._ping_responses[data_id].end = time.perf_counter_ns() + elif data.get("type") == "result": + logger.info("Received result message") + if data.get("success"): + self._result_responses[data_id] = ResultResponse.model_validate(data) + else: + error_resp = ErrorResponse.model_validate(data) + msg = f"[{error_resp.error.code}] {error_resp.error.message}" + raise ResponseError(msg) + elif data.get("type") == "event": + logger.info("Received event message %s", data["event"]) + self._event_responses[data_id].append(EventResponse.model_validate(data)) + else: + msg = f"Received unexpected message type: {data}" + raise ReceivingError(msg) diff --git a/homeassistant_api/client.py b/homeassistant_api/client.py index c5ddd6eb..66cdd7d1 100644 --- a/homeassistant_api/client.py +++ b/homeassistant_api/client.py @@ -1,44 +1,409 @@ -"""Module containing the primary Client class.""" +"""Module for all interaction with Home Assistant.""" +from __future__ import annotations + +import json import logging -import urllib.parse as urlparse +from http import HTTPMethod +from posixpath import join +from typing import TYPE_CHECKING from typing import Any -from .rawasyncclient import RawAsyncClient -from .rawclient import RawClient +from niquests import Session +from niquests import Timeout +from typing_extensions import Self + +from homeassistant_api.baseclient import BaseClient +from homeassistant_api.errors import BadTemplateError +from homeassistant_api.errors import RequestError +from homeassistant_api.errors import RequestTimeoutError +from homeassistant_api.models import Domain +from homeassistant_api.models import Entity +from homeassistant_api.models import Event +from homeassistant_api.models import Group +from homeassistant_api.models import History +from homeassistant_api.models import LogbookEntry +from homeassistant_api.models import State +from homeassistant_api.processing import ResponseType +from homeassistant_api.processing import process_response +from homeassistant_api.utils import prepare_entity_id + +if TYPE_CHECKING: + from collections.abc import Generator + from datetime import datetime + from types import TracebackType logger = logging.getLogger(__name__) -class Client(RawClient, RawAsyncClient): +class Client(BaseClient): """ - The all-in-one class to interact with Home Assistant! + The sync client for interacting with Home Assistant via the REST API. :param api_url: The location of the api endpoint. e.g. :code:`http://localhost:8123/api` Required. :param token: The refresh or long lived access token to authenticate your requests. Required. - :param global_request_kwargs: A dictionary or dict-like object of kwargs to pass to :func:`requests.request` or :meth:`aiohttp.ClientSession.request`. Optional. - :param cache_session: A :py:class:`requests_cache.CachedSession` object to use for caching requests. Optional. - :param async_cache_session: A :py:class:`aiohttp_client_cache.CachedSession` object to use for caching requests. Optional. + :param session: A custom :py:class:`niquests.Session` instance. Optional. + :param verify_ssl: Whether to verify SSL certificates. Default :code:`True`. + :param global_request_kwargs: Kwargs to pass to :func:`requests.request`. Optional. """ # pylint: disable=line-too-long + _session: Session + def __init__( self, - api_url: str, - token: str, - use_async: bool = False, + *args: Any, + session: Session | None = None, verify_ssl: bool = True, **kwargs: Any, ) -> None: - parsed = urlparse.urlparse(api_url) + BaseClient.__init__(self, *args, **kwargs) + self.global_request_kwargs["verify"] = verify_ssl + if session is not None: + self._session = session + else: + self._session = Session() - if parsed.scheme in {"http", "https"}: - if use_async: - RawAsyncClient.__init__( - self, api_url, token, verify_ssl=verify_ssl, **kwargs - ) - else: - RawClient.__init__( - self, api_url, token, verify_ssl=verify_ssl, **kwargs + def __enter__(self) -> Self: + logger.debug("Entering cached requests session %r.", self._session) + self._session.__enter__() + self.check_api_running() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + logger.debug("Exiting requests session %r", self._session) + self._session.close() + + def request( + self, + path: str, + *, + params: dict[str, Any] | None = None, + method: HTTPMethod = HTTPMethod.GET, + headers: dict[str, str] | None = None, + **kwargs: Any, + ) -> Any: + """Base method for making requests to the api""" + path = self.endpoint(path) + if params: + path = f"{path}?{self.construct_params(params)}" + if self.global_request_kwargs is not None: + kwargs.update(self.global_request_kwargs) + try: + logger.debug(f"{method} request to {path}") + resp = self._session.request( + method, + path, + headers=self.prepare_headers(headers), + **kwargs, + ) + except Timeout as err: + msg = f"Home Assistant did not respond in time (timeout: {kwargs.get('timeout', 300)} sec)" + raise RequestTimeoutError(msg, url=path) from err + return self.response_logic(response=resp) + + def _dict_request(self, *args: Any, **kwargs: Any) -> dict[str, Any]: + data = self.request(*args, **kwargs) + if not isinstance(data, dict): + msg = f"Expected dict response, got {type(data).__name__}" + raise TypeError(msg) + return data + + def _list_request(self, *args: Any, **kwargs: Any) -> list: + data = self.request(*args, **kwargs) + if not isinstance(data, list): + msg = f"Expected list response, got {type(data).__name__}" + raise TypeError(msg) + return data + + def _str_request(self, *args: Any, **kwargs: Any) -> str: + data = self.request(*args, **kwargs) + if not isinstance(data, str): + msg = f"Expected str response, got {type(data).__name__}" + raise TypeError(msg) + return data + + @staticmethod + def response_logic(response: ResponseType) -> Any: + """Processes responses from the API and formats them""" + return process_response(response) + + # API information methods + def get_error_log(self) -> str: + """ + Returns the server error log as a string. + :code:`GET /api/error_log` + """ + return self._str_request("error_log") + + def get_config(self) -> dict[str, Any]: + """ + Returns the configuration of Home Assistant. + :code:`GET /api/config` + """ + return self._dict_request("config") + + def get_logbook_entries( + self, + *args: Any, + **kwargs: Any, + ) -> Generator[LogbookEntry, None, None]: + """ + Returns a list of logbook entries from Home Assistant. + :code:`GET /api/logbook/` + """ + params, url = self.prepare_get_logbook_entry_params(*args, **kwargs) + data = self._list_request(url, params=params) + for entry in data: + yield LogbookEntry.model_validate(entry) + + def get_entity_histories( + self, + entities: tuple[Entity, ...] | None = None, + start_timestamp: datetime | None = None, + # Defaults to 1 day before. https://developers.home-assistant.io/docs/api/rest/ + end_timestamp: datetime | None = None, + *, + significant_changes_only: bool = False, + ) -> Generator[History, None, None]: + """ + Yields entity state histories. See docs on the :py:class:`History` model. + :code:`GET /api/history/period/` + """ + params, url = self.prepare_get_entity_histories_params( + entities=entities, + start_timestamp=start_timestamp, + end_timestamp=end_timestamp, + significant_changes_only=significant_changes_only, + ) + data = self._list_request(url, params=params) + for states in data: + yield History.model_validate({"states": states}) + + def get_rendered_template(self, template: str) -> str: + """ + Renders a Jinja2 template with Home Assistant context data. + See https://www.home-assistant.io/docs/configuration/templating. + :code:`POST /api/template` + """ + try: + return self._str_request( + "template", + json={"template": template}, + method=HTTPMethod.POST, + ) + except RequestError as err: + msg = ( + "Your template is invalid. " + "Try debugging it in the developer tools page of Home Assistant." + ) + raise BadTemplateError(msg) from err + + # API check methods + def check_api_config(self) -> bool: + """ + Asks Home Assistant to validate its configuration file. + :code:`POST /api/config/core/check_config` + """ + res = self._dict_request("config/core/check_config", method=HTTPMethod.POST) + return {"valid": True, "invalid": False}.get(res["result"], False) + + def check_api_running(self) -> bool: + """ + Asks Home Assistant if it is running. + :code:`GET /api/` + """ + res = self._dict_request("") + return res.get("message") == "API running." + + # Entity methods + def get_entities(self) -> dict[str, Group]: + """ + Fetches all entities from the api and returns them as a dictionary of :py:class:`Group`'s. + :code:`GET /api/states` + """ + entities: dict[str, Group] = {} + for state in self.get_states(): + group_id, entity_slug = state.entity_id.split(".") + if group_id not in entities: + entities[group_id] = Group( + group_id=group_id, + client=self, ) + entities[group_id]._add_entity(entity_slug, state) # noqa: SLF001 + return entities + + def get_entity( + self, + group_id: str | None = None, + slug: str | None = None, + entity_id: str | None = None, + ) -> Entity | None: + """ + Returns an :py:class:`Entity` model for an :code:`entity_id`. + :code:`GET /api/states/` + """ + if group_id is not None and slug is not None: + state = self.get_state(group_id=group_id, slug=slug) + elif entity_id is not None: + state = self.get_state(entity_id=entity_id) else: - raise ValueError(f"Unknown scheme {parsed.scheme} in {api_url}") + help_msg = ( + "Use keyword arguments to pass entity_id. " + "Or you can pass the group_id and slug instead" + ) + msg = f"Neither group_id and slug or entity_id provided. {help_msg}" + raise ValueError(msg) + split_group_id, split_slug = state.entity_id.split(".") + group = Group( + group_id=split_group_id, + client=self, + ) + group._add_entity(split_slug, state) # noqa: SLF001 + return group.get_entity(split_slug) + + # Services and domain methods + def get_domains(self) -> dict[str, Domain]: + """ + Fetches all service :py:class:`Domain`'s from the API. + :code:`GET /api/services` + """ + data = self._list_request("services") + domains = (Domain.from_json_with_client(json, client=self) for json in data) + return {domain.domain_id: domain for domain in domains} + + def get_domain(self, domain_id: str) -> Domain | None: + """ + Fetches all :py:class:`Service`'s under a particular service :py:class:`Domain`. + Uses cached data from :py:meth:`get_domains` if available. + """ + return self.get_domains().get(domain_id) + + def trigger_service( + self, + domain: str, + service: str, + **service_data: Any, + ) -> tuple[State, ...]: + """ + Tells Home Assistant to trigger a service, returns all states changed while in the process of being called. + :code:`POST /api/services//` + """ + data = self._list_request( + join("services", domain, service), + method=HTTPMethod.POST, + json=service_data, + ) + return tuple(map(State.from_json, data)) + + def trigger_service_with_response( + self, + domain: str, + service: str, + **service_data: Any, + ) -> tuple[tuple[State, ...], dict[str, Any]]: + """ + Tells Home Assistant to trigger a service, returns the response from the service call. + :code:`POST /api/services//` + + Returns a list of the states changed and the response from the service call. + """ + data = self._dict_request( + join("services", domain, service) + "?return_response", + method=HTTPMethod.POST, + json=service_data, + ) + states = tuple( + map( + State.from_json, + data.get("changed_states", []), + ), + ) + return states, data.get("service_response", {}) + + # EntityState methods + def get_state( # pylint: disable=duplicate-code + self, + *, + entity_id: str | None = None, + group_id: str | None = None, + slug: str | None = None, + ) -> State: + """ + Fetches the state of the entity specified. + :code:`GET /api/states/` + """ + entity_id = prepare_entity_id( + group_id=group_id, + slug=slug, + entity_id=entity_id, + ) + data = self._dict_request(join("states", entity_id)) + return State.from_json(data) + + def set_state( # pylint: disable=duplicate-code + self, + state: State, + ) -> State: + """ + This method sets the representation of a device within Home Assistant and will not communicate with the actual device. + To communicate with the device, use :py:meth:`Service.trigger`. + :code:`POST /api/states/` + """ + data = self._dict_request( + join("states", state.entity_id), + method=HTTPMethod.POST, + json=json.loads(state.model_dump_json()), + ) + return State.from_json(data) + + def get_states(self) -> tuple[State, ...]: + """ + Gets the states of all entities within Home Assistant. + :code:`GET /api/states` + """ + data = self._list_request("states") + states = map(State.from_json, data) + return tuple(states) + + # Event methods + def get_events(self) -> tuple[Event, ...]: + """ + Gets the Events that happen within Home Assistant. + :code:`GET /api/events` + """ + data = self._list_request("events") + return tuple(Event.from_json_with_client(json, client=self) for json in data) + + def get_event(self, name: str) -> Event | None: + """ + Gets the :py:class:`Event` with the specified name if it has at least one listener. + Uses cached data from :py:meth:`get_events` if available. + """ + for event in self.get_events(): + if event.event == name.strip().lower(): + return event + return None + + def fire_event(self, event_type: str, **event_data: Any) -> str: + """ + Fires a given event_type within Home Assistant. + `POST /api/events/` + """ + data = self._dict_request( + join("events", event_type), + method=HTTPMethod.POST, + json=event_data, + ) + return data.get("message", "No message provided") + + def get_components(self) -> tuple[str, ...]: + """ + Returns a tuple of all registered components. + :code:`GET /api/components` + """ + return tuple(self._list_request("components")) diff --git a/homeassistant_api/errors.py b/homeassistant_api/errors.py index 01136260..4e84c5fd 100644 --- a/homeassistant_api/errors.py +++ b/homeassistant_api/errors.py @@ -1,7 +1,5 @@ """Module for custom error classes""" -from typing import Optional, Union - class HomeassistantAPIError(Exception): """Base class for custom errors""" @@ -11,19 +9,23 @@ class RequestError(HomeassistantAPIError): """Error raised when an issue occurs when requesting to Homeassistant.""" def __init__( - self, data: Optional[str], /, url: str, message: Optional[str] = None + self, + data: str | None, + /, + url: str, + message: str | None = None, ) -> None: if message is not None: super().__init__( message + f" {url!r}" - + (f" with data: {data!r}" if data is not None else "") + + (f" with data: {data!r}" if data is not None else ""), ) elif data is None: super().__init__(f"An error occurred while making the request to {url!r}") else: super().__init__( - f"An error occurred while making the request to {url!r} with data: {data!r}" + f"An error occurred while making the request to {url!r} with data: {data!r}", ) @@ -61,20 +63,20 @@ class ParameterMissingError(HomeassistantAPIError): class InternalServerError(HomeassistantAPIError): """Error raised when Home Assistant says that it got itself in trouble.""" - def __init__(self, status_code: int, content: Union[str, bytes]) -> None: + def __init__(self, status_code: int, content: str | bytes) -> None: super().__init__( f"Home Assistant returned a response with an error status code {status_code!r}.\n" f"{content!r}\n" "If this happened, " "please report it at https://github.com/GrandMoff100/HomeAssistantAPI/issues " - "with the request status code and the request content. Thanks!" + "with the request status code and the request content. Thanks!", ) class UnauthorizedError(HomeassistantAPIError): """Error raised when an invalid token in used to authenticate with homeassistant.""" - def __init__(self, message: Optional[str] = None) -> None: + def __init__(self, message: str | None = None) -> None: super().__init__(message or "Invalid authentication token") diff --git a/homeassistant_api/models/__init__.py b/homeassistant_api/models/__init__.py index 6e66038d..8c0685a1 100644 --- a/homeassistant_api/models/__init__.py +++ b/homeassistant_api/models/__init__.py @@ -1,52 +1,83 @@ """The Model objects for the entire library.""" from .base import BaseModel -from .config_entries import ( - ConfigEntry, - ConfigEntryChange, - ConfigEntryDisabler, - ConfigEntryEvent, - ConfigEntryState, - ConfigFlowContext, - ConfigSubEntry, - DisableEnableResult, - DiscoveryKey, - FlowContext, - FlowResult, - FlowResultType, - IntegrationTypes, -) -from .domains import Domain, Service, ServiceField -from .entity import Entity, Group +from .config_entries import ConfigEntry +from .config_entries import ConfigEntryChange +from .config_entries import ConfigEntryDisabler +from .config_entries import ConfigEntryEvent +from .config_entries import ConfigEntryState +from .config_entries import ConfigFlowContext +from .config_entries import ConfigSubEntry +from .config_entries import DisableEnableResult +from .config_entries import DiscoveryKey +from .config_entries import FlowContext +from .config_entries import FlowResult +from .config_entries import FlowResultType +from .config_entries import IntegrationTypes +from .domains import AsyncDomain +from .domains import AsyncService +from .domains import BaseDomain +from .domains import BaseService +from .domains import Domain +from .domains import Service +from .domains import ServiceField +from .entity import AsyncEntity +from .entity import AsyncGroup +from .entity import BaseEntity +from .entity import BaseGroup +from .entity import Entity +from .entity import Group +from .entity_registry import EntityCategory +from .entity_registry import EntityDisabledBy +from .entity_registry import EntityHiddenBy +from .entity_registry import EntityRegistryEntry +from .entity_registry import EntityRegistryEntryExtended +from .entity_registry import EntityRegistryUpdateResult +from .events import AsyncEvent +from .events import BaseEvent from .events import Event from .history import History from .logbook import LogbookEntry from .states import State __all__ = ( - "Domain", - "Service", + "AsyncDomain", + "AsyncEntity", + "AsyncEvent", + "AsyncGroup", + "AsyncService", + "BaseDomain", + "BaseEntity", + "BaseEvent", + "BaseGroup", "BaseModel", + "BaseService", + "ConfigEntry", + "ConfigEntryChange", + "ConfigEntryDisabler", + "ConfigEntryEvent", + "ConfigEntryState", + "ConfigFlowContext", + "ConfigSubEntry", + "DisableEnableResult", + "DiscoveryKey", "Domain", - "Service", - "ServiceField", "Entity", - "Group", + "EntityCategory", + "EntityDisabledBy", + "EntityHiddenBy", + "EntityRegistryEntry", + "EntityRegistryEntryExtended", + "EntityRegistryUpdateResult", "Event", - "History", - "LogbookEntry", - "State", - "DisableEnableResult", - "DiscoveryKey", "FlowContext", - "ConfigFlowContext", "FlowResult", "FlowResultType", + "Group", + "History", "IntegrationTypes", - "ConfigEntryDisabler", - "ConfigEntryState", - "ConfigEntry", - "ConfigSubEntry", - "ConfigEntryChange", - "ConfigEntryEvent", + "LogbookEntry", + "Service", + "ServiceField", + "State", ) diff --git a/homeassistant_api/models/base.py b/homeassistant_api/models/base.py index 6ef6818c..0b4c0eef 100644 --- a/homeassistant_api/models/base.py +++ b/homeassistant_api/models/base.py @@ -1,14 +1,14 @@ """Module for Global Base Model Configuration inheritance.""" from datetime import datetime -from typing import Annotated, Any, Union +from typing import Annotated +from typing import Any from pydantic import BaseModel as PydanticBaseModel -from pydantic import ConfigDict, PlainSerializer +from pydantic import ConfigDict +from pydantic import PlainSerializer from typing_extensions import Self -from homeassistant_api.utils import JSONType - __all__ = ( "BaseModel", "DatetimeIsoField", @@ -27,10 +27,12 @@ class BaseModel(PydanticBaseModel): arbitrary_types_allowed=True, validate_assignment=True, protected_namespaces=(), + populate_by_name=True, + serialize_by_alias=True, ) # TODO: Any being accepted is not ideal. Narrow it down. @classmethod - def from_json(cls, json: Union[dict[str, JSONType], Any, None]) -> Self: + def from_json(cls, json: dict[str, Any] | Any | None) -> Self: """Constructs Self model from json data""" return cls.model_validate(json) diff --git a/homeassistant_api/models/config_entries.py b/homeassistant_api/models/config_entries.py index 48b4fc6a..7e9c490a 100644 --- a/homeassistant_api/models/config_entries.py +++ b/homeassistant_api/models/config_entries.py @@ -1,13 +1,12 @@ """File for models used in responses from config entries.""" -import asyncio -from enum import Enum -from typing import Any, Container, Dict, Optional, Tuple, Union +from enum import StrEnum +from typing import Any from .base import BaseModel -class FlowResultType(Enum): +class FlowResultType(StrEnum): """Result type for a data entry flow.""" FORM = "form" @@ -24,54 +23,54 @@ class DiscoveryKey(BaseModel): """Serializable discovery key.""" domain: str - key: Union[str, Tuple[str, ...]] + key: str | tuple[str, ...] version: int class FlowContext(BaseModel): """Base flow context""" - show_advanced_options: Union[bool, None] = None + show_advanced_options: bool | None = None source: str class ConfigFlowContext(FlowContext): """Context for config flow.""" - alternative_domain: Optional[str] = None - configuration_url: Optional[str] = None - confirm_only: Optional[bool] = None + alternative_domain: str | None = None + configuration_url: str | None = None + confirm_only: bool | None = None discovery_key: DiscoveryKey - entry_id: Optional[str] = None - title_placeholders: Optional[Dict[str, str]] = None - unique_id: Optional[str] = None + entry_id: str | None = None + title_placeholders: dict[str, str] | None = None + unique_id: str | None = None class FlowResult(BaseModel): """Base flow result .""" context: ConfigFlowContext - data_schema: Optional[Any] = None - data: Optional[Dict[str, Any]] = None - description_placeholders: Optional[Dict[str, str]] = None - description: Optional[str] = None - errors: Optional[Dict[str, str]] = None - extra: Optional[str] = None + data_schema: Any | None = None + data: dict[str, Any] | None = None + description_placeholders: dict[str, str] | None = None + description: str | None = None + errors: dict[str, str] | None = None + extra: str | None = None flow_id: str handler: str - last_step: Optional[bool] = None - menu_options: Optional[Container[str]] = None - preview: Optional[str] = None - progress_action: Optional[str] = None - progress_task: Optional[asyncio.Task[Any]] = None - reason: Optional[str] = None - required: Optional[bool] = None - result: Optional[Any] = None - step_id: Optional[str] = None - title: Optional[str] = None - translation_domain: Optional[str] = None - type: Optional[FlowResultType] = None - url: Optional[str] = None + last_step: bool | None = None + menu_options: list[str] | None = None + preview: str | None = None + progress_action: str | None = None + progress_task: str | None = None + reason: str | None = None + required: bool | None = None + result: Any | None = None + step_id: str | None = None + title: str | None = None + translation_domain: str | None = None + type: FlowResultType | None = None + url: str | None = None class DisableEnableResult(BaseModel): @@ -80,7 +79,7 @@ class DisableEnableResult(BaseModel): require_restart: bool -class IntegrationTypes(Enum): +class IntegrationTypes(StrEnum): """Types of integrations.""" ENTITY = "entity" @@ -93,7 +92,7 @@ class IntegrationTypes(Enum): VIRTUAL = "virtual" -class ConfigEntryState(str, Enum): +class ConfigEntryState(StrEnum): """Config entry state.""" LOADED = "loaded" @@ -106,7 +105,7 @@ class ConfigEntryState(str, Enum): UNLOAD_IN_PROGRESS = "unload_in_progress" -class ConfigEntryDisabler(Enum): +class ConfigEntryDisabler(StrEnum): """What disabled a config entry.""" USER = "user" @@ -126,13 +125,13 @@ class ConfigEntry(BaseModel): supports_remove_device: bool supports_unload: bool supports_reconfigure: bool - supported_subentry_types: Dict[str, Dict[str, bool]] + supported_subentry_types: dict[str, dict[str, bool]] pref_disable_new_entities: bool pref_disable_polling: bool - disabled_by: Optional[ConfigEntryDisabler] - reason: Optional[str] - error_reason_translation_key: Optional[str] - error_reason_translation_placeholders: Optional[Dict[str, Any]] + disabled_by: ConfigEntryDisabler | None + reason: str | None + error_reason_translation_key: str | None + error_reason_translation_placeholders: dict[str, Any] | None num_subentries: int @@ -142,10 +141,10 @@ class ConfigSubEntry(BaseModel): subentry_id: str subentry_type: str title: str - unique_id: Optional[str] + unique_id: str | None -class ConfigEntryChange(str, Enum): +class ConfigEntryChange(StrEnum): """What was changed in a config entry.""" ADDED = "added" @@ -154,5 +153,5 @@ class ConfigEntryChange(str, Enum): class ConfigEntryEvent(BaseModel): - type: Optional[ConfigEntryChange] + type: ConfigEntryChange | None entry: ConfigEntry diff --git a/homeassistant_api/models/domains.py b/homeassistant_api/models/domains.py index 467defe0..589ac488 100644 --- a/homeassistant_api/models/domains.py +++ b/homeassistant_api/models/domains.py @@ -2,98 +2,72 @@ from __future__ import annotations -import gc -import inspect -from enum import Enum -from typing import ( - TYPE_CHECKING, - Any, - Coroutine, - Dict, - List, - Optional, - Tuple, - Union, - cast, -) +from enum import StrEnum +from typing import TYPE_CHECKING +from typing import Any +from typing import cast from pydantic import Field -from typing_extensions import Self, override +from typing_extensions import Self +from typing_extensions import override from homeassistant_api.errors import RequestError -from homeassistant_api.utils import JSONType +from homeassistant_api.errors import ResponseError from .base import BaseModel -from .states import State if TYPE_CHECKING: - from homeassistant_api import Client, WebsocketClient + from homeassistant_api import AsyncClient + from homeassistant_api import AsyncWebsocketClient + from homeassistant_api import Client + from homeassistant_api import WebsocketClient + from .states import State -class Domain(BaseModel): + +class BaseDomain(BaseModel): """Model representing the domain that services belong to.""" - def __init__( - self, - *args, - _client: Optional[Union["Client", "WebsocketClient"]] = None, - **kwargs, - ) -> None: - super().__init__(*args, **kwargs) - if _client is None: - raise ValueError("No client passed.") - object.__setattr__(self, "_client", _client) - - _client: Union["Client", "WebsocketClient"] domain_id: str = Field( ..., description="The name of the domain that services belong to. " "(e.g. :code:`frontend` in :code:`frontend.reload_themes`", ) - services: Dict[str, "Service"] = Field( - {}, + services: dict[str, BaseService] = Field( + default_factory=dict, description="A dictionary of all services belonging to the domain indexed by their names", ) @classmethod @override - def from_json(cls, json: Union[dict[str, JSONType], Any, None], **kwargs) -> Self: - raise ValueError( - f"`{cls.__name__}` does not support `from_json()`. Use `from_json_with_client()`" - ) + def from_json(cls, json: dict[str, Any] | Any | None, **kwargs: Any) -> Self: + msg = f"`{cls.__name__}` does not support `from_json()`. Use `from_json_with_client()`" + raise NotImplementedError(msg) @classmethod - def from_json_with_client( - cls, json: Dict[str, JSONType], client: Union["Client", "WebsocketClient"] - ) -> "Domain": - """Constructs Domain and Service models from json data.""" + def _build_from_json(cls, json: dict[str, Any], **model_kwargs: Any) -> Self: + """Shared construction logic for Domain models from json data.""" if "domain" not in json or "services" not in json: - raise ValueError("Missing services or domain attribute in json argument.") - domain = cls(domain_id=cast(str, json.get("domain")), _client=client) - services = cast(dict[str, dict[str, JSONType]], json.get("services")) - assert isinstance(services, dict) + msg = "Missing services or domain attribute in json argument." + raise ValueError(msg) + domain = cls(domain_id=cast("str", json.get("domain")), **model_kwargs) + services = json.get("services") + if not isinstance(services, dict): + msg = f"Expected dict for services, got {type(services)}" + raise TypeError(msg) for service_id, data in services.items(): domain._add_service(service_id, **data) return domain - def _add_service(self, service_id: str, **data) -> None: + def _add_service(self, service_id: str, **data: Any) -> None: """Registers services into a domain to be used or accessed. Used internally.""" - # raise ValueError(data) - self.services.update( - { - service_id: Service( - service_id=service_id, - domain=self, - **data, - ) - } - ) + raise NotImplementedError - def get_service(self, service_id: str) -> Optional["Service"]: + def get_service(self, service_id: str) -> BaseService | None: """Return a Service with the given service_id, returns None if no such service exists""" return self.services.get(service_id) - def __getattr__(self, attr: str): + def __getattr__(self, attr: str) -> Any: """Allows services accessible as attributes""" if attr in self.services: return self.get_service(attr) @@ -106,6 +80,50 @@ def __getattr__(self, attr: str): raise e from err +class Domain(BaseDomain): + """Sync domain that creates sync Service instances.""" + + client: Client | WebsocketClient = Field(exclude=True, repr=False) + + @classmethod + def from_json_with_client( + cls, + json: dict[str, Any], + client: Client | WebsocketClient, + ) -> Domain: + """Constructs Domain and Service models from json data.""" + return cls._build_from_json(json, client=client) + + def _add_service(self, service_id: str, **data: Any) -> None: + self.services[service_id] = Service( + service_id=service_id, + domain=self, + **data, + ) + + +class AsyncDomain(BaseDomain): + """Async domain that creates async Service instances.""" + + client: AsyncClient | AsyncWebsocketClient = Field(exclude=True, repr=False) + + @classmethod + def from_json_with_client( + cls, + json: dict[str, Any], + client: AsyncClient | AsyncWebsocketClient, + ) -> AsyncDomain: + """Constructs Domain and Service models from json data.""" + return cls._build_from_json(json, client=client) + + def _add_service(self, service_id: str, **data: Any) -> None: + self.services[service_id] = AsyncService( + service_id=service_id, + domain=self, + **data, + ) + + # Sources: # https://developers.home-assistant.io/docs/dev_101_services/ # https://www.home-assistant.io/docs/blueprint/selectors/#date-selector @@ -115,58 +133,58 @@ def __getattr__(self, attr: str): # Helpers class ServiceFieldSelectorEntityFilter(BaseModel): - integration: Optional[str] = None - domain: Optional[Union[List[str], str]] = None - device_class: Optional[Union[List[str], str]] = None - supported_features: Optional[Union[List[int], int]] = None + integration: str | None = None + domain: list[str] | str | None = None + device_class: list[str] | str | None = None + supported_features: list[int] | int | None = None class ServiceFieldSelectorDeviceFilter(BaseModel): - integration: Optional[str] = None - manufacturer: Optional[str] = None - model: Optional[str] = None - model_id: Optional[str] = None + integration: str | None = None + manufacturer: str | None = None + model: str | None = None + model_id: str | None = None class CropOptions(BaseModel): round: bool - type: Optional[str] = None # "image/jpeg" / "image/png" - quality: Optional[Union[int, float]] = None - aspectRatio: Optional[Union[int, float]] = None + type: str | None = None # "image/jpeg" / "image/png" + quality: int | float | None = None + aspect_ratio: int | float | None = Field(default=None, alias="aspectRatio") class SelectBoxOptionImage(BaseModel): src: str - src_dark: Optional[str] = None - flip_rtl: Optional[bool] = None + src_dark: str | None = None + flip_rtl: bool | None = None -class ServiceFieldSelectorNumberMode(str, Enum): +class ServiceFieldSelectorNumberMode(StrEnum): BOX = "box" SLIDER = "slider" -class ServiceFieldSelectorSelectMode(str, Enum): +class ServiceFieldSelectorSelectMode(StrEnum): LIST = "list" DROPDOWN = "dropdown" BOX = "box" -class ServiceFieldSelectorQRCodeErrorCorrectionLevel(str, Enum): +class ServiceFieldSelectorQRCodeErrorCorrectionLevel(StrEnum): LOW = "low" MEDIUM = "medium" QUARTILE = "quartile" HIGH = "high" -class ServiceFieldSelectorTextType(str, Enum): +class ServiceFieldSelectorTextType(StrEnum): NUMBER = "number" TEXT = "text" SEARCH = "search" TEL = "tel" URL = "url" EMAIL = "email" - PASSWORD = "password" + PASSWORD = "password" # noqa: S105 DATE = "date" MONTH = "month" WEEK = "week" @@ -177,22 +195,22 @@ class ServiceFieldSelectorTextType(str, Enum): # Selectors class ServiceFieldSelectorAction(BaseModel): - optionsInSidebar: Optional[bool] = None + options_in_sidebar: bool | None = Field(default=None, alias="optionsInSidebar") class ServiceFieldSelectorAddon(BaseModel): - name: Optional[str] = None - slug: Optional[str] = None + name: str | None = None + slug: str | None = None class ServiceFieldSelectorArea(BaseModel): - entity: Optional[ - Union[List[ServiceFieldSelectorEntityFilter], ServiceFieldSelectorEntityFilter] - ] = None - device: Optional[ - Union[List[ServiceFieldSelectorDeviceFilter], ServiceFieldSelectorDeviceFilter] - ] = None - multiple: Optional[bool] = None + entity: ( + list[ServiceFieldSelectorEntityFilter] | ServiceFieldSelectorEntityFilter | None + ) = None + device: ( + list[ServiceFieldSelectorDeviceFilter] | ServiceFieldSelectorDeviceFilter | None + ) = None + multiple: bool | None = None class ServiceFieldSelectorAreasDisplay(BaseModel): @@ -200,17 +218,17 @@ class ServiceFieldSelectorAreasDisplay(BaseModel): class ServiceFieldSelectorAttribute(BaseModel): - entity_id: Optional[Union[List[str], str]] = None - hide_attributes: Optional[List[str]] = None + entity_id: list[str] | str | None = None + hide_attributes: list[str] | None = None class ServiceFieldSelectorAssistPipeline(BaseModel): - include_last_used: Optional[bool] = None + include_last_used: bool | None = None class ServiceFieldSelectorBackground(BaseModel): - original: Optional[bool] = None - crop: Optional[CropOptions] = None + original: bool | None = None + crop: CropOptions | None = None class ServiceFieldSelectorBackupLocation(BaseModel): @@ -222,9 +240,9 @@ class ServiceFieldSelectorBoolean(BaseModel): class ServiceFieldSelectorButtonToggle(BaseModel): - options: List[Union[str, ServiceFieldSelectorSelectOption]] - translation_key: Optional[str] = None - sort: Optional[bool] = None + options: list[str | ServiceFieldSelectorSelectOption] + translation_key: str | None = None + sort: bool | None = None class ServiceFieldSelectorColorRGB(BaseModel): @@ -232,34 +250,34 @@ class ServiceFieldSelectorColorRGB(BaseModel): class ServiceFieldSelectorColorTemp(BaseModel): - unit: Optional[str] = None - min: Optional[Union[int, float]] = None - max: Optional[Union[int, float]] = None - min_mireds: Optional[Union[int, float]] = None - max_mireds: Optional[Union[int, float]] = None + unit: str | None = None + min: int | float | None = None + max: int | float | None = None + min_mireds: int | float | None = None + max_mireds: int | float | None = None class ServiceFieldSelectorCondition(BaseModel): - optionsInSidebar: Optional[bool] = None + options_in_sidebar: bool | None = Field(default=None, alias="optionsInSidebar") class ServiceFieldSelectorConfigEntry(BaseModel): - integration: Optional[str] = None + integration: str | None = None class ServiceFieldSelectorConstant(BaseModel): - label: Optional[str] = None - value: Union[str, int, float, bool] - translation_key: Optional[str] = None + label: str | None = None + value: str | int | float | bool + translation_key: str | None = None class ServiceFieldSelectorConversationAgent(BaseModel): - language: Optional[str] = None # filtering by language not supported + language: str | None = None # filtering by language not supported class ServiceFieldSelectorCountry(BaseModel): - countries: List[str] - no_sort: Optional[bool] = None + countries: list[str] + no_sort: bool | None = None class ServiceFieldSelectorDate(BaseModel): @@ -271,50 +289,50 @@ class ServiceFieldSelectorDateTime(BaseModel): class ServiceFieldSelectorDevice(BaseModel): - entity: Optional[ - Union[List[ServiceFieldSelectorEntityFilter], ServiceFieldSelectorEntityFilter] - ] = None - filter: Optional[ - Union[List[ServiceFieldSelectorDeviceFilter], ServiceFieldSelectorDeviceFilter] - ] = None - multiple: Optional[bool] = None + entity: ( + list[ServiceFieldSelectorEntityFilter] | ServiceFieldSelectorEntityFilter | None + ) = None + filter: ( + list[ServiceFieldSelectorDeviceFilter] | ServiceFieldSelectorDeviceFilter | None + ) = None + multiple: bool | None = None class ServiceFieldSelectorDeviceLegacy(ServiceFieldSelectorDevice): - integration: Optional[str] = None - manufacturer: Optional[str] = None - model: Optional[str] = None + integration: str | None = None + manufacturer: str | None = None + model: str | None = None class ServiceFieldSelectorDuration(BaseModel): - enable_day: Optional[bool] = None - enable_millisecond: Optional[bool] = None + enable_day: bool | None = None + enable_millisecond: bool | None = None class ServiceFieldSelectorEntity(BaseModel): - multiple: Optional[bool] = None - include_entities: Optional[List[str]] = None - exclude_entities: Optional[List[str]] = None - filter: Optional[ - Union[List[ServiceFieldSelectorEntityFilter], ServiceFieldSelectorEntityFilter] - ] = None - reorder: Optional[bool] = None + multiple: bool | None = None + include_entities: list[str] | None = None + exclude_entities: list[str] | None = None + filter: ( + list[ServiceFieldSelectorEntityFilter] | ServiceFieldSelectorEntityFilter | None + ) = None + reorder: bool | None = None class ServiceFieldSelectorEntityLegacy(ServiceFieldSelectorEntity): - integration: Optional[str] = None - domain: Optional[Union[List[str], str]] = None - device_class: Optional[Union[List[str], str]] = None + integration: str | None = None + domain: list[str] | str | None = None + device_class: list[str] | str | None = None class ServiceFieldSelectorFloor(BaseModel): - entity: Optional[ - Union[List[ServiceFieldSelectorEntityFilter], ServiceFieldSelectorEntityFilter] - ] = None - device: Optional[ - Union[List[ServiceFieldSelectorDeviceFilter], ServiceFieldSelectorDeviceFilter] - ] = None - multiple: Optional[bool] = None + entity: ( + list[ServiceFieldSelectorEntityFilter] | ServiceFieldSelectorEntityFilter | None + ) = None + device: ( + list[ServiceFieldSelectorDeviceFilter] | ServiceFieldSelectorDeviceFilter | None + ) = None + multiple: bool | None = None class ServiceFieldSelectorFile(BaseModel): @@ -322,33 +340,33 @@ class ServiceFieldSelectorFile(BaseModel): class ServiceFieldSelectorIcon(BaseModel): - placeholder: Optional[str] = None - fallbackPath: Optional[str] = None + placeholder: str | None = None + fallback_path: str | None = Field(default=None, alias="fallbackPath") class ServiceFieldSelectorImage(BaseModel): - original: Optional[bool] = None - crop: Optional[CropOptions] = None + original: bool | None = None + crop: CropOptions | None = None class ServiceFieldSelectorLabel(BaseModel): - multiple: Optional[bool] = None + multiple: bool | None = None class ServiceFieldSelectorLanguage(BaseModel): - languages: Optional[List[str]] = None - native_name: Optional[bool] = None - no_sort: Optional[bool] = None + languages: list[str] | None = None + native_name: bool | None = None + no_sort: bool | None = None class ServiceFieldSelectorLocation(BaseModel): - radius: Optional[bool] = None - radius_readonly: Optional[bool] = None - icon: Optional[str] = None + radius: bool | None = None + radius_readonly: bool | None = None + icon: str | None = None class ServiceFieldSelectorMedia(BaseModel): - accept: Optional[List[str]] = None + accept: list[str] | None = None multiple: bool = False @@ -357,55 +375,53 @@ class ServiceFieldSelectorNavigation(BaseModel): class ServiceFieldSelectorNumber(BaseModel): - min: Optional[Union[int, float]] = None - max: Optional[Union[int, float]] = None - step: Optional[Union[Union[int, float], str]] = None - unit_of_measurement: Optional[str] = None - mode: Optional[ServiceFieldSelectorNumberMode] = None - slider_ticks: Optional[bool] = None - translation_key: Optional[str] = None + min: int | float | None = None + max: int | float | None = None + step: int | float | str | None = None + unit_of_measurement: str | None = None + mode: ServiceFieldSelectorNumberMode | None = None + slider_ticks: bool | None = None + translation_key: str | None = None class ServiceFieldSelectorObjectField(BaseModel): selector: ServiceFieldSelector - label: Optional[str] = None - required: Optional[bool] = None + label: str | None = None + required: bool | None = None class ServiceFieldSelectorObject(BaseModel): - label_field: Optional[str] = None - description_field: Optional[str] = None - translation_key: Optional[str] = None - fields: Optional[Dict[str, ServiceFieldSelectorObjectField]] = None - multiple: Optional[bool] = None + label_field: str | None = None + description_field: str | None = None + translation_key: str | None = None + fields: dict[str, ServiceFieldSelectorObjectField] | None = None + multiple: bool | None = None class ServiceFieldSelectorQRCode(BaseModel): data: str - scale: Optional[Union[int, float]] = None - error_correction_level: Optional[ServiceFieldSelectorQRCodeErrorCorrectionLevel] = ( - None - ) - center_image: Optional[str] = None + scale: int | float | None = None + error_correction_level: ServiceFieldSelectorQRCodeErrorCorrectionLevel | None = None + center_image: str | None = None class ServiceFieldSelectorSelectOption(BaseModel): label: str value: Any - description: Optional[str] = None - image: Optional[Union[str, SelectBoxOptionImage]] = None - disable: Optional[bool] = None + description: str | None = None + image: str | SelectBoxOptionImage | None = None + disable: bool | None = None class ServiceFieldSelectorSelect(BaseModel): - multiple: Optional[bool] = None - custom_value: Optional[bool] = None - mode: Optional[ServiceFieldSelectorSelectMode] = None - options: List[Union[str, ServiceFieldSelectorSelectOption]] - translation_key: Optional[str] = None - sort: Optional[bool] = None - reorder: Optional[bool] = None - box_max_columns: Optional[int] = None + multiple: bool | None = None + custom_value: bool | None = None + mode: ServiceFieldSelectorSelectMode | None = None + options: list[str | ServiceFieldSelectorSelectOption] + translation_key: str | None = None + sort: bool | None = None + reorder: bool | None = None + box_max_columns: int | None = None class ServiceFieldSelectorSelector(BaseModel): @@ -418,25 +434,25 @@ class ServiceFieldSelectorStateOption(BaseModel): class ServiceFieldSelectorState(BaseModel): - extra_options: Optional[List[ServiceFieldSelectorStateOption]] = None - entity_id: Optional[Union[str, List[str]]] = None - attribute: Optional[str] = None - hide_states: Optional[List[str]] = None - multiple: Optional[bool] = None + extra_options: list[ServiceFieldSelectorStateOption] | None = None + entity_id: str | list[str] | None = None + attribute: str | None = None + hide_states: list[str] | None = None + multiple: bool | None = None class ServiceFieldSelectorStatistic(BaseModel): - device_class: Optional[str] = None - multiple: Optional[bool] = None + device_class: str | None = None + multiple: bool | None = None class ServiceFieldSelectorTarget(BaseModel): - entity: Optional[ - Union[List[ServiceFieldSelectorEntityFilter], ServiceFieldSelectorEntityFilter] - ] = None - device: Optional[ - Union[List[ServiceFieldSelectorDeviceFilter], ServiceFieldSelectorDeviceFilter] - ] = None + entity: ( + list[ServiceFieldSelectorEntityFilter] | ServiceFieldSelectorEntityFilter | None + ) = None + device: ( + list[ServiceFieldSelectorDeviceFilter] | ServiceFieldSelectorDeviceFilter | None + ) = None class ServiceFieldSelectorTemplate(BaseModel): @@ -444,24 +460,24 @@ class ServiceFieldSelectorTemplate(BaseModel): class ServiceFieldSelectorSTT(BaseModel): - language: Optional[str] = None + language: str | None = None class ServiceFieldSelectorText(BaseModel): - multiline: Optional[bool] = None - type: Optional[ServiceFieldSelectorTextType] = None - prefix: Optional[str] = None - suffix: Optional[str] = None - autocomplete: Optional[str] = None - multiple: Optional[bool] = None + multiline: bool | None = None + type: ServiceFieldSelectorTextType | None = None + prefix: str | None = None + suffix: str | None = None + autocomplete: str | None = None + multiple: bool | None = None class ServiceFieldSelectorTheme(BaseModel): - include_default: Optional[bool] = None + include_default: bool | None = None class ServiceFieldSelectorTime(BaseModel): - no_second: Optional[bool] = None + no_second: bool | None = None class ServiceFieldSelectorTrigger(BaseModel): @@ -469,12 +485,12 @@ class ServiceFieldSelectorTrigger(BaseModel): class ServiceFieldSelectorTTS(BaseModel): - language: Optional[str] = None + language: str | None = None class ServiceFieldSelectorTTSVoice(BaseModel): - engineId: Optional[str] = None - language: Optional[str] = None + engine_id: str | None = Field(default=None, alias="engineId") + language: str | None = None class ServiceFieldSelectorUIAction(BaseModel): @@ -482,188 +498,163 @@ class ServiceFieldSelectorUIAction(BaseModel): class ServiceFieldSelectorUIColor(BaseModel): - default_color: Optional[str] = None - include_none: Optional[bool] = None - include_state: Optional[bool] = None + default_color: str | None = None + include_none: bool | None = None + include_state: bool | None = None class ServiceFieldSelectorUIStateContext(BaseModel): - entity_id: Optional[str] = None - allow_name: Optional[bool] = None + entity_id: str | None = None + allow_name: bool | None = None class ServiceFieldSelector(BaseModel): - action: Optional[ServiceFieldSelectorAction] = None - addon: Optional[ServiceFieldSelectorAddon] = None - area: Optional[ServiceFieldSelectorArea] = None - areas_display: Optional[ServiceFieldSelectorAreasDisplay] = None - attribute: Optional[ServiceFieldSelectorAttribute] = None - assist_pipeline: Optional[ServiceFieldSelectorAssistPipeline] = None - backup_location: Optional[ServiceFieldSelectorBackupLocation] = None - background: Optional[ServiceFieldSelectorBackground] = None - boolean: Optional[ServiceFieldSelectorBoolean] = None - button_toggle: Optional[ServiceFieldSelectorButtonToggle] = None - color_rgb: Optional[ServiceFieldSelectorColorRGB] = None - color_temp: Optional[ServiceFieldSelectorColorTemp] = None - condition: Optional[ServiceFieldSelectorCondition] = None - config_entry: Optional[ServiceFieldSelectorConfigEntry] = None - constant: Optional[ServiceFieldSelectorConstant] = None - conversation_agent: Optional[ServiceFieldSelectorConversationAgent] = None - country: Optional[ServiceFieldSelectorCountry] = None - date: Optional[ServiceFieldSelectorDate] = None - datetime: Optional[ServiceFieldSelectorDateTime] = None - device: Optional[ - Union[ServiceFieldSelectorDevice, ServiceFieldSelectorDeviceLegacy] - ] = None - duration: Optional[ServiceFieldSelectorDuration] = None - entity: Optional[ - Union[ServiceFieldSelectorEntity, ServiceFieldSelectorEntityLegacy] - ] = None - floor: Optional[ServiceFieldSelectorFloor] = None - file: Optional[ServiceFieldSelectorFile] = None - icon: Optional[ServiceFieldSelectorIcon] = None - image: Optional[ServiceFieldSelectorImage] = None - label: Optional[ServiceFieldSelectorLabel] = None - language: Optional[ServiceFieldSelectorLanguage] = None - location: Optional[ServiceFieldSelectorLocation] = None - media: Optional[ServiceFieldSelectorMedia] = None - navigation: Optional[ServiceFieldSelectorNavigation] = None - number: Optional[ServiceFieldSelectorNumber] = None - object: Optional[ServiceFieldSelectorObject] = None - qr_code: Optional[ServiceFieldSelectorQRCode] = None - select: Optional[ServiceFieldSelectorSelect] = None - selector: Optional[ServiceFieldSelectorSelector] = None - state: Optional[ServiceFieldSelectorState] = None - statistic: Optional[ServiceFieldSelectorStatistic] = None - target: Optional[ServiceFieldSelectorTarget] = None - template: Optional[ServiceFieldSelectorTemplate] = None - stt: Optional[ServiceFieldSelectorSTT] = None - text: Optional[ServiceFieldSelectorText] = None - theme: Optional[ServiceFieldSelectorTheme] = None - time: Optional[ServiceFieldSelectorTime] = None - trigger: Optional[ServiceFieldSelectorTrigger] = None - tts: Optional[ServiceFieldSelectorTTS] = None - tts_voice: Optional[ServiceFieldSelectorTTSVoice] = None - ui_action: Optional[ServiceFieldSelectorUIAction] = None - ui_color: Optional[ServiceFieldSelectorUIColor] = None - ui_state_content: Optional[ServiceFieldSelectorUIStateContext] = None + action: ServiceFieldSelectorAction | None = None + addon: ServiceFieldSelectorAddon | None = None + area: ServiceFieldSelectorArea | None = None + areas_display: ServiceFieldSelectorAreasDisplay | None = None + attribute: ServiceFieldSelectorAttribute | None = None + assist_pipeline: ServiceFieldSelectorAssistPipeline | None = None + backup_location: ServiceFieldSelectorBackupLocation | None = None + background: ServiceFieldSelectorBackground | None = None + boolean: ServiceFieldSelectorBoolean | None = None + button_toggle: ServiceFieldSelectorButtonToggle | None = None + color_rgb: ServiceFieldSelectorColorRGB | None = None + color_temp: ServiceFieldSelectorColorTemp | None = None + condition: ServiceFieldSelectorCondition | None = None + config_entry: ServiceFieldSelectorConfigEntry | None = None + constant: ServiceFieldSelectorConstant | None = None + conversation_agent: ServiceFieldSelectorConversationAgent | None = None + country: ServiceFieldSelectorCountry | None = None + date: ServiceFieldSelectorDate | None = None + datetime: ServiceFieldSelectorDateTime | None = None + device: ServiceFieldSelectorDevice | ServiceFieldSelectorDeviceLegacy | None = None + duration: ServiceFieldSelectorDuration | None = None + entity: ServiceFieldSelectorEntity | ServiceFieldSelectorEntityLegacy | None = None + floor: ServiceFieldSelectorFloor | None = None + file: ServiceFieldSelectorFile | None = None + icon: ServiceFieldSelectorIcon | None = None + image: ServiceFieldSelectorImage | None = None + label: ServiceFieldSelectorLabel | None = None + language: ServiceFieldSelectorLanguage | None = None + location: ServiceFieldSelectorLocation | None = None + media: ServiceFieldSelectorMedia | None = None + navigation: ServiceFieldSelectorNavigation | None = None + number: ServiceFieldSelectorNumber | None = None + object: ServiceFieldSelectorObject | None = None + qr_code: ServiceFieldSelectorQRCode | None = None + select: ServiceFieldSelectorSelect | None = None + selector: ServiceFieldSelectorSelector | None = None + state: ServiceFieldSelectorState | None = None + statistic: ServiceFieldSelectorStatistic | None = None + target: ServiceFieldSelectorTarget | None = None + template: ServiceFieldSelectorTemplate | None = None + stt: ServiceFieldSelectorSTT | None = None + text: ServiceFieldSelectorText | None = None + theme: ServiceFieldSelectorTheme | None = None + time: ServiceFieldSelectorTime | None = None + trigger: ServiceFieldSelectorTrigger | None = None + tts: ServiceFieldSelectorTTS | None = None + tts_voice: ServiceFieldSelectorTTSVoice | None = None + ui_action: ServiceFieldSelectorUIAction | None = None + ui_color: ServiceFieldSelectorUIColor | None = None + ui_state_content: ServiceFieldSelectorUIStateContext | None = None # Service bases class ServiceFieldFilter(BaseModel): - supported_features: Optional[Union[List[int], int]] = ( + supported_features: list[int] | int | None = ( None # Bitset (any needs to be supported [or all within specified list]) ) - attribute: Optional[Dict[str, Union[List[str], str]]] = None + attribute: dict[str, list[str] | str] | None = None class ServiceField(BaseModel): """Model for service parameters/fields.""" - description: Optional[str] = None - example: Optional[JSONType] = None - default: Optional[JSONType] = None - name: Optional[str] = None - required: Optional[bool] = None - advanced: Optional[bool] = None - selector: Optional[ServiceFieldSelector] = None - filter: Optional[ServiceFieldFilter] = None + description: str | None = None + example: Any | None = None + default: Any | None = None + name: str | None = None + required: bool | None = None + advanced: bool | None = None + selector: ServiceFieldSelector | None = None + filter: ServiceFieldFilter | None = None class ServiceFieldCollection(BaseModel): - collapsed: Optional[bool] = None - fields: Dict[str, ServiceField] + collapsed: bool | None = None + fields: dict[str, ServiceField] class ServiceResponse(BaseModel): - optional: Optional[bool] = None + optional: bool | None = None -class Service(BaseModel): +class BaseService(BaseModel): """Model representing services from homeassistant""" service_id: str + name: str | None = None + description: str | None = None + fields: dict[str, ServiceField | ServiceFieldCollection] | None = None + target: ServiceFieldSelectorTarget | None = None + response: ServiceResponse | None = None + + +class Service(BaseService): + """Sync service with sync trigger method.""" + domain: Domain = Field(exclude=True, repr=False) - name: Optional[str] = None - description: Optional[str] = None - fields: Optional[Dict[str, Union[ServiceField, ServiceFieldCollection]]] = None - target: Optional[ServiceFieldSelectorTarget] = None - response: Optional[ServiceResponse] = None def trigger( - self, **service_data - ) -> Union[ - Tuple[State, ...], - Tuple[Tuple[State, ...], dict[str, JSONType]], - dict[str, JSONType], - None, - ]: + self, + **service_data: Any, + ) -> ( + tuple[State, ...] + | tuple[tuple[State, ...], dict[str, Any]] + | dict[str, Any] + | None + ): """Triggers the service associated with this object.""" try: - return self.domain._client.trigger_service_with_response( + return self.domain.client.trigger_service_with_response( self.domain.domain_id, self.service_id, **service_data, ) - except RequestError: - return self.domain._client.trigger_service( + except (RequestError, ResponseError): + return self.domain.client.trigger_service( self.domain.domain_id, self.service_id, **service_data, ) - async def async_trigger( - self, **service_data - ) -> Union[ - Tuple[State, ...], - None, - dict[str, JSONType], - tuple[tuple[State, ...], dict[str, JSONType]], - ]: + +class AsyncService(BaseService): + """Async service with async trigger method.""" + + domain: AsyncDomain = Field(exclude=True, repr=False) + + async def trigger( + self, + **service_data: Any, + ) -> ( + tuple[State, ...] + | tuple[tuple[State, ...], dict[str, Any]] + | dict[str, Any] + | None + ): """Triggers the service associated with this object.""" try: - return await self.domain._client.async_trigger_service_with_response( + return await self.domain.client.trigger_service_with_response( self.domain.domain_id, self.service_id, **service_data, ) - except RequestError: - return await self.domain._client.async_trigger_service( + except (RequestError, ResponseError): + return await self.domain.client.trigger_service( self.domain.domain_id, self.service_id, **service_data, ) - - def __call__( - self, **service_data - ) -> Union[ - Union[ - Tuple[State, ...], - Tuple[Tuple[State, ...], dict[str, JSONType]], - dict[str, JSONType], - None, - ], - Coroutine[ - Any, - Any, - Union[ - Tuple[State, ...], - Tuple[Tuple[State, ...], dict[str, JSONType]], - dict[str, JSONType], - None, - ], - ], - ]: - """ - Triggers the service associated with this object. - """ - assert (frame := inspect.currentframe()) is not None - assert (parent_frame := frame.f_back) is not None - try: - if inspect.iscoroutinefunction( - caller := gc.get_referrers(parent_frame.f_code)[0] - ) or inspect.iscoroutine(caller): - return self.async_trigger(**service_data) - except IndexError: # pragma: no cover - pass - return self.trigger(**service_data) diff --git a/homeassistant_api/models/entity.py b/homeassistant_api/models/entity.py index 70cab66b..3efbaa8d 100644 --- a/homeassistant_api/models/entity.py +++ b/homeassistant_api/models/entity.py @@ -1,7 +1,10 @@ """Module for Entity and entity Group data models""" from datetime import datetime -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import TYPE_CHECKING +from typing import Any +from typing import Optional +from typing import Union from pydantic import Field @@ -10,29 +13,45 @@ from .states import State if TYPE_CHECKING: + from homeassistant_api import AsyncClient + from homeassistant_api import AsyncWebsocketClient from homeassistant_api import Client + from homeassistant_api import WebsocketClient -class Group(BaseModel): +class BaseGroup(BaseModel): """Represents the groups that entities belong to.""" - def __init__(self, *args, _client: Optional["Client"] = None, **kwargs) -> None: - super().__init__(*args, **kwargs) - object.__setattr__(self, "_client", _client) - group_id: str = Field( ..., description="A unique string identifying different types/groups of entities.", ) - _client: "Client" - entities: Dict[str, "Entity"] = Field( - {}, + entities: dict[str, "BaseEntity"] = Field( + default_factory=dict, description="A dictionary of all entities belonging to the group " "indexed by their :code:`entity_id`.", ) def _add_entity(self, slug: str, state: State) -> None: """Registers entities to this Group object""" + raise NotImplementedError + + def get_entity(self, slug: str) -> Optional["BaseEntity"]: + """Returns Entity with the given name if it exists. Otherwise returns None""" + return self.entities.get(slug) + + def __getattr__(self, key: str) -> Any: + if key in self.entities: + return self.get_entity(key) + return super().__getattribute__(key) + + +class Group(BaseGroup): + """Sync group that creates sync Entity instances.""" + + client: Union["Client", "WebsocketClient"] = Field(exclude=True, repr=False) + + def _add_entity(self, slug: str, state: State) -> None: self.entities[slug] = Entity( slug=slug, state=state, @@ -41,24 +60,58 @@ def _add_entity(self, slug: str, state: State) -> None: def get_entity(self, slug: str) -> Optional["Entity"]: """Returns Entity with the given name if it exists. Otherwise returns None""" - return self.entities.get(slug) + entity = self.entities.get(slug) + if entity is not None and not isinstance(entity, Entity): + msg = f"Expected Entity, got {type(entity)}" + raise TypeError(msg) + return entity - def __getattr__(self, key: str) -> Any: - if key in self.entities: - return self.get_entity(key) - return super().__getattribute__(key) + +class AsyncGroup(BaseGroup): + """Async group that creates async Entity instances.""" + + client: Union["AsyncClient", "AsyncWebsocketClient"] = Field( + exclude=True, + repr=False, + ) + + def _add_entity(self, slug: str, state: State) -> None: + self.entities[slug] = AsyncEntity( + slug=slug, + state=state, + group=self, + ) + + def get_entity(self, slug: str) -> Optional["AsyncEntity"]: + """Returns Entity with the given name if it exists. Otherwise returns None""" + entity = self.entities.get(slug) + if entity is not None and not isinstance(entity, AsyncEntity): + msg = f"Expected AsyncEntity, got {type(entity)}" + raise TypeError(msg) + return entity -class Entity(BaseModel): +class BaseEntity(BaseModel): """Represents entities inside of homeassistant""" slug: str state: State + group: "BaseGroup" = Field(exclude=True, repr=False) + + @property + def entity_id(self) -> str: + """Constructs the :code:`entity_id` string from its group and slug""" + return f"{self.group.group_id}.{self.slug}".strip() + + +class Entity(BaseEntity): + """Sync entity with sync client methods.""" + group: Group = Field(exclude=True, repr=False) def get_state(self) -> State: """Asks Home Assistant for the state of the entity and updates it locally""" - self.state = self.group._client.get_state(entity_id=self.entity_id) + self.state = self.group.client.get_state(entity_id=self.entity_id) return self.state def update_state(self) -> State: @@ -66,23 +119,17 @@ def update_state(self) -> State: Tells Home Assistant to set its current local State object. (You can modify the local state object yourself.) """ - self.state = self.group._client.set_state(self.state) + self.state = self.group.client.set_state(self.state) return self.state - @property - def entity_id(self) -> str: - """Constructs the :code:`entity_id` string from its group and slug""" - return f"{self.group.group_id}.{self.slug}".strip() - def get_history( self, - start_timestamp: Optional[datetime] = None, - # Defaults to 1 day before. https://developers.home-assistant.io/docs/api/rest/ - end_timestamp: Optional[datetime] = None, + start_timestamp: datetime | None = None, + end_timestamp: datetime | None = None, significant_changes_only: bool = False, - ) -> Optional[History]: + ) -> History | None: """Gets the previous :py:class:`State`'s of the :py:class:`Entity`""" - for history in self.group._client.get_entity_histories( + for history in self.group.client.get_entity_histories( entities=(self,), start_timestamp=start_timestamp, end_timestamp=end_timestamp, @@ -91,30 +138,35 @@ def get_history( return history return None - async def async_get_state(self) -> State: + +class AsyncEntity(BaseEntity): + """Async entity with async client methods.""" + + group: AsyncGroup = Field(exclude=True, repr=False) + + async def get_state(self) -> State: """Asks Home Assistant for the state of the entity and sets it locally""" - self.state = await self.group._client.async_get_state( + self.state = await self.group.client.get_state( group_id=self.group.group_id, slug=self.slug, ) return self.state - async def async_update_state(self) -> State: + async def update_state(self) -> State: """Tells Home Assistant to set the current local State object.""" - self.state = await self.group._client.async_set_state(self.state) + self.state = await self.group.client.set_state(self.state) return self.state - async def async_get_history( + async def get_history( self, - start_timestamp: Optional[datetime] = None, - # Defaults to 1 day before. https://developers.home-assistant.io/docs/api/rest/ - end_timestamp: Optional[datetime] = None, + start_timestamp: datetime | None = None, + end_timestamp: datetime | None = None, significant_changes_only: bool = False, - ) -> Optional[History]: + ) -> History | None: """ Gets the :py:class:`History` of previous :py:class:`State`'s of the :py:class:`Entity`. """ - async for history in self.group._client.async_get_entity_histories( + async for history in self.group.client.get_entity_histories( entities=(self,), start_timestamp=start_timestamp, end_timestamp=end_timestamp, diff --git a/homeassistant_api/models/entity_registry.py b/homeassistant_api/models/entity_registry.py new file mode 100644 index 00000000..f8395d8a --- /dev/null +++ b/homeassistant_api/models/entity_registry.py @@ -0,0 +1,97 @@ +"""Models for Home Assistant entity registry responses.""" + +from enum import StrEnum +from typing import Any +from typing import TypedDict + +from pydantic import Field +from typing_extensions import NotRequired + +from .base import BaseModel +from .base import DatetimeIsoField + + +class EntityDisabledBy(StrEnum): + """What disabled an entity.""" + + CONFIG_ENTRY = "config_entry" + DEVICE = "device" + HASS = "hass" + INTEGRATION = "integration" + USER = "user" + + +class EntityHiddenBy(StrEnum): + """What hid an entity.""" + + INTEGRATION = "integration" + USER = "user" + + +class EntityCategory(StrEnum): + """Category of an entity.""" + + CONFIG = "config" + DIAGNOSTIC = "diagnostic" + + +class EntityRegistryEntry(BaseModel): + """An entity registry entry as returned by ``config/entity_registry/list``.""" + + area_id: str | None = None + categories: dict[str, str] = Field(default_factory=dict) + config_entry_id: str | None = None + config_subentry_id: str | None = None + created_at: DatetimeIsoField + device_id: str | None = None + disabled_by: EntityDisabledBy | None = None + entity_category: EntityCategory | None = None + entity_id: str + has_entity_name: bool + hidden_by: EntityHiddenBy | None = None + icon: str | None = None + id: str + modified_at: DatetimeIsoField + name: str | None = None + options: dict[str, Any] = Field(default_factory=dict) + original_name: str | None = None + platform: str + translation_key: str | None = None + unique_id: str + + +class EntityRegistryEntryExtended(EntityRegistryEntry): + """Extended entity registry entry as returned by ``config/entity_registry/get``.""" + + aliases: list[str] | tuple[None] = Field(default_factory=list) + capabilities: dict[str, Any] | None = None + device_class: str | None = None + original_device_class: str | None = None + original_icon: str | None = None + + +class EntityRegistryUpdateResult(BaseModel): + """Result from ``config/entity_registry/update``.""" + + entity_entry: EntityRegistryEntryExtended + reload_delay: int | None = None + require_restart: bool = False + + +class EntityRegistryUpdateParams(TypedDict): + """Parameters used in ``config/entity_registry/update``.""" + + aliases: NotRequired[list[str]] + area_id: NotRequired[str | None] + categories: NotRequired[dict[str, str]] + device_class: NotRequired[str | None] + disabled_by: NotRequired[EntityHiddenBy | None] + entity_id: str + hidden_by: NotRequired[EntityHiddenBy | None] + icon: NotRequired[str | None] + labels: NotRequired[list[str]] + name: NotRequired[str | None] + new_entity_id: NotRequired[str] + # options and options_domain are inclusive, meaning only both or none of them have to be defined + options_domain: NotRequired[str] + options: NotRequired[dict[str, Any]] diff --git a/homeassistant_api/models/events.py b/homeassistant_api/models/events.py index a80f9b36..307c5f59 100644 --- a/homeassistant_api/models/events.py +++ b/homeassistant_api/models/events.py @@ -1,19 +1,20 @@ """Event Model File""" -from typing import TYPE_CHECKING, Any, Optional, Union -from typing_extensions import Self +from typing import TYPE_CHECKING +from typing import Any + from pydantic import Field +from typing_extensions import Self from typing_extensions import override -from homeassistant_api.utils import JSONType - from .base import BaseModel if TYPE_CHECKING: + from homeassistant_api import AsyncClient from homeassistant_api import Client -class Event(BaseModel): +class BaseEvent(BaseModel): """ Event class for Home Assistant Event Triggers @@ -21,35 +22,48 @@ class Event(BaseModel): https://data.home-assistant.io/docs/events """ - _client: "Client" event: str = Field(..., description="The event name/type.") listener_count: int = Field( ..., - description="How many listeners are interesting in this event in Home Assistant.", + description="How many listeners are interested in this event in Home Assistant.", ) - def __init__(self, *args, _client: Optional["Client"] = None, **kwargs) -> None: - super().__init__(*args, **kwargs) - object.__setattr__(self, "_client", _client) + @classmethod + @override + def from_json(cls, json: dict[str, Any] | Any | None, **kwargs: Any) -> Self: + msg = f"`{cls.__name__}` does not support `from_json()`. Use `from_json_with_client()`" + raise NotImplementedError(msg) - def fire(self, **event_data) -> Optional[str]: - """Fires the corresponding event in Home Assistant.""" - return self._client.fire_event(self.event, **event_data) - async def async_fire(self, **event_data) -> str: - """Fires the event type in homeassistant. Ex. `on_startup`""" - return await self._client.async_fire_event(self.event, **event_data) +class Event(BaseEvent): + """Sync event with sync fire method.""" + + client: "Client" = Field(exclude=True, repr=False) @classmethod - @override - def from_json(cls, json: Union[dict[str, JSONType], Any, None], **kwargs) -> Self: - raise ValueError( - f"`{cls.__name__}` does not support `from_json()`. Use `from_json_with_client()`" - ) + def from_json_with_client(cls, json: dict[str, Any], client: "Client") -> "Event": + """Constructs Event model from json data""" + return cls(**json, client=client) + + def fire(self, **event_data: Any) -> str | None: + """Fires the corresponding event in Home Assistant.""" + return self.client.fire_event(self.event, **event_data) + + +class AsyncEvent(BaseEvent): + """Async event with async fire method.""" + + client: "AsyncClient" = Field(exclude=True, repr=False) @classmethod def from_json_with_client( - cls, json: dict[str, JSONType], client: "Client" - ) -> "Event": + cls, + json: dict[str, Any], + client: "AsyncClient", + ) -> "AsyncEvent": """Constructs Event model from json data""" - return cls(**json, _client=client) + return cls(**json, client=client) + + async def fire(self, **event_data: Any) -> str: + """Fires the event type in homeassistant.""" + return await self.client.fire_event(self.event, **event_data) diff --git a/homeassistant_api/models/history.py b/homeassistant_api/models/history.py index f683d471..b78b9497 100644 --- a/homeassistant_api/models/history.py +++ b/homeassistant_api/models/history.py @@ -1,6 +1,6 @@ """Module for the History model.""" -from typing import Tuple +from typing import Any from pydantic import Field @@ -11,17 +11,22 @@ class History(BaseModel): """Model representing past :py:class:`State`'s of an entity.""" - states: Tuple[State, ...] = Field( - ..., description="A tuple of previous states of an entity." + states: tuple[State, ...] = Field( + ..., + description="A tuple of previous states of an entity.", ) - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - assert self.entity_id is not None + if not self.states: + msg = "History requires at least one state" + raise ValueError(msg) + entity_ids = {state.entity_id for state in self.states} + if len(entity_ids) != 1: + msg = f"All states in a History must share the same entity_id, got {entity_ids}" + raise ValueError(msg) @property def entity_id(self) -> str: """Returns the shared :code:`entity_id` of states.""" - entity_ids = [state.entity_id for state in self.states] - result, *_ = set(entity_ids) - return result + return self.states[0].entity_id diff --git a/homeassistant_api/models/logbook.py b/homeassistant_api/models/logbook.py index 08e21c7d..a3b160e2 100644 --- a/homeassistant_api/models/logbook.py +++ b/homeassistant_api/models/logbook.py @@ -1,10 +1,9 @@ """Module for the Logbook Entry model.""" -from typing import Optional - from pydantic import Field -from .base import BaseModel, DatetimeIsoField +from .base import BaseModel +from .base import DatetimeIsoField class LogbookEntry(BaseModel): @@ -12,15 +11,18 @@ class LogbookEntry(BaseModel): when: DatetimeIsoField = Field(..., description="When the entry was logged.") name: str = Field(..., description="The name of the entry.") - message: Optional[str] = Field(None, description="Optional message for the entry.") - entity_id: Optional[str] = Field(None, description="Optional relevant entity_id.") - state: Optional[str] = Field( - None, description="The new state information of the entity_id." + message: str | None = Field(None, description="Optional message for the entry.") + entity_id: str | None = Field(None, description="Optional relevant entity_id.") + state: str | None = Field( + None, + description="The new state information of the entity_id.", ) - domain: Optional[str] = Field(None, description="When the entry was logged.") - context_id: Optional[str] = Field( - None, description="Optional relevant context instead of an entity." + domain: str | None = Field(None, description="When the entry was logged.") + context_id: str | None = Field( + None, + description="Optional relevant context instead of an entity.", ) - icon: Optional[str] = Field( - None, description="An MDI icon associated with the entity_id." + icon: str | None = Field( + None, + description="An MDI icon associated with the entity_id.", ) diff --git a/homeassistant_api/models/states.py b/homeassistant_api/models/states.py index 00e80051..36b0b728 100644 --- a/homeassistant_api/models/states.py +++ b/homeassistant_api/models/states.py @@ -1,12 +1,13 @@ """Module for the Entity State model.""" -from datetime import datetime, timezone -from typing import Optional +from datetime import datetime +from datetime import timezone +from typing import Any from pydantic import Field -from homeassistant_api.models.base import BaseModel, DatetimeIsoField -from homeassistant_api.utils import JSONType +from homeassistant_api.models.base import BaseModel +from homeassistant_api.models.base import DatetimeIsoField __all__ = ( "Context", @@ -21,11 +22,11 @@ class Context(BaseModel): max_length=128, # arbitrary limit description="Unique string identifying the context.", ) - parent_id: Optional[str] = Field( + parent_id: str | None = Field( max_length=128, description="Unique string identifying the parent context.", ) - user_id: Optional[str] = Field( + user_id: str | None = Field( max_length=128, description="Unique string identifying the user.", ) @@ -36,23 +37,26 @@ class State(BaseModel): entity_id: str = Field(..., description="The entity_id this state corresponds to.") state: str = Field( - ..., description="The string representation of the state of the entity." + ..., + description="The string representation of the state of the entity.", ) - attributes: dict[str, JSONType] = Field( - {}, description="A dictionary of extra attributes of the state." + attributes: dict[str, Any] = Field( + default_factory=dict, + description="A dictionary of extra attributes of the state.", ) last_changed: DatetimeIsoField = Field( default_factory=lambda: datetime.now(timezone.utc), description="The last time the state was changed.", ) - last_updated: Optional[DatetimeIsoField] = Field( + last_updated: DatetimeIsoField | None = Field( default_factory=lambda: datetime.now(timezone.utc), description="The last time the state updated.", ) - last_reported: Optional[DatetimeIsoField] = Field( + last_reported: DatetimeIsoField | None = Field( default_factory=lambda: datetime.now(timezone.utc), description="The last time the state was reported to the server. Only used by some integrations.", ) - context: Optional[Context] = Field( - None, description="Provides information about the context of the state." + context: Context | None = Field( + default=None, + description="Provides information about the context of the state.", ) diff --git a/homeassistant_api/models/websocket.py b/homeassistant_api/models/websocket.py index 0cb8de7f..8838e648 100644 --- a/homeassistant_api/models/websocket.py +++ b/homeassistant_api/models/websocket.py @@ -1,21 +1,21 @@ """A module defining the responses we expect from the websocket API.""" -from typing import Any, List, Literal, Optional, Union +from typing import Any +from typing import Literal -from homeassistant_api.utils import JSONType - -from .base import BaseModel, DatetimeIsoField +from .base import BaseModel +from .base import DatetimeIsoField from .config_entries import ConfigEntryEvent from .states import Context __all__ = ( - "AuthRequired", - "AuthOk", "AuthInvalid", - "PingResponse", + "AuthOk", + "AuthRequired", "ErrorResponse", - "ResultResponse", "EventResponse", + "PingResponse", + "ResultResponse", ) @@ -40,15 +40,15 @@ class PingResponse(BaseModel): id: int type: Literal["pong"] start: int # added by the client, nanoseconds - end: Optional[int] = None # added by the client, nanoseconds + end: int | None = None # added by the client, nanoseconds class Error(BaseModel): code: str message: str - translation_key: Optional[str] = None - translation_placeholders: Optional[dict[str, str]] = None - translation_domain: Optional[str] = None + translation_key: str | None = None + translation_placeholders: dict[str, str] | None = None + translation_domain: str | None = None class ErrorResponse(BaseModel): @@ -66,33 +66,33 @@ class ResultResponse(BaseModel): id: int success: Literal[True] type: Literal["result"] - result: Optional[Any] + result: Any | None class FiredEvent(BaseModel): """A model to parse the `event` key of fired event websocket responses.""" event_type: str - data: dict[str, JSONType] + data: dict[str, Any] origin: Literal["LOCAL", "REMOTE"] # REMOTE if another API client or webhook fired the event # LOCAL if Home Assistant (or the auth token we used) fired the event time_fired: DatetimeIsoField # datetime.datetime - context: Optional[Context] + context: Context | None class TemplateEvent(BaseModel): - result: str - listeners: dict[str, JSONType] + result: Any + listeners: dict[str, Any] class FiredTrigger(BaseModel): """A model to parse the `trigger` key of fired event websocket responses.""" - context: Optional[Context] - variables: dict[str, JSONType] + context: Context | None + variables: dict[str, Any] class EventResponse(BaseModel): @@ -100,4 +100,4 @@ class EventResponse(BaseModel): id: int type: Literal["event"] - event: Union[FiredEvent, FiredTrigger, TemplateEvent, List[ConfigEntryEvent]] + event: FiredEvent | FiredTrigger | TemplateEvent | list[ConfigEntryEvent] diff --git a/homeassistant_api/processing.py b/homeassistant_api/processing.py index 1e430c71..91f6f1f8 100644 --- a/homeassistant_api/processing.py +++ b/homeassistant_api/processing.py @@ -1,145 +1,141 @@ """Module for processing API responses from homeassistant.""" -import inspect import json import logging -from typing import Any, Callable, ClassVar, Dict, Tuple, Union, cast +from collections.abc import Callable +from dataclasses import dataclass +from http import HTTPStatus +from typing import Any import simplejson -from aiohttp import ClientResponse -from aiohttp_client_cache.response import CachedResponse as AsyncCachedResponse -from requests import Response -from requests_cache.models.response import CachedResponse - -from homeassistant_api.errors import ( - EndpointNotFoundError, - InternalServerError, - MalformedDataError, - MethodNotAllowedError, - ProcessorNotFoundError, - RequestError, - UnauthorizedError, - UnexpectedStatusCodeError, -) -from homeassistant_api.utils import JSONType +from niquests import Response + +from homeassistant_api.errors import EndpointNotFoundError +from homeassistant_api.errors import InternalServerError +from homeassistant_api.errors import MalformedDataError +from homeassistant_api.errors import MethodNotAllowedError +from homeassistant_api.errors import ProcessorNotFoundError +from homeassistant_api.errors import RequestError +from homeassistant_api.errors import UnauthorizedError +from homeassistant_api.errors import UnexpectedStatusCodeError logger = logging.getLogger(__name__) +ResponseType = Response + + +@dataclass(frozen=True) +class ResponseInfo: + """Extracted metadata from an HTTP response for status code validation.""" + + status_code: int + url: str + method: str | None + + +# --- Status code validation --- + + +def _check_status(info: ResponseInfo, content: str) -> None: + """Raise appropriate error for non-success status codes. + + Content is only used in error messages for 400/500+ responses. + """ + if info.status_code in (HTTPStatus.OK, HTTPStatus.CREATED): + return # pragma: no cover + if info.status_code == HTTPStatus.BAD_REQUEST: + raise RequestError(content, url=info.url) + if info.status_code == HTTPStatus.UNAUTHORIZED: + raise UnauthorizedError + if info.status_code == HTTPStatus.NOT_FOUND: + raise EndpointNotFoundError(info.url) + if info.status_code == HTTPStatus.METHOD_NOT_ALLOWED: + raise MethodNotAllowedError(info.method or "UNKNOWN") + if info.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR: + raise InternalServerError(info.status_code, content) + raise UnexpectedStatusCodeError(info.status_code) + + +def _extract_info(response: ResponseType) -> ResponseInfo: + """Extract status code, URL, and method from a response.""" + if response.status_code is None: + msg = "Response is missing status code." + raise ValueError(msg) + if response.request is None: + msg = "Response is missing request information." + raise ValueError(msg) + if response.url is None: + msg = "Response is missing URL information." + raise ValueError(msg) + return ResponseInfo( + status_code=response.status_code, + url=response.url, + method=response.request.method, + ) + + +def _check_sync_status(response: ResponseType) -> None: + """Validate a response status code, lazily reading content only on error.""" + info = _extract_info(response) + if info.status_code in (HTTPStatus.OK, HTTPStatus.CREATED): + return + _check_status(info, content=_parse_text(response)) + -AsyncResponseType = Union[AsyncCachedResponse, ClientResponse] -ResponseType = Union[Response, CachedResponse] -AllResponseType = Union[AsyncCachedResponse, ClientResponse, Response, CachedResponse] -ProcessorType = Callable[[AllResponseType], Any] - - -class Processing: - """Uses to processor functions to convert json data into common python data types.""" - - _response: AllResponseType - _processors: ClassVar[Dict[str, Tuple[ProcessorType, ...]]] = {} - - def __init__(self, response: AllResponseType, decode_bytes: bool = True) -> None: - self._response = response - self._decode_bytes = decode_bytes - - @staticmethod - def processor(mimetype: str) -> Callable[[ProcessorType], ProcessorType]: - """A decorator used to register a response converter function.""" - - def register_processor(processor: ProcessorType) -> ProcessorType: - if mimetype not in Processing._processors: - Processing._processors[mimetype] = tuple() - Processing._processors[mimetype] += (processor,) - return processor - - return register_processor - - def process_content(self, *, async_: bool = False) -> Any: - """ - Looks up processors by their Content-Type header and then - calls the processor with the response. - """ - - mimetype_header = self._response.headers.get( - "content-type", - "text/plain", - ) - mimetype = mimetype_header.split(";")[0] - for processor in self._processors.get(mimetype, ()): - if not async_ ^ inspect.iscoroutinefunction(processor): - logger.debug("Using processor %r on %r", processor, self._response) - return processor(self._response) - raise ProcessorNotFoundError( - f"No response processor found for mimetype {mimetype!r}." - ) - - def process(self) -> Any: - """Validates the http status code before starting to process the repsonse content""" - content: Union[str, bytes] - if async_ := isinstance(self._response, (ClientResponse, AsyncCachedResponse)): - status_code = self._response.status - _buffer = self._response.content._buffer - content = b"" if not _buffer else _buffer[0] - elif isinstance(self._response, (Response, CachedResponse)): - status_code = self._response.status_code - content = self._response.content - else: - raise TypeError( - f"Unsupported response type: {type(self._response).__name__}" - ) - if self._decode_bytes and isinstance(content, bytes): - content = content.decode() - if status_code in (200, 201): - return self.process_content(async_=async_) - if status_code == 400: - raise RequestError(content, url=self._response.url) # type: ignore - if status_code == 401: - raise UnauthorizedError() - if status_code == 404: - raise EndpointNotFoundError(self._response.url) # type: ignore - if status_code == 405: - if isinstance(self._response, (Response, CachedResponse)): - method = self._response.request.method - else: - method = self._response.method - raise MethodNotAllowedError(cast(str, method)) - if status_code >= 500: - raise InternalServerError(status_code, content) - raise UnexpectedStatusCodeError(status_code) - - -# List of default processors -@Processing.processor("application/json") # type: ignore[arg-type] -def process_json(response: ResponseType) -> dict[str, JSONType]: - """Returns the json dict content of the response.""" +# --- Individual parse functions --- + + +def _parse_json(response: ResponseType) -> Any: + """Parse a sync response as JSON.""" try: - return cast(dict[str, JSONType], response.json()) + return response.json() except (json.JSONDecodeError, simplejson.JSONDecodeError) as err: - raise MalformedDataError( - f"Home Assistant responded with non-json response: {repr(response.text)}" - ) from err + msg = f"Home Assistant responded with non-json response: {response.text!r}" + raise MalformedDataError(msg) from err -@Processing.processor("text/plain") # type: ignore[arg-type] -@Processing.processor("application/octet-stream") # type: ignore[arg-type] -def process_text(response: ResponseType) -> str: - """Returns the plaintext of the reponse.""" +def _parse_text(response: ResponseType) -> str: + """Return the plaintext content of a sync response.""" + if response.text is None: + msg = "Response is missing text content." + raise MalformedDataError(msg) return response.text -@Processing.processor("application/json") # type: ignore[arg-type] -async def async_process_json(response: AsyncResponseType) -> dict[str, JSONType]: - """Returns the json dict content of the response.""" - try: - return cast(dict[str, JSONType], await response.json()) - except (json.JSONDecodeError, simplejson.JSONDecodeError) as err: - raise MalformedDataError( - f"Home Assistant responded with non-json response: {repr(await response.text())}" - ) from err +# --- MIME dispatch table --- + +_PARSERS: dict[str, Callable[[ResponseType], Any]] = { + "application/json": _parse_json, + "text/plain": _parse_text, + "application/octet-stream": _parse_text, +} + + +# --- Content dispatch --- + + +def _parse_content(response: ResponseType) -> Any: + """Look up and call the appropriate parser by content-type.""" + content_type = response.headers.get("content-type", "text/plain") + if isinstance(content_type, bytes): + content_type = content_type.decode("utf-8") # pragma: no cover + mimetype = str(content_type).split(";")[0].strip().lower() + + if (parser := _PARSERS.get(mimetype)) is None: + msg = f"No response processor found for mimetype {mimetype!r}." + raise ProcessorNotFoundError(msg) + return parser(response) + + +# --- Top-level entry points --- + + +def process_response(response: ResponseType) -> Any: + """Process a sync HTTP response: validate status, then parse content.""" + _check_sync_status(response) + return _parse_content(response) -@Processing.processor("text/plain") # type: ignore[arg-type] -@Processing.processor("application/octet-stream") # type: ignore[arg-type] -async def async_process_text(response: AsyncResponseType) -> str: - """Returns the plaintext of the reponse.""" - return await response.text() +async def async_process_response(response: ResponseType) -> Any: + """Process an async HTTP response: validate status, then parse content.""" + return process_response(response) diff --git a/homeassistant_api/rawasyncclient.py b/homeassistant_api/rawasyncclient.py deleted file mode 100644 index 18a9cc88..00000000 --- a/homeassistant_api/rawasyncclient.py +++ /dev/null @@ -1,420 +0,0 @@ -"""Module for interacting with Home Assistant asyncronously.""" - -from __future__ import annotations - -import asyncio -import json -import logging -from datetime import datetime -from posixpath import join -from typing import ( - TYPE_CHECKING, - Any, - AsyncGenerator, - Dict, - List, - Literal, - Optional, - Tuple, - Union, - cast, -) - -import aiohttp -import aiohttp_client_cache.session - -from .errors import BadTemplateError, RequestError, RequestTimeoutError -from .models import Domain, Entity, Event, Group, History, LogbookEntry, State -from .processing import AsyncResponseType, Processing -from .rawbaseclient import RawBaseClient -from .utils import JSONType, prepare_entity_id - -if TYPE_CHECKING: - from homeassistant_api import Client -else: - Client = None # pylint: disable=invalid-name - -logger = logging.getLogger(__name__) - - -class RawAsyncClient(RawBaseClient): - """ - The async equivalent of :py:class:`RawClient` - - :param api_url: The location of the api endpoint. e.g. :code:`http://localhost:8123/api` Required. - :param token: The refresh or long lived access token to authenticate your requests. Required. - :param global_request_kwargs: A dictionary or dict-like object of kwargs to pass to :func:`requests.request` or :meth:`aiohttp.request`. Optional. - """ # pylint: disable=line-too-long - - async_cache_session: Union[ - aiohttp_client_cache.session.CachedSession, aiohttp.ClientSession - ] - - def __init__( - self, - *args, - async_cache_session: Union[ - aiohttp_client_cache.session.CachedSession, - Literal[False], - Literal[None], - ] = None, # Explicitly disable cache with async_cache_session=False - verify_ssl: bool = True, - **kwargs, - ): - RawBaseClient.__init__(self, *args, **kwargs) - connector = aiohttp.TCPConnector(verify_ssl=False) if not verify_ssl else None - if async_cache_session is False: - self.async_cache_session = aiohttp.ClientSession(connector=connector) - elif async_cache_session is None: - self.async_cache_session = aiohttp_client_cache.CachedSession( # type: ignore[attr-defined] - cache=aiohttp_client_cache.CacheBackend( # type: ignore[attr-defined] - cache_name="default_async_cache", - expire_after=300, - ), - connector=connector, - ) - else: - self.async_cache_session = async_cache_session - - async def __aenter__(self): - logger.debug( - "Entering cached async requests session %r", self.async_cache_session - ) - await self.async_cache_session.__aenter__() - await self.async_check_api_running() - return self - - async def __aexit__(self, _, __, ___): - logger.debug("Exiting async requests session %r", self.async_cache_session) - await self.async_cache_session.close() - - # Very important request function - async def async_request( - self, - path: str, - *, - params: str = "", # should be a string of query parameters from construct_params() - method: str = "GET", - headers: Optional[Dict[str, str]] = None, - **kwargs, - ) -> Any: - """Base method for making requests to the api""" - try: - if self.global_request_kwargs is not None: - kwargs.update(self.global_request_kwargs) - return await self.async_response_logic( - await self.async_cache_session.request( - method, - self.endpoint(path) + f"?{params}" * bool(params), - headers=self.prepare_headers(headers), - **kwargs, - ) - ) - except asyncio.exceptions.TimeoutError as err: - raise RequestTimeoutError( - f"Home Assistant did not respond in time (timeout: {kwargs.get('timeout', 300)} sec)", - self.endpoint(path) + f"?{params}" * bool(params), - ) from err - - @staticmethod - async def async_response_logic(response: AsyncResponseType) -> Any: - """Processes custom mimetype content asyncronously.""" - return await Processing(response=response).process() - - # API information methods - async def async_get_error_log(self) -> str: - """ - Returns the server error log as a string. - :code:`GET /api/error_log` - """ - return cast(str, await self.async_request("error_log")) - - async def async_get_config(self) -> dict[str, JSONType]: - """ - Returns the yaml configuration of homeassistant. - :code:`GET /api/config` - """ - return cast(dict[str, JSONType], await self.async_request("config")) - - async def async_get_logbook_entries( - self, - *args, - **kwargs, - ) -> AsyncGenerator[LogbookEntry, None]: - """ - Returns a list of logbook entries from homeassistant. - :code:`GET /api/logbook/` - """ - params, url = self.prepare_get_logbook_entry_params(*args, **kwargs) - data = await self.async_request( - url, params=self.construct_params(cast(Dict[str, Optional[str]], params)) - ) - for entry in data: - yield LogbookEntry.model_validate(entry) - - async def async_get_entity_histories( - self, - entities: Optional[Tuple[Entity, ...]] = None, - start_timestamp: Optional[datetime] = None, - # Defaults to 1 day before. https://developers.home-assistant.io/docs/api/rest/ - end_timestamp: Optional[datetime] = None, - significant_changes_only: bool = False, - ) -> AsyncGenerator[History, None]: - """ - Returns a generator of entity state histories from homeassistant. - :code:`GET /api/history/period/` - """ - params, url = self.prepare_get_entity_histories_params( - entities=entities, - start_timestamp=start_timestamp, - end_timestamp=end_timestamp, - significant_changes_only=significant_changes_only, - ) - data = await self.async_request( - url, - params=self.construct_params(params), - ) - for states in data: - yield History.model_validate({"states": states}) - - async def async_get_rendered_template(self, template: str) -> str: - """ - Renders a given Jinja2 template string with Home Assistant context data. - :code:`POST /api/template` - """ - try: - return cast( - str, - await self.async_request( - "template", - json=dict(template=template), - method="POST", - ), - ) - except RequestError as err: - raise BadTemplateError( - "Your template is invalid. " - "Try debugging it in the developer tools page of homeassistant." - ) from err - - # API check methods - async def async_check_api_config(self) -> bool: - """ - Asks Home Assistant to validate its configuration file and returns true/false. - :code:`POST /api/config/core/check_config` - """ - res = await self.async_request("config/core/check_config", method="POST") - res = cast(Dict[Any, Any], res) - valid = {"valid": True, "invalid": False}.get( - cast( - str, - res["result"], - ), - False, - ) - return valid - - async def async_check_api_running(self) -> bool: - """ - Asks Home Assistant if its running. - :code:`GET /api/` - """ - res = cast(Dict[Any, Any], await self.async_request("")) - return res.get("message") == "API running." - - # Entity methods - async def async_get_entities(self) -> Dict[str, Group]: - """ - Fetches all entities from the api. - :code:`GET /api/states` - """ - entities: Dict[str, Group] = {} - for state in await self.async_get_states(): - group_id, entity_slug = state.entity_id.split(".") - if group_id not in entities: - entities[group_id] = Group(group_id=group_id, _client=self) # type: ignore[arg-type] - entities[group_id]._add_entity(entity_slug, state) - return entities - - async def async_get_entity( - self, - group_id: Optional[str] = None, - slug: Optional[str] = None, - entity_id: Optional[str] = None, - ) -> Optional[Entity]: - """ - Returns a Entity model for an :code:`entity_id`. - :code:`GET /api/states/` - """ - if group_id is not None and slug is not None: - state = await self.async_get_state(group_id=group_id, slug=slug) - elif entity_id is not None: - state = await self.async_get_state(entity_id=entity_id) - else: - help_msg = ( - "Use keyword arguments to pass entity_id. " - "Or you can pass the group_id and slug instead." - ) - raise ValueError( - f"Neither group_id and slug or entity_id provided. {help_msg}" - ) - group_id, entity_slug = state.entity_id.split(".") - group = Group(group_id=group_id, _client=self) # type: ignore[arg-type] - group._add_entity(entity_slug, state) - return group.get_entity(entity_slug) - - # Services and domain methods - async def async_get_domains(self) -> Dict[str, Domain]: - """ - Fetches all :py:class:`Service` 's from the API. - :code:`GET /api/services` - """ - data = await self.async_request("services") - domains = map( - lambda json: Domain.from_json_with_client(json, client=cast(Client, self)), - cast(Tuple[dict[str, JSONType], ...], data), - ) - return {domain.domain_id: domain for domain in domains} - - async def async_get_domain(self, domain_id: str) -> Optional[Domain]: - """ - Fetches all :py:class:`Service`'s under a particular service :py:class:`Domain`. - Uses cached data from :py:meth:`get_domains` if available. - """ - domains = await self.async_get_domains() - return domains.get(domain_id) - - async def async_trigger_service( - self, - domain: str, - service: str, - **service_data: Union[dict[str, JSONType], List[Any], str], - ) -> Tuple[State, ...]: - """ - Tells Home Assistant to trigger a service, returns all states changed while in the process of being called. - :code:`POST /api/services//` - """ - data = await self.async_request( - f"services/{domain}/{service}", - method="POST", - json=service_data, - ) - return tuple(map(State.from_json, cast(List[Dict[Any, Any]], data))) - - async def async_trigger_service_with_response( - self, - domain: str, - service: str, - **service_data: Union[dict[str, JSONType], List[Any], str], - ) -> tuple[tuple[State, ...], dict[str, JSONType]]: - """ - Tells Home Assistant to trigger a service, returns the response from the service call. - :code:`POST /api/services//` - - Returns a list of the states changed and the response from the service call. - """ - data = cast( - dict[str, dict[str, JSONType]], - await self.async_request( - join("services", domain, service) + "?return_response", - method="POST", - json=service_data, - ), - ) - states = tuple( - map( - State.from_json, - cast(List[Dict[Any, Any]], data.get("changed_states", [])), - ) - ) - return states, data.get("service_response", {}) - - # EntityState methods - async def async_get_state( # pylint: disable=duplicate-code - self, - *, - entity_id: Optional[str] = None, - group_id: Optional[str] = None, - slug: Optional[str] = None, - ) -> State: - """ - Fetches the state of the entity specified. - :code:`GET /api/states/` - """ - target_entity_id = prepare_entity_id( - group_id=group_id, - slug=slug, - entity_id=entity_id, - ) - data = await self.async_request(join("states", target_entity_id)) - return State.from_json(cast(Dict[Any, Any], data)) - - async def async_set_state( # pylint: disable=duplicate-code - self, - state: State, - ) -> State: - """ - This method sets the representation of a device within Home Assistant and will not communicate with the actual device. - To communicate with the device, use :py:meth:`Service.trigger` or :py:meth:`Service.async_trigger`. - :code:`POST /api/states/` - """ - data = await self.async_request( - join("states", state.entity_id), - method="POST", - json=json.loads(state.model_dump_json()), - ) - return State.from_json(cast(Dict[Any, Any], data)) - - async def async_get_states(self) -> Tuple[State, ...]: - """ - Gets the states of all entities within homeassistant. - :code:`GET /api/states` - """ - data = await self.async_request("states") - return tuple(map(State.from_json, cast(List[Dict[Any, Any]], data))) - - # Event methods - async def async_get_events(self) -> Tuple[Event, ...]: - """ - Gets the Events that happen within homeassistant - :code:`GET /api/events` - """ - data = await self.async_request("events") - return tuple( - map( - lambda json: Event.from_json_with_client( - json, client=cast(Client, self) - ), - cast(List[dict[str, JSONType]], data), - ) - ) - - async def async_get_event(self, name: str) -> Optional[Event]: - """ - Gets the :py:class:`Event` with the specified name if it has at least one listener. - Uses cached data from :py:meth:`get_events` if available. - """ - for event in await self.async_get_events(): - if event.event == name.strip().lower(): - return event - return None - - async def async_fire_event(self, event_type: str, **event_data: Any) -> str: - """ - Fires a given event_type within homeassistant. Must be an existing event_type. - :code:`POST /api/events/` - """ - data = await self.async_request( - join("events", event_type), - method="POST", - json=event_data, - ) - return cast(str, data.get("message", "No message provided")) - - async def async_get_components(self) -> Tuple[str, ...]: - """ - Returns a tuple of all registered components. - :code:`GET /api/components` - """ - data = await self.async_request("components") - return tuple(cast(List[str], data)) diff --git a/homeassistant_api/rawasyncwebsocket.py b/homeassistant_api/rawasyncwebsocket.py deleted file mode 100644 index 6c99ff6d..00000000 --- a/homeassistant_api/rawasyncwebsocket.py +++ /dev/null @@ -1,667 +0,0 @@ -import contextlib -import json -import logging -import time -from typing import ( - TYPE_CHECKING, - Any, - AsyncGenerator, - Dict, - Optional, - Tuple, - Union, - cast, -) - -import websockets.asyncio.client as ws -from pydantic import ValidationError - -from homeassistant_api.errors import ( - ReceivingError, - ResponseError, - UnauthorizedError, -) -from homeassistant_api.models import ( - ConfigEntry, - ConfigEntryEvent, - ConfigSubEntry, - Domain, - Entity, - Group, - State, -) -from homeassistant_api.models.config_entries import DisableEnableResult, FlowResult -from homeassistant_api.models.states import Context -from homeassistant_api.models.websocket import ( - AuthInvalid, - AuthOk, - AuthRequired, - EventResponse, - FiredEvent, - FiredTrigger, - PingResponse, - ResultResponse, - TemplateEvent, -) -from homeassistant_api.rawbasewebsocket import RawBaseWebsocketClient -from homeassistant_api.utils import JSONType, prepare_entity_id - -if TYPE_CHECKING: - from homeassistant_api import WebsocketClient -else: - WebsocketClient = None # pylint: disable=invalid-name - -logger = logging.getLogger(__name__) - - -class RawAsyncWebsocketClient(RawBaseWebsocketClient): - _async_conn: Optional[ws.ClientConnection] - - def __init__(self, api_url: str, token: str) -> None: - super().__init__(api_url, token) - self._async_conn = None - - async def __aenter__(self): - self._async_conn = await ws.connect(self.api_url) - await self._async_conn.__aenter__() - okay = await self.async_authentication_phase() - logging.info("Authenticated with Home Assistant (%s)", okay.ha_version) - await self.async_supported_features_phase() - return self - - async def __aexit__(self, exc_type, exc_value, traceback): - if not self._async_conn: - raise ReceivingError("Connection is not open!") - await self._async_conn.__aexit__(exc_type, exc_value, traceback) - self._async_conn = None - - async def _async_send(self, data: dict[str, JSONType]) -> None: - """Send a message to the websocket server.""" - logger.debug(f"Sending message: {data}") - if self._async_conn is None: - raise ReceivingError("Connection is not open!") - await self._async_conn.send(json.dumps(data)) - - async def _async_recv(self) -> dict[str, JSONType]: - """Receive a message from the websocket server.""" - if self._async_conn is None: - raise ReceivingError("Connection is not open!") - _bytes = await self._async_conn.recv() - logger.debug("Received message: %s", _bytes) - return cast(dict[str, JSONType], json.loads(_bytes)) - - async def async_send(self, type: str, include_id: bool = True, **data: Any) -> int: - """ - Send a command message to the websocket server and wait for a "result" response. - - Returns the id of the message sent. - """ - if include_id: # auth messages don't have an id - data["id"] = self._request_id() - - data["type"] = type - await self._async_send(data) - - if "id" in data: - assert isinstance(data["id"], int) - if data["type"] == "ping": - self._ping_responses[data["id"]] = PingResponse( - start=time.perf_counter_ns(), - id=data["id"], - type="pong", - ) - else: - self._event_responses[data["id"]] = [] - self._result_responses[data["id"]] = None - return data["id"] - return -1 # non-command messages don't have an id - - async def async_recv( - self, id: int - ) -> Union[EventResponse, ResultResponse, PingResponse]: - """Receive a response to a message from the websocket server.""" - while True: - ## have we received a message with the id we're looking for? - if self._result_responses.get(id) is not None: - return cast(dict[int, ResultResponse], self._result_responses).pop( - id - ) # ughhh why can't mypy figure this out - if self._event_responses.get(id, []): - return self._event_responses[id].pop(0) - if self._ping_responses.get(id) is not None: - if self._ping_responses[id].end is not None: - return self._ping_responses.pop(id) - - ## if not, keep receiving messages until we do - self.handle_recv(await self._async_recv()) - - async def async_authentication_phase(self) -> AuthOk: - """Authenticate with the websocket server.""" - # Capture the first message from the server saying we need to authenticate - try: - welcome = AuthRequired.model_validate(await self._async_recv()) - logger.debug(f"Received welcome message: {welcome}") - except ValidationError as e: - raise ResponseError("Unexpected response during authentication") from e - - # Send our authentication token - await self.async_send("auth", access_token=self.token, include_id=False) - logger.debug("Sent auth message") - - # Check the response - resp = await self._async_recv() - try: - return AuthOk.model_validate(resp) - except ValidationError as e: - error_resp = AuthInvalid.model_validate(resp) - raise UnauthorizedError(error_resp.message) from e - except Exception as e: - raise ResponseError( - "Unexpected response during authentication", resp["message"] - ) from e - - async def async_supported_features_phase(self) -> None: - """Get the supported features from the websocket server.""" - resp = await self.async_recv( - await self.async_send( - "supported_features", - features={ - # "coalesce_messages": 42, # including this key sets it to True - }, - ) - ) - assert cast(ResultResponse, resp).result is None - - async def async_ping_latency(self) -> float: - """Get the latency (in milliseconds) of the connection by sending a ping message.""" - pong = cast(PingResponse, await self.async_recv(await self.async_send("ping"))) - assert pong.end is not None - return (pong.end - pong.start) / 1_000_000 - - async def async_get_rendered_template(self, template: str) -> str: - """ - Renders a Jinja2 template with Home Assistant context data. - See https://www.home-assistant.io/docs/configuration/templating. - - Sends command :code:`{"type": "render_template", ...}`. - """ - id = await self.async_send( - "render_template", template=template, report_errors=True - ) - first = await self.async_recv(id) - assert cast(ResultResponse, first).result is None - second = await self.async_recv(id) - await self._async_unsubscribe(id) - return cast(TemplateEvent, cast(EventResponse, second).event).result - - async def async_get_config(self) -> dict[str, JSONType]: - """ - Get the Home Assistant configuration. - - Sends command :code:`{"type": "get_config", ...}`. - """ - return cast( - dict[str, JSONType], - cast( - ResultResponse, - await self.async_recv(await self.async_send("get_config")), - ).result, - ) - - async def async_get_states(self) -> Tuple[State, ...]: - """ - Get a list of states. - - Sends command :code:`{"type": "get_states", ...}`. - """ - return tuple( - State.from_json(state) - for state in cast( - list[dict[str, JSONType]], - cast( - ResultResponse, - await self.async_recv(await self.async_send("get_states")), - ).result, - ) - ) - - async def async_get_state( # pylint: disable=duplicate-code - self, - *, - entity_id: Optional[str] = None, - group_id: Optional[str] = None, - slug: Optional[str] = None, - ) -> State: - """ - Just calls the :py:meth:`get_states` method and filters the result. - - Please tell home-assistant/core to add a :code:`{"type": "get_state", ...}` command to the WS API! - There is a lot of disappointment and frustration in the community because this is not available. - """ - entity_id = prepare_entity_id( - group_id=group_id, - slug=slug, - entity_id=entity_id, - ) - - for state in await self.async_get_states(): - if state.entity_id == entity_id: - return state - raise ValueError(f"Entity {entity_id} not found!") - - async def async_get_entities(self) -> Dict[str, Group]: - """ - Fetches all entities from the Websocket API and returns them as a dictionary of :py:class:`Group`'s. - For example :code:`light.living_room` would be in the group :code:`light` (i.e. :code:`get_entities()["light"].living_room`). - """ - entities: Dict[str, Group] = {} - for state in await self.async_get_states(): - group_id, entity_slug = state.entity_id.split(".") - if group_id not in entities: - entities[group_id] = Group( - group_id=group_id, - _client=self, # type: ignore[arg-type] - ) - entities[group_id]._add_entity(entity_slug, state) - return entities - - async def async_get_entity( - self, - group_id: Optional[str] = None, - slug: Optional[str] = None, - entity_id: Optional[str] = None, - ) -> Optional[Entity]: - """ - Returns an :py:class:`Entity` model for an :code:`entity_id`. - - Calls :py:meth:`get_states` under the hood. - - Please tell home-assistant/core to add a :code:`{"type": "get_state", ...}` command to the WS API! - There is a lot of disappointment and frustration in the community because this is not available. - """ - if group_id is not None and slug is not None: - state = await self.async_get_state(group_id=group_id, slug=slug) - elif entity_id is not None: - state = await self.async_get_state(entity_id=entity_id) - else: - help_msg = ( - "Use keyword arguments to pass entity_id. " - "Or you can pass the group_id and slug instead" - ) - raise ValueError( - f"Neither group_id and slug or entity_id provided. {help_msg}" - ) - split_group_id, split_slug = state.entity_id.split(".") - group = Group( - group_id=split_group_id, - _client=self, # type: ignore[arg-type] - ) - group._add_entity(split_slug, state) - return group.get_entity(split_slug) - - async def async_get_domains(self) -> dict[str, Domain]: - """ - Get a list of services that Home Assistant offers (organized into a dictionary of service domains). - - For example, the service :code:`light.turn_on` would be in the domain :code:`light`. - - Sends command :code:`{"type": "get_services", ...}`. - """ - resp = await self.async_recv(await self.async_send("get_services")) - domains = map( - lambda item: Domain.from_json_with_client( - {"domain": item[0], "services": item[1]}, - client=cast(WebsocketClient, self), - ), - cast(dict[str, JSONType], cast(ResultResponse, resp).result).items(), - ) - return {domain.domain_id: domain for domain in domains} - - async def async_get_domain(self, domain: str) -> Domain: - """Get a domain. - - Note: This is not a method in the WS API client... yet. - - Please tell home-assistant/core to add a `get_domain` command to the WS API! - - For now, just call the :py:meth":`get_domains` method and parsing the result. - """ - return (await self.async_get_domains())[domain] - - async def async_trigger_service( - self, - domain: str, - service: str, - entity_id: Optional[str] = None, - **service_data, - ) -> None: - """ - Trigger a service (that doesn't return a response). - - Sends command :code:`{"type": "call_service", ...}`. - """ - params = { - "domain": domain, - "service": service, - "service_data": service_data, - "return_response": False, - } - if entity_id is not None: - params["target"] = {"entity_id": entity_id} - - data = await self.async_recv( - await self.async_send("call_service", include_id=True, **params) - ) - - # TODO: handle data["result"]["context"] ? - - assert ( - cast( - dict[str, JSONType], - cast(ResultResponse, data).result, - ).get("response") - is None - ) # should always be None for services without a response - - async def async_trigger_service_with_response( - self, - domain: str, - service: str, - entity_id: Optional[str] = None, - **service_data, - ) -> dict[str, JSONType]: - """ - Trigger a service (that returns a response) and return the response. - - Sends command :code:`{"type": "call_service", ...}`. - """ - params = { - "domain": domain, - "service": service, - "service_data": service_data, - "return_response": True, - } - if entity_id is not None: - params["target"] = {"entity_id": entity_id} - - data = await self.async_recv( - await self.async_send("call_service", include_id=True, **params) - ) - - return cast(dict[str, dict[str, JSONType]], cast(ResultResponse, data).result)[ - "response" - ] - - @contextlib.asynccontextmanager - async def async_listen_events( - self, - event_type: Optional[str] = None, - ) -> AsyncGenerator[AsyncGenerator[FiredEvent, None], None]: - """ - Listen for all events of a certain type. - - For example, to listen for all events of type `test_event`: - - .. code-block:: python - - async with ws_client.listen_events("test_event") as events: - async for i, event in zip(range(2), events): # to only wait for two events to be received - print(event) - """ - subscription = await self._async_subscribe_events(event_type) - yield cast(AsyncGenerator[FiredEvent, None], self._async_wait_for(subscription)) - await self._async_unsubscribe(subscription) - - async def _async_subscribe_events(self, event_type: Optional[str]) -> int: - """ - Subscribe to all events of a certain type. - - - Sends command :code:`{"type": "subscribe_events", ...}`. - """ - params = {"event_type": event_type} if event_type else {} - return ( - await self.async_recv( - await self.async_send("subscribe_events", include_id=True, **params) - ) - ).id - - @contextlib.asynccontextmanager - async def async_listen_trigger( - self, trigger: str, **trigger_fields - ) -> AsyncGenerator[AsyncGenerator[dict[str, JSONType], None], None]: - """ - Listen to a Home Assistant trigger. - Allows additional trigger keyword parameters with :code:`**kwargs` (i.e. passing :code:`tag_id=...` for NFC tag triggers). - - For example, in Home Assistant Automations we can subscribe to a state trigger for a light entity with YAML: - - .. code-block:: yaml - - triggers: - # ... - - trigger: state - entity_id: light.kitchen - - To subscribe to that same state trigger with :py:class:`AsyncWebsocketClient` instead - - .. code-block:: python - - async with ws_client.listen_trigger("state", entity_id="light.kitchen") as trigger: - async for event in trigger: # will iterate until we manually break out of the loop - print(event) - if : - break - # exiting the context manager unsubscribes from the trigger - - Woohoo! We can now listen to triggers in Python code! - """ - subscription = await self._async_subscribe_trigger(trigger, **trigger_fields) - yield ( - fired_trigger.variables - async for fired_trigger in cast( - AsyncGenerator[FiredTrigger, None], - self._async_wait_for(subscription), - ) - ) - await self._async_unsubscribe(subscription) - - async def _async_subscribe_trigger(self, trigger: str, **trigger_fields) -> int: - """ - Return the subscription id of the trigger we subscribe to. - - Sends command :code:`{"type": "subscribe_trigger", ...}`. - """ - return ( - await self.async_recv( - await self.async_send( - "subscribe_trigger", trigger={"platform": trigger, **trigger_fields} - ) - ) - ).id - - async def _async_wait_for( - self, subscription_id: int - ) -> AsyncGenerator[Union[FiredEvent, FiredTrigger], None]: - """ - An iterator that waits for events of a certain type. - """ - while True: - yield cast( - Union[ - FiredEvent, FiredTrigger - ], # we can cast this because TemplateEvent is only used for rendering templates - cast(EventResponse, await self.async_recv(subscription_id)).event, - ) - - async def _async_unsubscribe(self, subcription_id: int) -> None: - """ - Unsubscribe from all events of a certain type. - - Sends command :code:`{"type": "unsubscribe_events", ...}`. - """ - resp = await self.async_recv( - await self.async_send("unsubscribe_events", subscription=subcription_id) - ) - assert cast(ResultResponse, resp).result is None - self._event_responses.pop(subcription_id) - - async def async_get_config_entries(self) -> Tuple[ConfigEntry, ...]: - """ - Get all config entries. - - Sends command :code:`{"type": "config_entries/get", ...}`. - """ - resp = await self.async_recv(await self.async_send("config_entries/get")) - return tuple( - ConfigEntry.from_json(entry) - for entry in cast( - list[dict[str, JSONType]], - cast(ResultResponse, resp).result, - ) - ) - - async def async_disable_config_entry(self, entry_id: str) -> DisableEnableResult: - """ - Disable a config entry. - - Sends command :code:`{"type": "config_entries/disable", ...}`. - """ - resp = await self.async_recv( - await self.async_send( - "config_entries/disable", - entry_id=entry_id, - disabled_by="user", - ) - ) - return DisableEnableResult.from_json( - cast(dict[str, JSONType], cast(ResultResponse, resp).result) - ) - - async def async_enable_config_entry(self, entry_id: str) -> DisableEnableResult: - """ - Enable a config entry. - - Sends command :code:`{"type": "config_entries/disable", ...}`. - """ - resp = await self.async_recv( - await self.async_send( - "config_entries/disable", - entry_id=entry_id, - disabled_by=None, - ) - ) - return DisableEnableResult.from_json( - cast(dict[str, JSONType], cast(ResultResponse, resp).result) - ) - - async def async_ignore_config_flow(self, flow_id: str, title: str) -> None: - """ - Ignore a config flow. - - Sends command :code:`{"type": "config_entries/ignore_flow", ...}`. - """ - await self.async_recv( - await self.async_send( - "config_entries/ignore_flow", - flow_id=flow_id, - title=title, - ) - ) - - async def async_get_nonuser_flows_in_progress(self) -> Tuple[FlowResult, ...]: - """ - Get non-user config flows in progress. - - Sends command :code:`{"type": "config_entries/flow/progress", ...}`. - """ - resp = await self.async_recv( - await self.async_send("config_entries/flow/progress") - ) - return tuple( - FlowResult.from_json(flow) - for flow in cast( - list[dict[str, JSONType]], - cast(ResultResponse, resp).result, - ) - ) - - async def async_get_entry_subentries( - self, entry_id: str - ) -> Tuple[ConfigSubEntry, ...]: - """ - Get subentries for a config entry. - - Sends command :code:`{"type": "config_entries/subentries/list", ...}`. - """ - resp = await self.async_recv( - await self.async_send("config_entries/subentries/list", entry_id=entry_id) - ) - return tuple( - ConfigSubEntry.from_json(subentry) - for subentry in cast( - list[dict[str, JSONType]], - cast(ResultResponse, resp).result, - ) - ) - - async def async_delete_entry_subentry( - self, entry_id: str, subentry_id: str - ) -> None: - """ - Delete a subentry from a config entry. - - Sends command :code:`{"type": "config_entries/subentries/delete", ...}`. - """ - await self.async_recv( - await self.async_send( - "config_entries/subentries/delete", - entry_id=entry_id, - subentry_id=subentry_id, - ) - ) - - @contextlib.asynccontextmanager - async def async_listen_config_entries( - self, - ) -> AsyncGenerator[AsyncGenerator[list[ConfigEntryEvent], None], None]: - """ - Listen for config entry changes. - - Sends command :code:`{"type": "config_entries/subscribe", ...}`. - """ - subscription = ( - await self.async_recv(await self.async_send("config_entries/subscribe")) - ).id - yield self._async_wait_for_config_entries(subscription) - await self._async_unsubscribe(subscription) - - async def _async_wait_for_config_entries( - self, subscription_id: int - ) -> AsyncGenerator[list[ConfigEntryEvent], None]: - """An async iterator that waits for config entry events.""" - while True: - event_resp = cast(EventResponse, await self.async_recv(subscription_id)) - entries = cast(list[dict[str, JSONType]], event_resp.event) - yield [ConfigEntryEvent.from_json(entry) for entry in entries] - - async def async_fire_event(self, event_type: str, **event_data) -> Context: - """ - Fire an event. - - Sends command :code:`{"type": "fire_event", ...}`. - """ - params: dict[str, JSONType] = {"event_type": event_type} - if event_data: - params["event_data"] = event_data - return Context.from_json( - cast( - dict[str, dict[str, JSONType]], - cast( - ResultResponse, - await self.async_recv( - await self.async_send("fire_event", include_id=True, **params) - ), - ).result, - )["context"] - ) diff --git a/homeassistant_api/rawbasewebsocket.py b/homeassistant_api/rawbasewebsocket.py deleted file mode 100644 index f959d502..00000000 --- a/homeassistant_api/rawbasewebsocket.py +++ /dev/null @@ -1,82 +0,0 @@ -import logging -import time -from typing import Optional, cast - -from pydantic import ValidationError - -from homeassistant_api.errors import ( - ReceivingError, - RequestError, -) -from homeassistant_api.models.websocket import ( - ErrorResponse, - EventResponse, - PingResponse, - ResultResponse, -) -from homeassistant_api.utils import JSONType - -logger = logging.getLogger(__name__) - - -class RawBaseWebsocketClient: - """Shared methods for Websocket clients.""" - - api_url: str - token: str - _id_counter: int - _result_responses: dict[int, Optional[ResultResponse]] - _event_responses: dict[int, list[EventResponse]] - _ping_responses: dict[int, PingResponse] - - def __init__(self, api_url: str, token: str) -> None: - self.api_url = api_url - self.token = token.strip() - - self._id_counter = 0 - self._result_responses = {} # id -> response - self._event_responses = {} # id -> [response, ...] - self._ping_responses = {} # id -> (sent, received) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.api_url!r})" - - def _request_id(self) -> int: - """Get a unique id for a message.""" - self._id_counter += 1 - return self._id_counter - - def check_success(self, data: dict[str, JSONType]) -> None: - """Check if a command message was successful.""" - try: - error_resp = ErrorResponse.model_validate(data) - raise RequestError(error_resp.error.code, error_resp.error.message) - except ValidationError: - pass - - def handle_recv(self, data: dict[str, JSONType]) -> None: - """Handle a received message.""" - if "id" not in data: - raise ReceivingError( - "Received a message without an id outside the auth phase." - ) - self.check_success(data) - self.parse_response(data) - - def parse_response(self, data: dict[str, JSONType]) -> None: - data_id = cast(int, data["id"]) - if data.get("type") == "pong": - logger.info("Received pong message") - self._ping_responses[data_id].end = time.perf_counter_ns() - elif data.get("type") == "result": - logger.info("Received result message") - if data.get("success"): - self._result_responses[data_id] = ResultResponse.model_validate(data) - else: - error_resp = ErrorResponse.model_validate(data) - raise RequestError(error_resp.error.code, error_resp.error.message) - elif data.get("type") == "event": - logger.info("Received event message %s", data["event"]) - self._event_responses[data_id].append(EventResponse.model_validate(data)) - else: - raise ReceivingError(f"Received unexpected message type: {data}") diff --git a/homeassistant_api/rawclient.py b/homeassistant_api/rawclient.py deleted file mode 100644 index afe8e64f..00000000 --- a/homeassistant_api/rawclient.py +++ /dev/null @@ -1,423 +0,0 @@ -"""Module for all interaction with homeassistant.""" - -from __future__ import annotations - -import json -import logging -from datetime import datetime -from posixpath import join -from typing import ( - TYPE_CHECKING, - Any, - Dict, - Generator, - List, - Literal, - Optional, - Tuple, - Union, - cast, -) - -import requests -import requests_cache - -from homeassistant_api.errors import BadTemplateError, RequestError, RequestTimeoutError -from homeassistant_api.models import ( - Domain, - Entity, - Event, - Group, - History, - LogbookEntry, - State, -) -from homeassistant_api.processing import Processing, ResponseType -from homeassistant_api.rawbaseclient import RawBaseClient -from homeassistant_api.utils import JSONType, prepare_entity_id - -if TYPE_CHECKING: - from homeassistant_api import Client -else: - Client = None # pylint: disable=invalid-name - - -logger = logging.getLogger(__name__) - - -class RawClient(RawBaseClient): - """ - The base object for interacting with Homeassistant via the REST API. - - :param api_url: The location of the api endpoint. e.g. :code:`http://localhost:8123/api` Required. - :param token: The refresh or long lived access token to authenticate your requests. Required. - :param global_request_kwargs: Kwargs to pass to :func:`requests.request` or :meth:`aiohttp.ClientSession.request`. Optional. - """ # pylint: disable=line-too-long - - cache_session: Union[requests_cache.CachedSession, requests.Session] - - def __init__( - self, - *args, - cache_session: Union[ - requests_cache.CachedSession, - Literal[False], - Literal[None], - ] = None, # Explicitly disable cache with cache_session=False - verify_ssl: bool = True, - **kwargs, - ): - RawBaseClient.__init__(self, *args, **kwargs) - self.global_request_kwargs["verify"] = verify_ssl - if cache_session is False: - self.cache_session = requests.Session() - elif cache_session is None: - self.cache_session = requests_cache.CachedSession( - cache_name="default_cache", - backend="memory", - expire_after=300, - ) - else: - self.cache_session = cache_session - - def __enter__(self) -> "RawClient": - logger.debug("Entering cached requests session %r.", self.cache_session) - self.cache_session.__enter__() - self.check_api_running() - return self - - def __exit__(self, _, __, ___) -> None: - logger.debug("Exiting requests session %r", self.cache_session) - self.cache_session.close() - - def request( - self, - path: str, - *, - params: str = "", # should be a string of query parameters from construct_params() - method="GET", - headers: Optional[Dict[str, str]] = None, - decode_bytes: bool = True, - **kwargs, - ) -> Any: - """Base method for making requests to the api""" - try: - if self.global_request_kwargs is not None: - kwargs.update(self.global_request_kwargs) - logger.debug("%s request to %s", method, self.endpoint(path)) - resp = self.cache_session.request( - method, - self.endpoint(path) + f"?{params}" * bool(params), - headers=self.prepare_headers(headers), - **kwargs, - ) - except requests.exceptions.Timeout as err: - raise RequestTimeoutError( - f"Home Assistant did not respond in time (timeout: {kwargs.get('timeout', 300)} sec)", - url=self.endpoint(path) + f"?{params}" * bool(params), - ) from err - return self.response_logic(response=resp, decode_bytes=decode_bytes) - - @classmethod - def response_logic(cls, response: ResponseType, decode_bytes: bool = True) -> Any: - """Processes responses from the API and formats them""" - return Processing(response=response, decode_bytes=decode_bytes).process() - - # API information methods - def get_error_log(self) -> str: - """ - Returns the server error log as a string. - :code:`GET /api/error_log` - """ - return cast(str, self.request("error_log")) - - def get_config(self) -> dict[str, JSONType]: - """ - Returns the yaml configuration of homeassistant. - :code:`GET /api/config` - """ - return cast(dict[str, JSONType], self.request("config")) - - def get_logbook_entries( - self, - *args, - **kwargs, - ) -> Generator[LogbookEntry, None, None]: - """ - Returns a list of logbook entries from homeassistant. - :code:`GET /api/logbook/` - """ - params, url = self.prepare_get_logbook_entry_params(*args, **kwargs) - data = self.request( - url, params=self.construct_params(cast(Dict[str, Optional[str]], params)) - ) - for entry in data: - yield LogbookEntry.model_validate(entry) - - def get_entity_histories( - self, - entities: Optional[Tuple[Entity, ...]] = None, - start_timestamp: Optional[datetime] = None, - # Defaults to 1 day before. https://developers.home-assistant.io/docs/api/rest/ - end_timestamp: Optional[datetime] = None, - significant_changes_only: bool = False, - ) -> Generator[History, None, None]: - """ - Yields entity state histories. See docs on the :py:class:`History` model. - :code:`GET /api/history/period/` - """ - params, url = self.prepare_get_entity_histories_params( - entities=entities, - start_timestamp=start_timestamp, - end_timestamp=end_timestamp, - significant_changes_only=significant_changes_only, - ) - data = self.request( - url, - params=self.construct_params(params), - ) - for states in data: - yield History.model_validate({"states": states}) - - def get_rendered_template(self, template: str) -> str: - """ - Renders a Jinja2 template with Home Assistant context data. - See https://www.home-assistant.io/docs/configuration/templating. - :code:`POST /api/template` - """ - try: - return cast( - str, - self.request( - "template", - json=dict(template=template), - method="POST", - ), - ) - except RequestError as err: - raise BadTemplateError( - "Your template is invalid. " - "Try debugging it in the developer tools page of homeassistant." - ) from err - - # API check methods - def check_api_config(self) -> bool: - """ - Asks Home Assistant to validate its configuration file. - :code:`POST /api/config/core/check_config` - """ - res = cast( - dict[str, str], self.request("config/core/check_config", method="POST") - ) - valid = {"valid": True, "invalid": False}.get(res["result"], False) - return valid - - def check_api_running(self) -> bool: - """ - Asks Home Assistant if it is running. - :code:`GET /api/` - """ - res = self.request("") - return cast(dict[str, JSONType], res).get("message") == "API running." - - # Entity methods - def get_entities(self) -> Dict[str, Group]: - """ - Fetches all entities from the api and returns them as a dictionary of :py:class:`Group`'s. - :code:`GET /api/states` - """ - entities: Dict[str, Group] = {} - for state in self.get_states(): - group_id, entity_slug = state.entity_id.split(".") - if group_id not in entities: - entities[group_id] = Group( - group_id=group_id, - _client=self, # type: ignore[arg-type] - ) - entities[group_id]._add_entity(entity_slug, state) - return entities - - def get_entity( - self, - group_id: Optional[str] = None, - slug: Optional[str] = None, - entity_id: Optional[str] = None, - ) -> Optional[Entity]: - """ - Returns an :py:class:`Entity` model for an :code:`entity_id`. - :code:`GET /api/states/` - """ - if group_id is not None and slug is not None: - state = self.get_state(group_id=group_id, slug=slug) - elif entity_id is not None: - state = self.get_state(entity_id=entity_id) - else: - help_msg = ( - "Use keyword arguments to pass entity_id. " - "Or you can pass the group_id and slug instead" - ) - raise ValueError( - f"Neither group_id and slug or entity_id provided. {help_msg}" - ) - split_group_id, split_slug = state.entity_id.split(".") - group = Group( - group_id=split_group_id, - _client=self, # type: ignore[arg-type] - ) - group._add_entity(split_slug, state) - return group.get_entity(split_slug) - - # Services and domain methods - def get_domains(self) -> Dict[str, Domain]: - """ - Fetches all :py:class:`Service` 's from the API. - :code:`GET /api/services` - """ - data = self.request("services") - domains = map( - lambda json: Domain.from_json_with_client(json, client=cast(Client, self)), - cast(Tuple[dict[str, JSONType], ...], data), - ) - return {domain.domain_id: domain for domain in domains} - - def get_domain(self, domain_id: str) -> Optional[Domain]: - """ - Fetches all :py:class:`Service`'s under a particular service :py:class:`Domain`. - Uses cached data from :py:meth:`get_domains` if available. - """ - return self.get_domains().get(domain_id) - - def trigger_service( - self, - domain: str, - service: str, - **service_data, - ) -> Tuple[State, ...]: - """ - Tells Home Assistant to trigger a service, returns all states changed while in the process of being called. - :code:`POST /api/services//` - """ - data = self.request( - join("services", domain, service), - method="POST", - json=service_data, - ) - return tuple(map(State.from_json, cast(List[dict[str, JSONType]], data))) - - def trigger_service_with_response( - self, - domain: str, - service: str, - **service_data, - ) -> tuple[tuple[State, ...], dict[str, JSONType]]: - """ - Tells Home Assistant to trigger a service, returns the response from the service call. - :code:`POST /api/services//` - - Returns a list of the states changed and the response from the service call. - """ - data = cast( - dict[str, dict[str, JSONType]], - self.request( - join("services", domain, service) + "?return_response", - method="POST", - json=service_data, - ), - ) - states = tuple( - map( - State.from_json, - cast(List[Dict[Any, Any]], data.get("changed_states", [])), - ) - ) - return states, data.get("service_response", {}) - - # EntityState methods - def get_state( # pylint: disable=duplicate-code - self, - *, - entity_id: Optional[str] = None, - group_id: Optional[str] = None, - slug: Optional[str] = None, - ) -> State: - """ - Fetches the state of the entity specified. - :code:`GET /api/states/` - """ - entity_id = prepare_entity_id( - group_id=group_id, - slug=slug, - entity_id=entity_id, - ) - data = self.request(join("states", entity_id)) - return State.from_json(cast(dict[str, JSONType], data)) - - def set_state( # pylint: disable=duplicate-code - self, - state: State, - ) -> State: - """ - This method sets the representation of a device within Home Assistant and will not communicate with the actual device. - To communicate with the device, use :py:meth:`Service.trigger` or :py:meth:`Service.async_trigger`. - :code:`POST /api/states/` - """ - data = self.request( - join("states", state.entity_id), - method="POST", - json=json.loads(state.model_dump_json()), - ) - return State.from_json(cast(dict[str, JSONType], data)) - - def get_states(self) -> Tuple[State, ...]: - """ - Gets the states of all entities within homeassistant. - :code:`GET /api/states` - """ - data = self.request("states") - states = map(State.from_json, cast(List[dict[str, JSONType]], data)) - return tuple(states) - - # Event methods - def get_events(self) -> Tuple[Event, ...]: - """ - Gets the Events that happen within homeassistant - :code:`GET /api/events` - """ - data = self.request("events") - return tuple( - map( - lambda json: Event.from_json_with_client( - json, client=cast(Client, self) - ), - cast(List[dict[str, JSONType]], data), - ) - ) - - def get_event(self, name: str) -> Optional[Event]: - """ - Gets the :py:class:`Event` with the specified name if it has at least one listener. - Uses cached data from :py:meth:`get_events` if available. - """ - for event in self.get_events(): - if event.event == name.strip().lower(): - return event - return None - - def fire_event(self, event_type: str, **event_data) -> Optional[str]: - """ - Fires a given event_type within homeassistant. Must be an existing event_type. - `POST /api/events/` - """ - data = self.request( - join("events", event_type), - method="POST", - json=event_data, - ) - return cast(dict[str, str], data).get("message") - - def get_components(self) -> Tuple[str, ...]: - """ - Returns a tuple of all registered components. - :code:`GET /api/components` - """ - return tuple(self.request("components")) diff --git a/homeassistant_api/rawwebsocket.py b/homeassistant_api/rawwebsocket.py deleted file mode 100644 index 3e71d85a..00000000 --- a/homeassistant_api/rawwebsocket.py +++ /dev/null @@ -1,639 +0,0 @@ -import contextlib -import json -import logging -import time -from typing import TYPE_CHECKING, Any, Dict, Generator, Optional, Tuple, Union, cast - -import websockets.sync.client as ws -from pydantic import ValidationError - -from homeassistant_api.errors import ( - ReceivingError, - ResponseError, - UnauthorizedError, -) -from homeassistant_api.models import ( - ConfigEntry, - ConfigEntryEvent, - ConfigSubEntry, - Domain, - Entity, - Group, - State, -) -from homeassistant_api.models.config_entries import DisableEnableResult, FlowResult -from homeassistant_api.models.states import Context -from homeassistant_api.models.websocket import ( - AuthInvalid, - AuthOk, - AuthRequired, - EventResponse, - FiredEvent, - FiredTrigger, - PingResponse, - ResultResponse, - TemplateEvent, -) -from homeassistant_api.rawbasewebsocket import RawBaseWebsocketClient -from homeassistant_api.utils import JSONType, prepare_entity_id - -if TYPE_CHECKING: - from homeassistant_api import WebsocketClient -else: - WebsocketClient = None # pylint: disable=invalid-name - -logger = logging.getLogger(__name__) - - -class RawWebsocketClient(RawBaseWebsocketClient): - _conn: Optional[ws.ClientConnection] - - def __init__(self, api_url: str, token: str) -> None: - super().__init__(api_url, token) - self._conn = None - - self._id_counter = 0 - self._result_responses: dict[ - int, Optional[ResultResponse] - ] = {} # id -> response - self._event_responses: dict[ - int, list[EventResponse] - ] = {} # id -> [response, ...] - self._ping_responses: dict[int, PingResponse] = {} # id -> (sent, received) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.api_url!r})" - - def __enter__(self): - self._conn = ws.connect(self.api_url) - self._conn.__enter__() - okay = self.authentication_phase() - logging.info("Authenticated with Home Assistant (%s)", okay.ha_version) - self.supported_features_phase() - return self - - def __exit__(self, exc_type, exc_value, traceback): - if not self._conn: - raise ReceivingError("Connection is not open!") - self._conn.__exit__(exc_type, exc_value, traceback) - self._conn = None - - def _send(self, data: dict[str, JSONType]) -> None: - """Send a message to the websocket server.""" - logger.debug(f"Sending message: {data}") - if self._conn is None: - raise ReceivingError("Connection is not open!") - self._conn.send(json.dumps(data)) - - def _recv(self) -> dict[str, JSONType]: - """Receive a message from the websocket server.""" - if self._conn is None: - raise ReceivingError("Connection is not open!") - _bytes = self._conn.recv() - logger.debug("Received message: %s", _bytes) - return cast(dict[str, JSONType], json.loads(_bytes)) - - def send(self, type: str, include_id: bool = True, **data: Any) -> int: - """ - Send a command message to the websocket server and wait for a "result" response. - - Returns the id of the message sent. - """ - if include_id: # auth messages don't have an id - data["id"] = self._request_id() - - data["type"] = type - self._send(data) - - if "id" in data: - assert isinstance(data["id"], int) - if data["type"] == "ping": - self._ping_responses[data["id"]] = PingResponse( - start=time.perf_counter_ns(), - id=data["id"], - type="pong", - ) - else: - self._event_responses[data["id"]] = [] - self._result_responses[data["id"]] = None - return data["id"] - return -1 # non-command messages don't have an id - - def recv(self, id: int) -> Union[EventResponse, ResultResponse, PingResponse]: - """Receive a response to a message from the websocket server.""" - while True: - ## have we received a message with the id we're looking for? - if self._result_responses.get(id) is not None: - return cast(dict[int, ResultResponse], self._result_responses).pop( - id - ) # ughhh why can't mypy figure this out - if self._event_responses.get(id, []): - return self._event_responses[id].pop(0) - if self._ping_responses.get(id) is not None: - if self._ping_responses[id].end is not None: - return self._ping_responses.pop(id) - - ## if not, keep receiving messages until we do - self.handle_recv(self._recv()) - - def authentication_phase(self) -> AuthOk: - """Authenticate with the websocket server.""" - # Capture the first message from the server saying we need to authenticate - try: - welcome = AuthRequired.model_validate(self._recv()) - logger.debug(f"Received welcome message: {welcome}") - except ValidationError as e: - raise ResponseError("Unexpected response during authentication") from e - - # Send our authentication token - self.send("auth", access_token=self.token, include_id=False) - logger.debug("Sent auth message") - - # Check the response - resp = self._recv() - try: - return AuthOk.model_validate(resp) - except ValidationError as e: - error_resp = AuthInvalid.model_validate(resp) - raise UnauthorizedError(error_resp.message) from e - except Exception as e: - raise ResponseError( - "Unexpected response during authentication", resp["message"] - ) from e - - def supported_features_phase(self) -> None: - """Get the supported features from the websocket server.""" - resp = self.recv( - self.send( - "supported_features", - features={ - # "coalesce_messages": 42, # including this key sets it to True - }, - ) - ) - assert cast(ResultResponse, resp).result is None - - def ping_latency(self) -> float: - """Get the latency (in milliseconds) of the connection by sending a ping message.""" - pong = cast(PingResponse, self.recv(self.send("ping"))) - assert pong.end is not None - return (pong.end - pong.start) / 1_000_000 - - def get_rendered_template(self, template: str) -> str: - """ - Renders a Jinja2 template with Home Assistant context data. - See https://www.home-assistant.io/docs/configuration/templating. - - Sends command :code:`{"type": "render_template", ...}`. - """ - id = self.send("render_template", template=template, report_errors=True) - first = self.recv(id) - assert cast(ResultResponse, first).result is None - second = self.recv(id) - self._unsubscribe(id) - return cast(TemplateEvent, cast(EventResponse, second).event).result - - def get_config(self) -> dict[str, JSONType]: - """ - Get the Home Assistant configuration. - - Sends command :code:`{"type": "get_config", ...}`. - """ - return cast( - dict[str, JSONType], - cast( - ResultResponse, - self.recv(self.send("get_config")), - ).result, - ) - - def get_states(self) -> Tuple[State, ...]: - """ - Get a list of states. - - Sends command :code:`{"type": "get_states", ...}`. - """ - return tuple( - State.from_json(state) - for state in cast( - list[dict[str, JSONType]], - cast(ResultResponse, self.recv(self.send("get_states"))).result, - ) - ) - - def get_state( # pylint: disable=duplicate-code - self, - *, - entity_id: Optional[str] = None, - group_id: Optional[str] = None, - slug: Optional[str] = None, - ) -> State: - """ - Just calls the :py:meth:`get_states` method and filters the result. - - Please tell home-assistant/core to add a :code:`{"type": "get_state", ...}` command to the WS API! - There is a lot of disappointment and frustration in the community because this is not available. - """ - entity_id = prepare_entity_id( - group_id=group_id, - slug=slug, - entity_id=entity_id, - ) - - for state in self.get_states(): - if state.entity_id == entity_id: - return state - raise ValueError(f"Entity {entity_id} not found!") - - def get_entities(self) -> Dict[str, Group]: - """ - Fetches all entities from the Websocket API and returns them as a dictionary of :py:class:`Group`'s. - For example :code:`light.living_room` would be in the group :code:`light` (i.e. :code:`get_entities()["light"].living_room`). - """ - entities: Dict[str, Group] = {} - for state in self.get_states(): - group_id, entity_slug = state.entity_id.split(".") - if group_id not in entities: - entities[group_id] = Group( - group_id=group_id, - _client=self, # type: ignore[arg-type] - ) - entities[group_id]._add_entity(entity_slug, state) - return entities - - def get_entity( - self, - group_id: Optional[str] = None, - slug: Optional[str] = None, - entity_id: Optional[str] = None, - ) -> Optional[Entity]: - """ - Returns an :py:class:`Entity` model for an :code:`entity_id`. - - Calls :py:meth:`get_states` under the hood. - - Please tell home-assistant/core to add a :code:`{"type": "get_state", ...}` command to the WS API! - There is a lot of disappointment and frustration in the community because this is not available. - """ - if group_id is not None and slug is not None: - state = self.get_state(group_id=group_id, slug=slug) - elif entity_id is not None: - state = self.get_state(entity_id=entity_id) - else: - help_msg = ( - "Use keyword arguments to pass entity_id. " - "Or you can pass the group_id and slug instead" - ) - raise ValueError( - f"Neither group_id and slug or entity_id provided. {help_msg}" - ) - split_group_id, split_slug = state.entity_id.split(".") - group = Group( - group_id=split_group_id, - _client=self, # type: ignore[arg-type] - ) - group._add_entity(split_slug, state) - return group.get_entity(split_slug) - - def get_domains(self) -> dict[str, Domain]: - """ - Get a list of services that Home Assistant offers (organized into a dictionary of service domains). - - For example, the service :code:`light.turn_on` would be in the domain :code:`light`. - - Sends command :code:`{"type": "get_services", ...}`. - """ - resp = self.recv(self.send("get_services")) - domains = map( - lambda item: Domain.from_json_with_client( - {"domain": item[0], "services": item[1]}, - client=cast(WebsocketClient, self), - ), - cast(dict[str, JSONType], cast(ResultResponse, resp).result).items(), - ) - return {domain.domain_id: domain for domain in domains} - - def get_domain(self, domain: str) -> Domain: - """Get a domain. - - Note: This is not a method in the WS API client... yet. - - Please tell home-assistant/core to add a `get_domain` command to the WS API! - - For now, just call the :py:meth":`get_domains` method and parsing the result. - """ - return self.get_domains()[domain] - - def trigger_service( - self, - domain: str, - service: str, - entity_id: Optional[str] = None, - **service_data, - ) -> None: - """ - Trigger a service (that doesn't return a response). - - Sends command :code:`{"type": "call_service", ...}`. - """ - params = { - "domain": domain, - "service": service, - "service_data": service_data, - "return_response": False, - } - if entity_id is not None: - params["target"] = {"entity_id": entity_id} - - data = self.recv(self.send("call_service", include_id=True, **params)) - - # TODO: handle data["result"]["context"] ? - - assert ( - cast( - dict[str, JSONType], - cast(ResultResponse, data).result, - ).get("response") - is None - ) # should always be None for services without a response - - def trigger_service_with_response( - self, - domain: str, - service: str, - entity_id: Optional[str] = None, - **service_data, - ) -> dict[str, JSONType]: - """ - Trigger a service (that returns a response) and return the response. - - Sends command :code:`{"type": "call_service", ...}`. - """ - params = { - "domain": domain, - "service": service, - "service_data": service_data, - "return_response": True, - } - if entity_id is not None: - params["target"] = {"entity_id": entity_id} - - data = self.recv(self.send("call_service", include_id=True, **params)) - - return cast(dict[str, dict[str, JSONType]], cast(ResultResponse, data).result)[ - "response" - ] - - @contextlib.contextmanager - def listen_events( - self, - event_type: Optional[str] = None, - ) -> Generator[Generator[FiredEvent, None, None], None, None]: - """ - Listen for all events of a certain type. - - For example, to listen for all events of type `test_event`: - - .. code-block:: python - - with ws_client.listen_events("test_event") as events: - for i, event in zip(range(2), events): # to only wait for two events to be received - print(event) - """ - subscription = self._subscribe_events(event_type) - yield cast(Generator[FiredEvent, None, None], self._wait_for(subscription)) - self._unsubscribe(subscription) - - def _subscribe_events(self, event_type: Optional[str]) -> int: - """ - Subscribe to all events of a certain type. - - - Sends command :code:`{"type": "subscribe_events", ...}`. - """ - params = {"event_type": event_type} if event_type else {} - return self.recv(self.send("subscribe_events", include_id=True, **params)).id - - @contextlib.contextmanager - def listen_trigger( - self, trigger: str, **trigger_fields - ) -> Generator[Generator[dict[str, JSONType], None, None], None, None]: - """ - Listen to a Home Assistant trigger. - Allows additional trigger keyword parameters with :code:`**kwargs` (i.e. passing :code:`tag_id=...` for NFC tag triggers). - - For example, in Home Assistant Automations we can subscribe to a state trigger for a light entity with YAML: - - .. code-block:: yaml - - triggers: - # ... - - trigger: state - entity_id: light.kitchen - - To subscribe to that same state trigger with :py:class:`WebsocketClient` instead - - .. code-block:: python - - with ws_client.listen_trigger("state", entity_id="light.kitchen") as trigger: - for event in trigger: # will iterate until we manually break out of the loop - print(event) - if : - break - # exiting the context manager unsubscribes from the trigger - - Woohoo! We can now listen to triggers in Python code! - """ - subscription = self._subscribe_trigger(trigger, **trigger_fields) - yield ( - fired_trigger.variables - for fired_trigger in cast( - Generator[FiredTrigger, None, None], - self._wait_for(subscription), - ) - ) - self._unsubscribe(subscription) - - def _subscribe_trigger(self, trigger: str, **trigger_fields) -> int: - """ - Return the subscription id of the trigger we subscribe to. - - Sends command :code:`{"type": "subscribe_trigger", ...}`. - """ - return self.recv( - self.send( - "subscribe_trigger", trigger={"platform": trigger, **trigger_fields} - ) - ).id - - def _wait_for( - self, subscription_id: int - ) -> Generator[Union[FiredEvent, FiredTrigger], None, None]: - """ - An iterator that waits for events of a certain type. - """ - while True: - yield cast( - Union[ - FiredEvent, FiredTrigger - ], # we can cast this because TemplateEvent is only used for rendering templates - cast(EventResponse, self.recv(subscription_id)).event, - ) - - def _unsubscribe(self, subcription_id: int) -> None: - """ - Unsubscribe from all events of a certain type. - - Sends command :code:`{"type": "unsubscribe_events", ...}`. - """ - resp = self.recv(self.send("unsubscribe_events", subscription=subcription_id)) - assert cast(ResultResponse, resp).result is None - self._event_responses.pop(subcription_id) - - def get_config_entries(self) -> Tuple[ConfigEntry, ...]: - """ - Get all config entries. - - Sends command :code:`{"type": "config_entries/get", ...}`. - """ - resp = self.recv(self.send("config_entries/get")) - return tuple( - ConfigEntry.from_json(entry) - for entry in cast( - list[dict[str, JSONType]], - cast(ResultResponse, resp).result, - ) - ) - - def disable_config_entry(self, entry_id: str) -> DisableEnableResult: - """ - Disable a config entry. - - Sends command :code:`{"type": "config_entries/disable", ...}`. - """ - resp = self.recv( - self.send( - "config_entries/disable", - entry_id=entry_id, - disabled_by="user", - ) - ) - return DisableEnableResult.from_json( - cast(dict[str, JSONType], cast(ResultResponse, resp).result) - ) - - def enable_config_entry(self, entry_id: str) -> DisableEnableResult: - """ - Enable a config entry. - - Sends command :code:`{"type": "config_entries/disable", ...}`. - """ - resp = self.recv( - self.send( - "config_entries/disable", - entry_id=entry_id, - disabled_by=None, - ) - ) - return DisableEnableResult.from_json( - cast(dict[str, JSONType], cast(ResultResponse, resp).result) - ) - - def ignore_config_flow(self, flow_id: str, title: str) -> None: - """ - Ignore a config flow. - - Sends command :code:`{"type": "config_entries/ignore_flow", ...}`. - """ - self.recv( - self.send( - "config_entries/ignore_flow", - flow_id=flow_id, - title=title, - ) - ) - - def get_nonuser_flows_in_progress(self) -> Tuple[FlowResult, ...]: - """ - Get non-user config flows in progress. - - Sends command :code:`{"type": "config_entries/flow/progress", ...}`. - """ - resp = self.recv(self.send("config_entries/flow/progress")) - return tuple( - FlowResult.from_json(flow) - for flow in cast( - list[dict[str, JSONType]], - cast(ResultResponse, resp).result, - ) - ) - - def get_entry_subentries(self, entry_id: str) -> Tuple[ConfigSubEntry, ...]: - """ - Get subentries for a config entry. - - Sends command :code:`{"type": "config_entries/subentries/list", ...}`. - """ - resp = self.recv(self.send("config_entries/subentries/list", entry_id=entry_id)) - return tuple( - ConfigSubEntry.from_json(subentry) - for subentry in cast( - list[dict[str, JSONType]], - cast(ResultResponse, resp).result, - ) - ) - - def delete_entry_subentry(self, entry_id: str, subentry_id: str) -> None: - """ - Delete a subentry from a config entry. - - Sends command :code:`{"type": "config_entries/subentries/delete", ...}`. - """ - self.recv( - self.send( - "config_entries/subentries/delete", - entry_id=entry_id, - subentry_id=subentry_id, - ) - ) - - @contextlib.contextmanager - def listen_config_entries( - self, - ) -> Generator[Generator[list[ConfigEntryEvent], None, None], None, None]: - """ - Listen for config entry changes. - - Sends command :code:`{"type": "config_entries/subscribe", ...}`. - """ - subscription = self.recv(self.send("config_entries/subscribe")).id - yield self._wait_for_config_entries(subscription) - self._unsubscribe(subscription) - - def _wait_for_config_entries( - self, subscription_id: int - ) -> Generator[list[ConfigEntryEvent], None, None]: - """An iterator that waits for config entry events.""" - while True: - event_resp = cast(EventResponse, self.recv(subscription_id)) - entries = cast(list[dict[str, JSONType]], event_resp.event) - yield [ConfigEntryEvent.from_json(entry) for entry in entries] - - def fire_event(self, event_type: str, **event_data) -> Context: - """ - Fire an event. - - Sends command :code:`{"type": "fire_event", ...}`. - """ - params: dict[str, JSONType] = {"event_type": event_type} - if event_data: - params["event_data"] = event_data - return Context.from_json( - cast( - dict[str, dict[str, JSONType]], - cast( - ResultResponse, - self.recv(self.send("fire_event", include_id=True, **params)), - ).result, - )["context"] - ) diff --git a/homeassistant_api/utils.py b/homeassistant_api/utils.py index 305c703c..1d2a1757 100644 --- a/homeassistant_api/utils.py +++ b/homeassistant_api/utils.py @@ -1,16 +1,4 @@ -import os import re -from typing import TYPE_CHECKING, Optional, Union # noqa: F401 - -from typing_extensions import TypeAliasType - -if TYPE_CHECKING or os.getenv("DOCUMENTATION_MODE") != "true": - JSONType = TypeAliasType( - "JSONType", - "Optional[Union[int, float, str, bool, list[JSONType], dict[str, JSONType]]]", - ) -else: - JSONType = type("JSONType", (object,), {}) def format_entity_id(entity_id: str) -> str: @@ -23,21 +11,24 @@ def format_entity_id(entity_id: str) -> str: def prepare_entity_id( *, - group_id: Optional[str] = None, - slug: Optional[str] = None, - entity_id: Optional[str] = None, + group_id: str | None = None, + slug: str | None = None, + entity_id: str | None = None, ) -> str: """ Combines optional :code:`group` and :code:`slug` into an :code:`entity_id` if provided. Favors :code:`entity_id` over :code:`group` or :code:`slug`. """ if (group_id is None or slug is None) and entity_id is None: - raise ValueError( + msg = ( "To use group or slug you need to pass both, not just one. " "Otherwise pass entity_id. " "Also make sure you are using keyword arguments." ) + raise ValueError(msg) if group_id is not None and slug is not None: entity_id = f"{group_id}.{slug}" - assert entity_id is not None + if entity_id is None: + msg = "entity_id must be provided, or both group_id and slug must be provided" + raise ValueError(msg) return format_entity_id(entity_id) diff --git a/homeassistant_api/websocket.py b/homeassistant_api/websocket.py index 5a4d0d0a..f4ea8948 100644 --- a/homeassistant_api/websocket.py +++ b/homeassistant_api/websocket.py @@ -1,45 +1,750 @@ -"""Module containing the primary Client class.""" +from __future__ import annotations +import contextlib +import json import logging -import urllib.parse as urlparse +import time +from typing import TYPE_CHECKING +from typing import Any -from .rawasyncwebsocket import RawAsyncWebsocketClient -from .rawwebsocket import RawWebsocketClient +from niquests import Session +from pydantic import ValidationError +from typing_extensions import Self + +from homeassistant_api.basewebsocket import BaseWebsocketClient +from homeassistant_api.errors import ReceivingError +from homeassistant_api.errors import ResponseError +from homeassistant_api.errors import UnauthorizedError +from homeassistant_api.models import ConfigEntry +from homeassistant_api.models import ConfigEntryEvent +from homeassistant_api.models import ConfigSubEntry +from homeassistant_api.models import Domain +from homeassistant_api.models import Entity +from homeassistant_api.models import Group +from homeassistant_api.models import History +from homeassistant_api.models import State +from homeassistant_api.models.config_entries import DisableEnableResult +from homeassistant_api.models.config_entries import FlowResult +from homeassistant_api.models.entity_registry import EntityRegistryEntry +from homeassistant_api.models.entity_registry import EntityRegistryEntryExtended +from homeassistant_api.models.entity_registry import EntityRegistryUpdateParams +from homeassistant_api.models.entity_registry import EntityRegistryUpdateResult +from homeassistant_api.models.states import Context +from homeassistant_api.models.websocket import AuthInvalid +from homeassistant_api.models.websocket import AuthOk +from homeassistant_api.models.websocket import AuthRequired +from homeassistant_api.models.websocket import EventResponse +from homeassistant_api.models.websocket import FiredEvent +from homeassistant_api.models.websocket import FiredTrigger +from homeassistant_api.models.websocket import PingResponse +from homeassistant_api.models.websocket import ResultResponse +from homeassistant_api.models.websocket import TemplateEvent +from homeassistant_api.utils import prepare_entity_id + +if TYPE_CHECKING: + from collections.abc import Generator + from datetime import datetime + from types import TracebackType + + from urllib3.contrib.webextensions.protocol import ExtensionFromHTTP logger = logging.getLogger(__name__) -class WebsocketClient(RawWebsocketClient, RawAsyncWebsocketClient): - """ +class WebsocketClient(BaseWebsocketClient): + _session: Session + _ws: ExtensionFromHTTP + + def __init__( + self, + api_url: str, + token: str, + *, + max_size: int = 2**24, + session: Session | None = None, + ) -> None: + super().__init__(api_url, token, max_size=max_size) + self._session = session if session is not None else Session() - The main class for interacting with the Home Assistant WebSocket API client. + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.api_url!r})" - Here's a quick example of how to use the :py:class:`WebsocketClient` class: + def __enter__(self) -> Self: + self._session.__enter__() + resp = self._session.get(self.api_url) + resp.raise_for_status() + if resp.extension is None: + msg = "Server did not upgrade to WebSocket" + raise ReceivingError(msg) + self._ws = resp.extension + okay = self.authentication_phase() + logger.info("Authenticated with Home Assistant (%s)", okay.ha_version) + self.supported_features_phase() + return self - .. code-block:: python + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + self._ws.close() + del self._ws + self._session.__exit__(exc_type, exc_value, traceback) - from homeassistant_api import WebsocketClient + def _send(self, data: dict[str, Any]) -> None: + """Send a message to the websocket server.""" + if data.get("type") != "auth": + logger.debug(f"Sending message: {data}") + self._ws.send_payload(json.dumps(data)) - with WebsocketClient( - '', # i.e. 'ws://homeassistant.local:8123/api/websocket' - '' - ) as ws_client: - light = ws_client.trigger_service('light', 'turn_on', entity_id="light.living_room") - """ + def _recv(self) -> dict[str, Any]: + """Receive a complete JSON message from the websocket server, buffering fragments.""" + buf = "" + while True: + chunk = self._ws.next_payload() + if chunk is None: + msg = "WebSocket connection closed" + raise ReceivingError(msg) + buf += chunk if isinstance(chunk, str) else chunk.decode() + try: + r = json.loads(buf) + except json.JSONDecodeError: # pragma: no cover + continue + logger.debug("Received message: %s", buf) + if not isinstance(r, dict): + msg = f"Expected dict, got {type(r).__name__}" + raise TypeError(msg) + return r - def __init__(self, api_url: str, token: str, use_async: bool = False) -> None: - parsed = urlparse.urlparse(api_url) + def send(self, msg_type: str, *, include_id: bool = True, **data: Any) -> int: + """ + Send a command message to the websocket server and wait for a "result" response. - if parsed.scheme in {"ws", "wss"}: - if use_async: - RawAsyncWebsocketClient.__init__(self, api_url, token) - client_type = "Async" - else: - RawWebsocketClient.__init__(self, api_url, token) - client_type = "" + Returns the id of the message sent. + """ + if include_id: # auth messages don't have an id + data["id"] = self._request_id() + + data["type"] = msg_type + self._send(data) + + if "id" not in data: + return -1 # non-command messages don't have an id + if not isinstance(data["id"], int): + msg = f"Expected int for message id, got {type(data['id'])}" + raise TypeError(msg) + if data["type"] == "ping": + self._ping_responses[data["id"]] = PingResponse( + start=time.perf_counter_ns(), + id=data["id"], + type="pong", + ) else: - raise ValueError(f"Unknown scheme {parsed.scheme} in {api_url}") + self._event_responses[data["id"]] = [] + self._result_responses[data["id"]] = None + return data["id"] + + def recv(self, msg_id: int) -> EventResponse | ResultResponse | PingResponse | None: + """Receive a response to a message from the websocket server.""" + while True: + ## have we received a message with the id we're looking for? + if self._result_responses.get(msg_id) is not None: + return self._result_responses.pop(msg_id) + if self._event_responses.get(msg_id, []): + return self._event_responses[msg_id].pop(0) + if ( + self._ping_responses.get(msg_id) is not None + and self._ping_responses[msg_id].end is not None + ): + return self._ping_responses.pop(msg_id) + + ## if not, keep receiving messages until we do + self.handle_recv(self._recv()) + + def recv_result(self, msg_id: int) -> ResultResponse: + """Receive a ResultResponse, raising TypeError if the response is not a ResultResponse.""" + resp = self.recv(msg_id) + if not isinstance(resp, ResultResponse): + msg = f"Expected ResultResponse, got {type(resp).__name__}" + raise TypeError(msg) + return resp + + def recv_result_dict(self, msg_id: int) -> dict[str, Any]: + """Receive a ResultResponse and return its result as a dict.""" + resp = self.recv_result(msg_id) + if not isinstance(resp.result, dict): + msg = f"Expected dict result, got {type(resp.result).__name__}" + raise TypeError(msg) + return resp.result + + def recv_result_list(self, msg_id: int) -> list[dict[str, Any]]: + """Receive a ResultResponse and return its result as a list.""" + resp = self.recv_result(msg_id) + if not isinstance(resp.result, list): + msg = f"Expected list result, got {type(resp.result).__name__}" + raise TypeError(msg) + return resp.result + + def recv_event(self, msg_id: int) -> EventResponse: + """Receive an EventResponse, raising TypeError if the response is not an EventResponse.""" + resp = self.recv(msg_id) + if not isinstance(resp, EventResponse): + msg = f"Expected EventResponse, got {type(resp).__name__}" + raise TypeError(msg) + return resp + + def recv_ping(self, msg_id: int) -> PingResponse: + """Receive a PingResponse, raising TypeError if the response is not a PingResponse.""" + resp = self.recv(msg_id) + if not isinstance(resp, PingResponse): + msg = f"Expected PingResponse, got {type(resp).__name__}" + raise TypeError(msg) + return resp + + def authentication_phase(self) -> AuthOk: + """Authenticate with the websocket server.""" + # Capture the first message from the server saying we need to authenticate + try: + welcome = AuthRequired.model_validate(self._recv()) + logger.debug(f"Received welcome message: {welcome}") + except ValidationError as e: + msg = "Unexpected response during authentication" + raise ResponseError(msg) from e + + # Send our authentication token + self.send("auth", access_token=self.token, include_id=False) + logger.debug("Sent auth message") + + # Check the response + resp = self._recv() + try: + return AuthOk.model_validate(resp) + except ValidationError: + try: + error_resp = AuthInvalid.model_validate(resp) + raise UnauthorizedError(error_resp.message) from None + except ValidationError: + msg = f"Unexpected response during authentication: {resp}" + raise ResponseError(msg) from None + except Exception as e: + msg = f"Unexpected response during authentication: {resp}" + raise ResponseError(msg) from e + + def supported_features_phase(self) -> None: + """Get the supported features from the websocket server.""" + resp = self.recv_result(self.send("supported_features", features={})) + if resp.result is not None: + msg = "Expected None result for unsubscribe" + raise ValueError(msg) + + def ping_latency(self) -> float: + """Get the latency (in milliseconds) of the connection by sending a ping message.""" + pong = self.recv_ping(self.send("ping")) + if pong.end is None: + msg = "Pong response missing end timestamp" + raise ValueError(msg) + return (pong.end - pong.start) / 1_000_000 + + def get_rendered_template(self, template: str) -> str: + """ + Renders a Jinja2 template with Home Assistant context data. + See https://www.home-assistant.io/docs/configuration/templating. + + Sends command :code:`{"type": "render_template", ...}`. + """ + msg_id = self.send("render_template", template=template, report_errors=True) + self.recv_result(msg_id) + second = self.recv_event(msg_id) + self._unsubscribe(msg_id) + if not isinstance(second.event, TemplateEvent): + msg = f"Expected TemplateEvent, got {type(second.event).__name__}" + raise TypeError(msg) + return str(second.event.result) + + def get_config(self) -> dict[str, Any]: + """ + Returns the configuration of Home Assistant. + + Sends command :code:`{"type": "get_config", ...}`. + """ + return self.recv_result_dict(self.send("get_config")) + + def get_states(self) -> tuple[State, ...]: + """ + Gets the states of all entities within Home Assistant. + + Sends command :code:`{"type": "get_states", ...}`. + """ + return tuple( + State.from_json(state) + for state in self.recv_result_list(self.send("get_states")) + ) + + def get_state( # pylint: disable=duplicate-code + self, + *, + entity_id: str | None = None, + group_id: str | None = None, + slug: str | None = None, + ) -> State: + """ + Fetches the state of the entity specified. + + Note: The WebSocket API has no single-entity state command, so this fetches all states and filters. + + Sends command :code:`{"type": "get_states", ...}`. + """ + entity_id = prepare_entity_id( + group_id=group_id, + slug=slug, + entity_id=entity_id, + ) + + for state in self.get_states(): + if state.entity_id == entity_id: + return state + msg = f"Entity {entity_id} not found!" + raise ValueError(msg) + + def get_entities(self) -> dict[str, Group]: + """ + Fetches all entities from the Websocket API and returns them as a dictionary of :py:class:`Group`'s. + For example :code:`light.living_room` would be in the group :code:`light` (i.e. :code:`get_entities()["light"].living_room`). + """ + entities: dict[str, Group] = {} + for state in self.get_states(): + group_id, entity_slug = state.entity_id.split(".") + if group_id not in entities: + entities[group_id] = Group( + group_id=group_id, + client=self, + ) + entities[group_id]._add_entity(entity_slug, state) # noqa: SLF001 + return entities + + def get_entity( + self, + group_id: str | None = None, + slug: str | None = None, + entity_id: str | None = None, + ) -> Entity | None: + """ + Returns an :py:class:`Entity` model for an :code:`entity_id`. + + Note: The WebSocket API has no single-entity state command, so this fetches all states and filters. + """ + if group_id is not None and slug is not None: + state = self.get_state(group_id=group_id, slug=slug) + elif entity_id is not None: + state = self.get_state(entity_id=entity_id) + else: + help_msg = ( + "Use keyword arguments to pass entity_id. " + "Or you can pass the group_id and slug instead" + ) + msg = f"Neither group_id and slug or entity_id provided. {help_msg}" + raise ValueError(msg) + split_group_id, split_slug = state.entity_id.split(".") + group = Group(group_id=split_group_id, client=self) + group._add_entity(split_slug, state) # noqa: SLF001 + return group.get_entity(split_slug) + + def set_state(self, state: State) -> State: + """Not supported over WebSocket. Use the REST :py:class:`Client` instead.""" + msg = "set_state is not supported over the WebSocket API. Use the REST Client." + raise NotImplementedError(msg) + + def get_entity_histories( + self, + entities: tuple[Entity, ...] | None = None, + start_timestamp: datetime | None = None, + end_timestamp: datetime | None = None, + significant_changes_only: bool = False, + ) -> Generator[History, None, None]: + """Not supported over WebSocket. Use the REST :py:class:`Client` instead.""" + msg = "get_entity_histories is not supported over the WebSocket API. Use the REST Client." + raise NotImplementedError(msg) + + def get_domains(self) -> dict[str, Domain]: + """ + Fetches all service :py:class:`Domain`'s from the API. + + Sends command :code:`{"type": "get_services", ...}`. + """ + result = self.recv_result_dict(self.send("get_services")) + domains = ( + Domain.from_json_with_client( + {"domain": item[0], "services": item[1]}, + client=self, + ) + for item in result.items() + ) + return {domain.domain_id: domain for domain in domains} + + def get_domain(self, domain_id: str) -> Domain | None: + """ + Fetches all :py:class:`Service`'s under a particular service :py:class:`Domain`. + + Note: The WebSocket API has no single-domain command, so this fetches all domains and filters. + """ + return self.get_domains().get(domain_id) + + def trigger_service( + self, + domain: str, + service: str, + **service_data: Any, + ) -> None: + """ + Trigger a service (that doesn't return a response). + + Sends command :code:`{"type": "call_service", ...}`. + + Note: Unlike the REST API, the WebSocket API does not return changed states. + Subscribe to ``state_changed`` events via :py:meth:`listen_events` to track changes. + """ + params = { + "domain": domain, + "service": service, + "service_data": service_data, + "return_response": False, + } + + result = self.recv_result_dict( + self.send("call_service", include_id=True, **params), + ) + + # TODO: handle result["context"] ? + if result.get("response") is not None: + # response should always be empty + msg = f"got unexpected response: {result.get('response')}" + raise TypeError(msg) + + def trigger_service_with_response( + self, + domain: str, + service: str, + **service_data: Any, + ) -> dict[str, Any]: + """ + Trigger a service (that returns a response) and return the response. + + Sends command :code:`{"type": "call_service", ...}`. + + Note: Unlike the REST API, the WebSocket API does not return changed states, + only the service response data. Subscribe to ``state_changed`` events via + :py:meth:`listen_events` to track changes. + """ + params = { + "domain": domain, + "service": service, + "service_data": service_data, + "return_response": True, + } + + result = self.recv_result_dict( + self.send("call_service", include_id=True, **params), + ) + + return result["response"] + + @contextlib.contextmanager + def listen_events( + self, + event_type: str | None = None, + ) -> Generator[Generator[FiredEvent | FiredTrigger, None, None], None, None]: + """ + Listen for all events of a certain type. + + For example, to listen for all events of type `test_event`: + + .. code-block:: python + + with ws_client.listen_events("test_event") as events: + for i, event in zip(range(2), events): # to only wait for two events to be received + print(event) + """ + subscription = self._subscribe_events(event_type) + yield self._wait_for(subscription) + self._unsubscribe(subscription) + + def _subscribe_events(self, event_type: str | None) -> int: + """ + Subscribe to all events of a certain type. + + + Sends command :code:`{"type": "subscribe_events", ...}`. + """ + params = {"event_type": event_type} if event_type else {} + return self.recv_result( + self.send("subscribe_events", include_id=True, **params), + ).id + + @contextlib.contextmanager + def listen_trigger( + self, + trigger: str, + **trigger_fields: Any, + ) -> Generator[Generator[dict[str, Any], None, None], None, None]: + """ + Listen to a Home Assistant trigger. + Allows additional trigger keyword parameters with :code:`**kwargs` (i.e. passing :code:`tag_id=...` for NFC tag triggers). + + For example, in Home Assistant Automations we can subscribe to a state trigger for a light entity with YAML: + + .. code-block:: yaml + + triggers: + # ... + - trigger: state + entity_id: light.kitchen + + To subscribe to that same state trigger with :py:class:`WebsocketClient` instead + + .. code-block:: python + + with ws_client.listen_trigger("state", entity_id="light.kitchen") as trigger: + for event in trigger: # will iterate until we manually break out of the loop + print(event) + if : + break + # exiting the context manager unsubscribes from the trigger + + Woohoo! We can now listen to triggers in Python code! + """ + subscription = self._subscribe_trigger(trigger, **trigger_fields) + yield ( + fired_trigger.variables + for fired_trigger in self._wait_for(subscription) + if isinstance(fired_trigger, FiredTrigger) + ) + self._unsubscribe(subscription) + + def _subscribe_trigger(self, trigger: str, **trigger_fields: Any) -> int: + """ + Return the subscription id of the trigger we subscribe to. + + Sends command :code:`{"type": "subscribe_trigger", ...}`. + """ + result = self.recv( + self.send( + "subscribe_trigger", + trigger={"platform": trigger, **trigger_fields}, + ), + ) + if result is None: + msg = "No response received for subscribe_trigger" + raise TypeError(msg) + return result.id + + def _wait_for( + self, + subscription_id: int, + ) -> Generator[FiredEvent | FiredTrigger, None, None]: + """ + An iterator that waits for events of a certain type. + """ + while True: + event_resp = self.recv_event(subscription_id) + if isinstance(event_resp.event, FiredEvent | FiredTrigger): + yield event_resp.event + + def _unsubscribe(self, subscription_id: int) -> None: + """ + Unsubscribe from all events of a certain type. + + Sends command :code:`{"type": "unsubscribe_events", ...}`. + """ + try: + resp = self.recv_result( + self.send("unsubscribe_events", subscription=subscription_id), + ) + if resp.result is not None: + msg = f"leftover events {resp.result}" + raise TypeError(msg) + finally: + self._event_responses.pop(subscription_id, None) + + def get_config_entries(self) -> tuple[ConfigEntry, ...]: + """ + Get all config entries. + + Sends command :code:`{"type": "config_entries/get", ...}`. + """ + return tuple( + ConfigEntry.from_json(entry) + for entry in self.recv_result_list(self.send("config_entries/get")) + ) + + def disable_config_entry(self, entry_id: str) -> DisableEnableResult: + """ + Disable a config entry. + + Sends command :code:`{"type": "config_entries/disable", ...}`. + """ + result = self.recv_result_dict( + self.send( + "config_entries/disable", + entry_id=entry_id, + disabled_by="user", + ), + ) + return DisableEnableResult.from_json(result) + + def enable_config_entry(self, entry_id: str) -> DisableEnableResult: + """ + Enable a config entry. + + Sends command :code:`{"type": "config_entries/disable", ...}`. + """ + result = self.recv_result_dict( + self.send( + "config_entries/disable", + entry_id=entry_id, + disabled_by=None, + ), + ) + return DisableEnableResult.from_json(result) + + def ignore_config_flow(self, flow_id: str, title: str) -> None: + """ + Ignore a config flow. + + Sends command :code:`{"type": "config_entries/ignore_flow", ...}`. + """ + self.recv( + self.send( + "config_entries/ignore_flow", + flow_id=flow_id, + title=title, + ), + ) + + def get_nonuser_flows_in_progress(self) -> tuple[FlowResult, ...]: + """ + Get non-user config flows in progress. + + Sends command :code:`{"type": "config_entries/flow/progress", ...}`. + """ + return tuple( + FlowResult.from_json(flow) + for flow in self.recv_result_list(self.send("config_entries/flow/progress")) + ) + + def get_entry_subentries(self, entry_id: str) -> tuple[ConfigSubEntry, ...]: + """ + Get subentries for a config entry. + + Sends command :code:`{"type": "config_entries/subentries/list", ...}`. + """ + return tuple( + ConfigSubEntry.from_json(subentry) + for subentry in self.recv_result_list( + self.send("config_entries/subentries/list", entry_id=entry_id), + ) + ) + + def delete_entry_subentry(self, entry_id: str, subentry_id: str) -> None: + """ + Delete a subentry from a config entry. + + Sends command :code:`{"type": "config_entries/subentries/delete", ...}`. + """ + self.recv( + self.send( + "config_entries/subentries/delete", + entry_id=entry_id, + subentry_id=subentry_id, + ), + ) + + # ── Entity Registry ───────────────────────────────────────── + + def list_entity_registry(self) -> tuple[EntityRegistryEntry, ...]: + """ + List all entity registry entries. + + Sends command :code:`{"type": "config/entity_registry/list", ...}`. + """ + return tuple( + EntityRegistryEntry.from_json(entry) + for entry in self.recv_result_list( + self.send("config/entity_registry/list"), + ) + ) + + def get_entity_registry_entry(self, entity_id: str) -> EntityRegistryEntryExtended: + """ + Get a single entity registry entry. + + Sends command :code:`{"type": "config/entity_registry/get", ...}`. + """ + result = self.recv_result_dict( + self.send("config/entity_registry/get", entity_id=entity_id), + ) + return EntityRegistryEntryExtended.from_json(result) + + def update_entity_registry_entry( + self, + parameters: EntityRegistryUpdateParams, + ) -> EntityRegistryUpdateResult: + """ + Update an entity registry entry. + + Sends command :code:`{"type": "config/entity_registry/update", ...}`. + """ + result = self.recv_result_dict( + self.send( + "config/entity_registry/update", + **parameters, + ), + ) + return EntityRegistryUpdateResult.from_json(result) + + def remove_entity_registry_entry(self, entity_id: str) -> None: + """ + Remove an entity from the entity registry. + + Sends command :code:`{"type": "config/entity_registry/remove", ...}`. + """ + self.recv( + self.send("config/entity_registry/remove", entity_id=entity_id), + ) + + @contextlib.contextmanager + def listen_config_entries( + self, + ) -> Generator[Generator[list[ConfigEntryEvent], None, None], None, None]: + """ + Listen for config entry changes. + + Sends command :code:`{"type": "config_entries/subscribe", ...}`. + """ + subscription = self.recv_result(self.send("config_entries/subscribe")).id + yield self._wait_for_config_entries(subscription) + self._unsubscribe(subscription) + + def _wait_for_config_entries( + self, + subscription_id: int, + ) -> Generator[list[ConfigEntryEvent], None, None]: + """An iterator that waits for config entry events.""" + while True: + event_resp = self.recv_event(subscription_id) + if isinstance(event_resp.event, list): + yield [ConfigEntryEvent.from_json(entry) for entry in event_resp.event] + + def fire_event(self, event_type: str, **event_data: Any) -> Context: + """ + Fires a given event_type within Home Assistant. - logger.debug( - f"{client_type}WebSocketClient initialized with api_url: {api_url}" + Sends command :code:`{"type": "fire_event", ...}`. + """ + params: dict[str, Any] = {"event_type": event_type} + if event_data: + params["event_data"] = event_data + result = self.recv_result_dict( + self.send("fire_event", include_id=True, **params), ) + return Context.from_json(result["context"]) diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 2d9cd390..00000000 --- a/poetry.lock +++ /dev/null @@ -1,2749 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.0 and should not be changed by hand. - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -description = "Happy Eyeballs for asyncio" -optional = false -python-versions = ">=3.9" -files = [ - {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, - {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, -] - -[[package]] -name = "aiohttp" -version = "3.13.3" -description = "Async http client/server framework (asyncio)" -optional = false -python-versions = ">=3.9" -files = [ - {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7"}, - {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821"}, - {file = "aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845"}, - {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af"}, - {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940"}, - {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160"}, - {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7"}, - {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455"}, - {file = "aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279"}, - {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e"}, - {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d"}, - {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808"}, - {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40"}, - {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29"}, - {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11"}, - {file = "aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd"}, - {file = "aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c"}, - {file = "aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b"}, - {file = "aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64"}, - {file = "aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea"}, - {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a"}, - {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540"}, - {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b"}, - {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3"}, - {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1"}, - {file = "aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3"}, - {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440"}, - {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7"}, - {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c"}, - {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51"}, - {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4"}, - {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29"}, - {file = "aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239"}, - {file = "aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f"}, - {file = "aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c"}, - {file = "aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168"}, - {file = "aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d"}, - {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29"}, - {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3"}, - {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d"}, - {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463"}, - {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc"}, - {file = "aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf"}, - {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033"}, - {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f"}, - {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679"}, - {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423"}, - {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce"}, - {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a"}, - {file = "aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046"}, - {file = "aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57"}, - {file = "aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c"}, - {file = "aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9"}, - {file = "aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3"}, - {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf"}, - {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6"}, - {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d"}, - {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261"}, - {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0"}, - {file = "aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730"}, - {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91"}, - {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3"}, - {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4"}, - {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998"}, - {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0"}, - {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591"}, - {file = "aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf"}, - {file = "aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e"}, - {file = "aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808"}, - {file = "aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415"}, - {file = "aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f"}, - {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6"}, - {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687"}, - {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26"}, - {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a"}, - {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1"}, - {file = "aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25"}, - {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603"}, - {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a"}, - {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926"}, - {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba"}, - {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c"}, - {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43"}, - {file = "aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1"}, - {file = "aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984"}, - {file = "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c"}, - {file = "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592"}, - {file = "aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f"}, - {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29"}, - {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc"}, - {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2"}, - {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587"}, - {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8"}, - {file = "aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632"}, - {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64"}, - {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0"}, - {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56"}, - {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72"}, - {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df"}, - {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa"}, - {file = "aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767"}, - {file = "aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344"}, - {file = "aiohttp-3.13.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e"}, - {file = "aiohttp-3.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7"}, - {file = "aiohttp-3.13.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02"}, - {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43"}, - {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6"}, - {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce"}, - {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80"}, - {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a"}, - {file = "aiohttp-3.13.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6"}, - {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558"}, - {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7"}, - {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877"}, - {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3"}, - {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704"}, - {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f"}, - {file = "aiohttp-3.13.3-cp39-cp39-win32.whl", hash = "sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1"}, - {file = "aiohttp-3.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538"}, - {file = "aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88"}, -] - -[package.dependencies] -aiohappyeyeballs = ">=2.5.0" -aiosignal = ">=1.4.0" -async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} -attrs = ">=17.3.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -propcache = ">=0.2.0" -yarl = ">=1.17.0,<2.0" - -[package.extras] -speedups = ["Brotli (>=1.2)", "aiodns (>=3.3.0)", "backports.zstd", "brotlicffi (>=1.2)"] - -[[package]] -name = "aiohttp-client-cache" -version = "0.14.3" -description = "Persistent cache for aiohttp requests" -optional = false -python-versions = ">=3.9" -files = [ - {file = "aiohttp_client_cache-0.14.3-py3-none-any.whl", hash = "sha256:1154497739dcf9c7f6f6f1f27dc3985d8a7f5f8f31fb76710c06044ffac6f983"}, - {file = "aiohttp_client_cache-0.14.3.tar.gz", hash = "sha256:329f4038c6a8ed0b410023980b6d1a2c484af33e667a89ce245c899d62c1fba1"}, -] - -[package.dependencies] -aiohttp = ">=3.8" -attrs = ">=21.2" -itsdangerous = ">=2.0" -typing-extensions = {version = ">=4", markers = "python_version <= \"3.10\""} -url-normalize = ">=2.2" - -[package.extras] -all = ["aioboto3 (>=9.0)", "aiobotocore (>=2.0)", "aiofiles (>=0.6.0)", "aiosqlite (>=0.20)", "motor (>=3.1)", "redis (>=4.2)"] -dynamodb = ["aioboto3 (>=9.0)", "aiobotocore (>=2.0)"] -filesystem = ["aiofiles (>=0.6.0)", "aiosqlite (>=0.20)"] -mongodb = ["motor (>=3.1)"] -redis = ["redis (>=4.2)"] -sqlite = ["aiosqlite (>=0.20)"] - -[[package]] -name = "aiosignal" -version = "1.4.0" -description = "aiosignal: a list of registered asynchronous callbacks" -optional = false -python-versions = ">=3.9" -files = [ - {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, - {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, -] - -[package.dependencies] -frozenlist = ">=1.1.0" -typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} - -[[package]] -name = "aiosqlite" -version = "0.22.1" -description = "asyncio bridge to the standard sqlite3 module" -optional = false -python-versions = ">=3.9" -files = [ - {file = "aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb"}, - {file = "aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650"}, -] - -[package.extras] -dev = ["attribution (==1.8.0)", "black (==25.11.0)", "build (>=1.2)", "coverage[toml] (==7.10.7)", "flake8 (==7.3.0)", "flake8-bugbear (==24.12.12)", "flit (==3.12.0)", "mypy (==1.19.0)", "ufmt (==2.8.0)", "usort (==1.0.8.post1)"] -docs = ["sphinx (==8.1.3)", "sphinx-mdinclude (==0.6.2)"] - -[[package]] -name = "alabaster" -version = "0.7.16" -description = "A light, configurable Sphinx theme" -optional = false -python-versions = ">=3.9" -files = [ - {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, - {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "async-timeout" -version = "5.0.1" -description = "Timeout context manager for asyncio programs" -optional = false -python-versions = ">=3.8" -files = [ - {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, - {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, -] - -[[package]] -name = "attrs" -version = "25.4.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.9" -files = [ - {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, - {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, -] - -[[package]] -name = "autodoc-pydantic" -version = "2.2.0" -description = "Seamlessly integrate pydantic models in your Sphinx documentation." -optional = false -python-versions = "<4.0.0,>=3.8.1" -files = [ - {file = "autodoc_pydantic-2.2.0-py3-none-any.whl", hash = "sha256:8c6a36fbf6ed2700ea9c6d21ea76ad541b621fbdf16b5a80ee04673548af4d95"}, -] - -[package.dependencies] -pydantic = ">=2.0,<3.0.0" -pydantic-settings = ">=2.0,<3.0.0" -Sphinx = ">=4.0" - -[package.extras] -docs = ["myst-parser (>=3.0.0,<4.0.0)", "sphinx-copybutton (>=0.5.0,<0.6.0)", "sphinx-rtd-theme (>=2.0.0,<3.0.0)", "sphinx-tabs (>=3,<4)", "sphinxcontrib-mermaid (>=0.9.0,<0.10.0)"] -erdantic = ["erdantic (<2.0)"] -linting = ["ruff (>=0.4.0,<0.5.0)"] -security = ["pip-audit (>=2.7.2,<3.0.0)"] -test = ["coverage (>=7,<8)", "defusedxml (>=0.7.1)", "pytest (>=8.0.0,<9.0.0)", "pytest-sugar (>=1.0.0,<2.0.0)"] -type-checking = ["mypy (>=1.9,<2.0)", "types-docutils (>=0.20,<0.21)", "typing-extensions (>=4.11,<5.0)"] - -[[package]] -name = "babel" -version = "2.18.0" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.8" -files = [ - {file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"}, - {file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"}, -] - -[package.extras] -dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] - -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." -optional = false -python-versions = "<3.11,>=3.8" -files = [ - {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, - {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, -] - -[[package]] -name = "cattrs" -version = "25.3.0" -description = "Composable complex class support for attrs and dataclasses." -optional = false -python-versions = ">=3.9" -files = [ - {file = "cattrs-25.3.0-py3-none-any.whl", hash = "sha256:9896e84e0a5bf723bc7b4b68f4481785367ce07a8a02e7e9ee6eb2819bc306ff"}, - {file = "cattrs-25.3.0.tar.gz", hash = "sha256:1ac88d9e5eda10436c4517e390a4142d88638fe682c436c93db7ce4a277b884a"}, -] - -[package.dependencies] -attrs = ">=25.4.0" -exceptiongroup = {version = ">=1.1.1", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.14.0" - -[package.extras] -bson = ["pymongo (>=4.4.0)"] -cbor2 = ["cbor2 (>=5.4.6)"] -msgpack = ["msgpack (>=1.0.5)"] -msgspec = ["msgspec (>=0.19.0)"] -orjson = ["orjson (>=3.11.3)"] -pyyaml = ["pyyaml (>=6.0)"] -tomlkit = ["tomlkit (>=0.11.8)"] -ujson = ["ujson (>=5.10.0)"] - -[[package]] -name = "certifi" -version = "2026.2.25" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.7" -files = [ - {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, - {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, -] - -[[package]] -name = "cfgv" -version = "3.4.0" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.6" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -files = [ - {file = "charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6"}, - {file = "charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4"}, - {file = "charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb"}, - {file = "charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389"}, - {file = "charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f"}, - {file = "charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4"}, - {file = "charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-win32.whl", hash = "sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae"}, - {file = "charset_normalizer-3.4.6-cp38-cp38-win_amd64.whl", hash = "sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-win32.whl", hash = "sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-win_amd64.whl", hash = "sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8"}, - {file = "charset_normalizer-3.4.6-cp39-cp39-win_arm64.whl", hash = "sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8"}, - {file = "charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69"}, - {file = "charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "coverage" -version = "7.10.7" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a"}, - {file = "coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87"}, - {file = "coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0"}, - {file = "coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13"}, - {file = "coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b"}, - {file = "coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807"}, - {file = "coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59"}, - {file = "coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e"}, - {file = "coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2"}, - {file = "coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61"}, - {file = "coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14"}, - {file = "coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2"}, - {file = "coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a"}, - {file = "coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417"}, - {file = "coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6"}, - {file = "coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb"}, - {file = "coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1"}, - {file = "coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256"}, - {file = "coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba"}, - {file = "coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf"}, - {file = "coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d"}, - {file = "coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49"}, - {file = "coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c"}, - {file = "coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f"}, - {file = "coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698"}, - {file = "coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843"}, - {file = "coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546"}, - {file = "coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c"}, - {file = "coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0"}, - {file = "coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999"}, - {file = "coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2"}, - {file = "coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a"}, - {file = "coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb"}, - {file = "coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb"}, - {file = "coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520"}, - {file = "coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360"}, - {file = "coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e"}, - {file = "coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd"}, - {file = "coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2"}, - {file = "coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681"}, - {file = "coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880"}, - {file = "coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63"}, - {file = "coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699"}, - {file = "coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0"}, - {file = "coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399"}, - {file = "coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235"}, - {file = "coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d"}, - {file = "coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a"}, - {file = "coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3"}, - {file = "coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594"}, - {file = "coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0"}, - {file = "coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f"}, - {file = "coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431"}, - {file = "coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07"}, - {file = "coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260"}, - {file = "coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239"}, -] - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "distlib" -version = "0.4.0" -description = "Distribution utilities" -optional = false -python-versions = "*" -files = [ - {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, - {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, -] - -[[package]] -name = "docutils" -version = "0.20.1" -description = "Docutils -- Python Documentation Utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, - {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.1" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, - {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "filelock" -version = "3.19.1" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.9" -files = [ - {file = "filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d"}, - {file = "filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58"}, -] - -[[package]] -name = "filelock" -version = "3.25.2" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.10" -files = [ - {file = "filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70"}, - {file = "filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694"}, -] - -[[package]] -name = "frozenlist" -version = "1.8.0" -description = "A list-like structure which implements collections.abc.MutableSequence" -optional = false -python-versions = ">=3.9" -files = [ - {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"}, - {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"}, - {file = "frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad"}, - {file = "frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2"}, - {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186"}, - {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e"}, - {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450"}, - {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef"}, - {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4"}, - {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff"}, - {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c"}, - {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f"}, - {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7"}, - {file = "frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a"}, - {file = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"}, - {file = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"}, - {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84"}, - {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9"}, - {file = "frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93"}, - {file = "frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f"}, - {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695"}, - {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52"}, - {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581"}, - {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567"}, - {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b"}, - {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92"}, - {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d"}, - {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd"}, - {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967"}, - {file = "frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25"}, - {file = "frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b"}, - {file = "frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a"}, - {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1"}, - {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b"}, - {file = "frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4"}, - {file = "frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383"}, - {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4"}, - {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8"}, - {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b"}, - {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52"}, - {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29"}, - {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3"}, - {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143"}, - {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608"}, - {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa"}, - {file = "frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf"}, - {file = "frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746"}, - {file = "frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd"}, - {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a"}, - {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7"}, - {file = "frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40"}, - {file = "frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027"}, - {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822"}, - {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121"}, - {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5"}, - {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e"}, - {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11"}, - {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1"}, - {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1"}, - {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8"}, - {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed"}, - {file = "frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496"}, - {file = "frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231"}, - {file = "frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62"}, - {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94"}, - {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c"}, - {file = "frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52"}, - {file = "frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51"}, - {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65"}, - {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82"}, - {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714"}, - {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d"}, - {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506"}, - {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51"}, - {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e"}, - {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0"}, - {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41"}, - {file = "frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b"}, - {file = "frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888"}, - {file = "frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042"}, - {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0"}, - {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f"}, - {file = "frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c"}, - {file = "frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2"}, - {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8"}, - {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686"}, - {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e"}, - {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a"}, - {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128"}, - {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f"}, - {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7"}, - {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30"}, - {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7"}, - {file = "frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806"}, - {file = "frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0"}, - {file = "frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b"}, - {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d"}, - {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed"}, - {file = "frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930"}, - {file = "frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c"}, - {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24"}, - {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37"}, - {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a"}, - {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2"}, - {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef"}, - {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe"}, - {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8"}, - {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a"}, - {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e"}, - {file = "frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df"}, - {file = "frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd"}, - {file = "frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79"}, - {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47"}, - {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca"}, - {file = "frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068"}, - {file = "frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95"}, - {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459"}, - {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675"}, - {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61"}, - {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6"}, - {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5"}, - {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3"}, - {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1"}, - {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178"}, - {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda"}, - {file = "frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087"}, - {file = "frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a"}, - {file = "frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103"}, - {file = "frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d"}, - {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, -] - -[[package]] -name = "identify" -version = "2.6.15" -description = "File identification library for Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757"}, - {file = "identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.11" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, - {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "imagesize" -version = "1.5.0" -description = "Getting image size from png/jpeg/jpeg2000/gif file" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -files = [ - {file = "imagesize-1.5.0-py2.py3-none-any.whl", hash = "sha256:32677681b3f434c2cb496f00e89c5a291247b35b1f527589909e008057da5899"}, - {file = "imagesize-1.5.0.tar.gz", hash = "sha256:8bfc5363a7f2133a89f0098451e0bcb1cd71aba4dc02bbcecb39d99d40e1b94f"}, -] - -[[package]] -name = "importlib-metadata" -version = "8.7.1" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -files = [ - {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, - {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=3.4)"] -perf = ["ipython"] -test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["mypy (<1.19)", "pytest-mypy (>=1.0.1)"] - -[[package]] -name = "iniconfig" -version = "2.1.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.8" -files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, -] - -[[package]] -name = "itsdangerous" -version = "2.2.0" -description = "Safely pass data to untrusted environments and back." -optional = false -python-versions = ">=3.8" -files = [ - {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, - {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "librt" -version = "0.8.1" -description = "Mypyc runtime library" -optional = false -python-versions = ">=3.9" -files = [ - {file = "librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc"}, - {file = "librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7"}, - {file = "librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6"}, - {file = "librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0"}, - {file = "librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b"}, - {file = "librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6"}, - {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71"}, - {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7"}, - {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05"}, - {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891"}, - {file = "librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7"}, - {file = "librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2"}, - {file = "librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd"}, - {file = "librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965"}, - {file = "librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da"}, - {file = "librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0"}, - {file = "librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e"}, - {file = "librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3"}, - {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac"}, - {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596"}, - {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99"}, - {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe"}, - {file = "librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb"}, - {file = "librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b"}, - {file = "librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9"}, - {file = "librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a"}, - {file = "librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9"}, - {file = "librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb"}, - {file = "librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d"}, - {file = "librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7"}, - {file = "librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440"}, - {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9"}, - {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972"}, - {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921"}, - {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0"}, - {file = "librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a"}, - {file = "librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444"}, - {file = "librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d"}, - {file = "librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35"}, - {file = "librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583"}, - {file = "librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c"}, - {file = "librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04"}, - {file = "librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363"}, - {file = "librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0"}, - {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012"}, - {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb"}, - {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b"}, - {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d"}, - {file = "librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a"}, - {file = "librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79"}, - {file = "librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0"}, - {file = "librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f"}, - {file = "librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c"}, - {file = "librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc"}, - {file = "librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c"}, - {file = "librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3"}, - {file = "librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14"}, - {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7"}, - {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6"}, - {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071"}, - {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78"}, - {file = "librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023"}, - {file = "librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730"}, - {file = "librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3"}, - {file = "librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1"}, - {file = "librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee"}, - {file = "librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7"}, - {file = "librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040"}, - {file = "librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e"}, - {file = "librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732"}, - {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624"}, - {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4"}, - {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382"}, - {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994"}, - {file = "librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a"}, - {file = "librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4"}, - {file = "librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61"}, - {file = "librt-0.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3dff3d3ca8db20e783b1bc7de49c0a2ab0b8387f31236d6a026597d07fcd68ac"}, - {file = "librt-0.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08eec3a1fc435f0d09c87b6bf1ec798986a3544f446b864e4099633a56fcd9ed"}, - {file = "librt-0.8.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e3f0a41487fd5fad7e760b9e8a90e251e27c2816fbc2cff36a22a0e6bcbbd9dd"}, - {file = "librt-0.8.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bacdb58d9939d95cc557b4dbaa86527c9db2ac1ed76a18bc8d26f6dc8647d851"}, - {file = "librt-0.8.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d7ab1f01aa753188605b09a51faa44a3327400b00b8cce424c71910fc0a128"}, - {file = "librt-0.8.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4998009e7cb9e896569f4be7004f09d0ed70d386fa99d42b6d363f6d200501ac"}, - {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2cc68eeeef5e906839c7bb0815748b5b0a974ec27125beefc0f942715785b551"}, - {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0bf69d79a23f4f40b8673a947a234baeeb133b5078b483b7297c5916539cf5d5"}, - {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:22b46eabd76c1986ee7d231b0765ad387d7673bbd996aa0d0d054b38ac65d8f6"}, - {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:237796479f4d0637d6b9cbcb926ff424a97735e68ade6facf402df4ec93375ed"}, - {file = "librt-0.8.1-cp39-cp39-win32.whl", hash = "sha256:4beb04b8c66c6ae62f8c1e0b2f097c1ebad9295c929a8d5286c05eae7c2fc7dc"}, - {file = "librt-0.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:64548cde61b692dc0dc379f4b5f59a2f582c2ebe7890d09c1ae3b9e66fa015b7"}, - {file = "librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73"}, -] - -[[package]] -name = "markupsafe" -version = "3.0.3" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -files = [ - {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, - {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, - {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, - {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, - {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, - {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, - {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, - {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, - {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, - {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, - {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, - {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, - {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, - {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, - {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, - {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, - {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, - {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, - {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, - {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, - {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, - {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, - {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, - {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, - {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, - {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, - {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, - {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, - {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, - {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, - {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, - {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, - {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, - {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, - {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, -] - -[[package]] -name = "multidict" -version = "6.7.1" -description = "multidict implementation" -optional = false -python-versions = ">=3.9" -files = [ - {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, - {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, - {file = "multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505"}, - {file = "multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122"}, - {file = "multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df"}, - {file = "multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db"}, - {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d"}, - {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e"}, - {file = "multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa"}, - {file = "multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a"}, - {file = "multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b"}, - {file = "multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6"}, - {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172"}, - {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd"}, - {file = "multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba"}, - {file = "multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511"}, - {file = "multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19"}, - {file = "multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf"}, - {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23"}, - {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2"}, - {file = "multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33"}, - {file = "multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3"}, - {file = "multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5"}, - {file = "multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df"}, - {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1"}, - {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963"}, - {file = "multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108"}, - {file = "multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32"}, - {file = "multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8"}, - {file = "multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118"}, - {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee"}, - {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2"}, - {file = "multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b"}, - {file = "multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d"}, - {file = "multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f"}, - {file = "multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5"}, - {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581"}, - {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a"}, - {file = "multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2"}, - {file = "multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7"}, - {file = "multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5"}, - {file = "multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2"}, - {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f"}, - {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358"}, - {file = "multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5"}, - {file = "multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0"}, - {file = "multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4"}, - {file = "multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9"}, - {file = "multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56"}, - {file = "multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} - -[[package]] -name = "mypy" -version = "1.19.1" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"}, - {file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"}, - {file = "mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6"}, - {file = "mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74"}, - {file = "mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1"}, - {file = "mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac"}, - {file = "mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288"}, - {file = "mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab"}, - {file = "mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6"}, - {file = "mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331"}, - {file = "mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925"}, - {file = "mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042"}, - {file = "mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1"}, - {file = "mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e"}, - {file = "mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2"}, - {file = "mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8"}, - {file = "mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a"}, - {file = "mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13"}, - {file = "mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250"}, - {file = "mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b"}, - {file = "mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e"}, - {file = "mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef"}, - {file = "mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75"}, - {file = "mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd"}, - {file = "mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1"}, - {file = "mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718"}, - {file = "mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b"}, - {file = "mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045"}, - {file = "mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957"}, - {file = "mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f"}, - {file = "mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3"}, - {file = "mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a"}, - {file = "mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67"}, - {file = "mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e"}, - {file = "mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376"}, - {file = "mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24"}, - {file = "mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247"}, - {file = "mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba"}, -] - -[package.dependencies] -librt = {version = ">=0.6.2", markers = "platform_python_implementation != \"PyPy\""} -mypy_extensions = ">=1.0.0" -pathspec = ">=0.9.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing_extensions = ">=4.6.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -faster-cache = ["orjson"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.8" -files = [ - {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, - {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, -] - -[[package]] -name = "nodeenv" -version = "1.10.0" -description = "Node.js virtual environment builder" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"}, - {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}, -] - -[[package]] -name = "packaging" -version = "26.0" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, - {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, -] - -[[package]] -name = "pathspec" -version = "1.0.4" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.9" -files = [ - {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, - {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, -] - -[package.extras] -hyperscan = ["hyperscan (>=0.7)"] -optional = ["typing-extensions (>=4)"] -re2 = ["google-re2 (>=1.1)"] -tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] - -[[package]] -name = "platformdirs" -version = "4.4.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.9" -files = [ - {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, - {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.14.1)"] - -[[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - -[[package]] -name = "pre-commit" -version = "2.21.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, - {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "propcache" -version = "0.4.1" -description = "Accelerated property cache" -optional = false -python-versions = ">=3.9" -files = [ - {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db"}, - {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8"}, - {file = "propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925"}, - {file = "propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21"}, - {file = "propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5"}, - {file = "propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db"}, - {file = "propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900"}, - {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c"}, - {file = "propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb"}, - {file = "propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37"}, - {file = "propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581"}, - {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf"}, - {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5"}, - {file = "propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e"}, - {file = "propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566"}, - {file = "propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165"}, - {file = "propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc"}, - {file = "propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757"}, - {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f"}, - {file = "propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1"}, - {file = "propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6"}, - {file = "propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239"}, - {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2"}, - {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403"}, - {file = "propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207"}, - {file = "propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72"}, - {file = "propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367"}, - {file = "propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4"}, - {file = "propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9"}, - {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75"}, - {file = "propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8"}, - {file = "propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db"}, - {file = "propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1"}, - {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf"}, - {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311"}, - {file = "propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74"}, - {file = "propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe"}, - {file = "propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af"}, - {file = "propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c"}, - {file = "propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61"}, - {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66"}, - {file = "propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81"}, - {file = "propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e"}, - {file = "propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1"}, - {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b"}, - {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566"}, - {file = "propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835"}, - {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e"}, - {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859"}, - {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b"}, - {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7"}, - {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1"}, - {file = "propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717"}, - {file = "propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37"}, - {file = "propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a"}, - {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12"}, - {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c"}, - {file = "propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded"}, - {file = "propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641"}, - {file = "propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4"}, - {file = "propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44"}, - {file = "propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49"}, - {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144"}, - {file = "propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f"}, - {file = "propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153"}, - {file = "propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992"}, - {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f"}, - {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393"}, - {file = "propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0"}, - {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a"}, - {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be"}, - {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc"}, - {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36"}, - {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455"}, - {file = "propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85"}, - {file = "propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1"}, - {file = "propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9"}, - {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff"}, - {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb"}, - {file = "propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac"}, - {file = "propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888"}, - {file = "propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc"}, - {file = "propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a"}, - {file = "propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781"}, - {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183"}, - {file = "propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19"}, - {file = "propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f"}, - {file = "propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938"}, - {file = "propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237"}, - {file = "propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d"}, -] - -[[package]] -name = "pydantic" -version = "2.12.5" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, - {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.41.5" -typing-extensions = ">=4.14.1" -typing-inspection = ">=0.4.2" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] - -[[package]] -name = "pydantic-core" -version = "2.41.5" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, - {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, - {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, - {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, - {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, - {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, - {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, - {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, - {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, - {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, - {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, - {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, - {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, - {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, - {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, - {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, - {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, - {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, - {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, - {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, - {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, - {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, - {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, - {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, - {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, - {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, - {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, - {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, -] - -[package.dependencies] -typing-extensions = ">=4.14.1" - -[[package]] -name = "pydantic-settings" -version = "2.11.0" -description = "Settings management using Pydantic" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c"}, - {file = "pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180"}, -] - -[package.dependencies] -pydantic = ">=2.7.0" -python-dotenv = ">=0.21.0" -typing-inspection = ">=0.4.0" - -[package.extras] -aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"] -azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] -gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] -toml = ["tomli (>=2.0.1)"] -yaml = ["pyyaml (>=6.0.1)"] - -[[package]] -name = "pygments" -version = "2.19.2" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pytest" -version = "8.4.2" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, - {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, -] - -[package.dependencies] -colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} -iniconfig = ">=1" -packaging = ">=20" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-asyncio" -version = "1.2.0" -description = "Pytest support for asyncio" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99"}, - {file = "pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57"}, -] - -[package.dependencies] -backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} -pytest = ">=8.2,<9" -typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} - -[package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] - -[[package]] -name = "pytest-cov" -version = "7.0.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.9" -files = [ - {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, - {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, -] - -[package.dependencies] -coverage = {version = ">=7.10.6", extras = ["toml"]} -pluggy = ">=1.2" -pytest = ">=7" - -[package.extras] -testing = ["process-tests", "pytest-xdist", "virtualenv"] - -[[package]] -name = "python-discovery" -version = "1.1.3" -description = "Python interpreter discovery" -optional = false -python-versions = ">=3.8" -files = [ - {file = "python_discovery-1.1.3-py3-none-any.whl", hash = "sha256:90e795f0121bc84572e737c9aa9966311b9fde44ffb88a5953b3ec9b31c6945e"}, - {file = "python_discovery-1.1.3.tar.gz", hash = "sha256:7acca36e818cd88e9b2ba03e045ad7e93e1713e29c6bbfba5d90202310b7baa5"}, -] - -[package.dependencies] -filelock = ">=3.15.4" -platformdirs = ">=4.3.6,<5" - -[package.extras] -docs = ["furo (>=2025.12.19)", "sphinx (>=9.1)", "sphinx-autodoc-typehints (>=3.6.3)", "sphinxcontrib-mermaid (>=2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.5.4)", "pytest (>=8.3.5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"] - -[[package]] -name = "python-dotenv" -version = "1.2.1" -description = "Read key-value pairs from a .env file and set them as environment variables" -optional = false -python-versions = ">=3.9" -files = [ - {file = "python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61"}, - {file = "python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "pyyaml" -version = "6.0.3" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, - {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, - {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, - {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, - {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, - {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, - {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, - {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, - {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, - {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, - {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, - {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, - {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, - {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, - {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, - {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, - {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, - {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, - {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, - {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, - {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, - {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, - {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, - {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, - {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, - {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, - {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, - {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, - {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, - {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, - {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, - {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, - {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, - {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, - {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, - {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, - {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, - {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, - {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, - {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, - {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, - {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, - {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, - {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, - {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, - {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, - {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, - {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, - {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, -] - -[[package]] -name = "requests" -version = "2.32.5" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.9" -files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset_normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "requests-cache" -version = "1.3.1" -description = "A persistent cache for python requests" -optional = false -python-versions = ">=3.8" -files = [ - {file = "requests_cache-1.3.1-py3-none-any.whl", hash = "sha256:43a67448c3b2964c631ac7027b84607f2f63438e28104b68ad2211f32d9f606c"}, - {file = "requests_cache-1.3.1.tar.gz", hash = "sha256:784e9d07f72db4fe234830a065230c59eb446489528f271ba288c640897e47c4"}, -] - -[package.dependencies] -attrs = ">=21.2" -cattrs = ">=22.2" -platformdirs = ">=2.5" -requests = ">=2.22" -url-normalize = ">=2.0" -urllib3 = ">=1.25.5" - -[package.extras] -all = ["boto3 (>=1.15)", "botocore (>=1.18)", "itsdangerous (>=2.0)", "orjson (>=3.0)", "pymongo (>=3)", "pyyaml (>=6.0.1)", "redis (>=3)", "ujson (>=5.4)"] -dynamodb = ["boto3 (>=1.15)", "botocore (>=1.18)"] -mongodb = ["pymongo (>=3)"] -redis = ["redis (>=3)"] -security = ["itsdangerous (>=2.0)"] -yaml = ["pyyaml (>=6.0.1)"] - -[[package]] -name = "ruff" -version = "0.15.6" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -files = [ - {file = "ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff"}, - {file = "ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3"}, - {file = "ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb"}, - {file = "ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8"}, - {file = "ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e"}, - {file = "ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15"}, - {file = "ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9"}, - {file = "ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab"}, - {file = "ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e"}, - {file = "ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c"}, - {file = "ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512"}, - {file = "ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0"}, - {file = "ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb"}, - {file = "ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0"}, - {file = "ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c"}, - {file = "ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406"}, - {file = "ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837"}, - {file = "ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4"}, -] - -[[package]] -name = "simplejson" -version = "3.20.2" -description = "Simple, fast, extensible JSON encoder/decoder for Python" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.5" -files = [ - {file = "simplejson-3.20.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:11847093fd36e3f5a4f595ff0506286c54885f8ad2d921dfb64a85bce67f72c4"}, - {file = "simplejson-3.20.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d291911d23b1ab8eb3241204dd54e3ec60ddcd74dfcb576939d3df327205865"}, - {file = "simplejson-3.20.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:da6d16d7108d366bbbf1c1f3274662294859c03266e80dd899fc432598115ea4"}, - {file = "simplejson-3.20.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9ddf9a07694c5bbb4856271cbc4247cc6cf48f224a7d128a280482a2f78bae3d"}, - {file = "simplejson-3.20.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:3a0d2337e490e6ab42d65a082e69473717f5cc75c3c3fb530504d3681c4cb40c"}, - {file = "simplejson-3.20.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8ba88696351ed26a8648f8378a1431223f02438f8036f006d23b4f5b572778fa"}, - {file = "simplejson-3.20.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:00bcd408a4430af99d1f8b2b103bb2f5133bb688596a511fcfa7db865fbb845e"}, - {file = "simplejson-3.20.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4fc62feb76f590ccaff6f903f52a01c58ba6423171aa117b96508afda9c210f0"}, - {file = "simplejson-3.20.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6d7286dc11af60a2f76eafb0c2acde2d997e87890e37e24590bb513bec9f1bc5"}, - {file = "simplejson-3.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c01379b4861c3b0aa40cba8d44f2b448f5743999aa68aaa5d3ef7049d4a28a2d"}, - {file = "simplejson-3.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a16b029ca25645b3bc44e84a4f941efa51bf93c180b31bd704ce6349d1fc77c1"}, - {file = "simplejson-3.20.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e22a5fb7b1437ffb057e02e1936a3bfb19084ae9d221ec5e9f4cf85f69946b6"}, - {file = "simplejson-3.20.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8b6ff02fc7b8555c906c24735908854819b0d0dc85883d453e23ca4c0445d01"}, - {file = "simplejson-3.20.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bfc1c396ad972ba4431130b42307b2321dba14d988580c1ac421ec6a6b7cee3"}, - {file = "simplejson-3.20.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a97249ee1aee005d891b5a211faf58092a309f3d9d440bc269043b08f662eda"}, - {file = "simplejson-3.20.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f1036be00b5edaddbddbb89c0f80ed229714a941cfd21e51386dc69c237201c2"}, - {file = "simplejson-3.20.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5d6f5bacb8cdee64946b45f2680afa3f54cd38e62471ceda89f777693aeca4e4"}, - {file = "simplejson-3.20.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8db6841fb796ec5af632f677abf21c6425a1ebea0d9ac3ef1a340b8dc69f52b8"}, - {file = "simplejson-3.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0a341f7cc2aae82ee2b31f8a827fd2e51d09626f8b3accc441a6907c88aedb7"}, - {file = "simplejson-3.20.2-cp310-cp310-win32.whl", hash = "sha256:27f9c01a6bc581d32ab026f515226864576da05ef322d7fc141cd8a15a95ce53"}, - {file = "simplejson-3.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0a63ec98a4547ff366871bf832a7367ee43d047bcec0b07b66c794e2137b476"}, - {file = "simplejson-3.20.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:06190b33cd7849efc413a5738d3da00b90e4a5382fd3d584c841ac20fb828c6f"}, - {file = "simplejson-3.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ad4eac7d858947a30d2c404e61f16b84d16be79eb6fb316341885bdde864fa8"}, - {file = "simplejson-3.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b392e11c6165d4a0fde41754a0e13e1d88a5ad782b245a973dd4b2bdb4e5076a"}, - {file = "simplejson-3.20.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51eccc4e353eed3c50e0ea2326173acdc05e58f0c110405920b989d481287e51"}, - {file = "simplejson-3.20.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:306e83d7c331ad833d2d43c76a67f476c4b80c4a13334f6e34bb110e6105b3bd"}, - {file = "simplejson-3.20.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f820a6ac2ef0bc338ae4963f4f82ccebdb0824fe9caf6d660670c578abe01013"}, - {file = "simplejson-3.20.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e7a066528a5451433eb3418184f05682ea0493d14e9aae690499b7e1eb6b81"}, - {file = "simplejson-3.20.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:438680ddde57ea87161a4824e8de04387b328ad51cfdf1eaf723623a3014b7aa"}, - {file = "simplejson-3.20.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cac78470ae68b8d8c41b6fca97f5bf8e024ca80d5878c7724e024540f5cdaadb"}, - {file = "simplejson-3.20.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7524e19c2da5ef281860a3d74668050c6986be15c9dd99966034ba47c68828c2"}, - {file = "simplejson-3.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e9b6d845a603b2eef3394eb5e21edb8626cd9ae9a8361d14e267eb969dbe413"}, - {file = "simplejson-3.20.2-cp311-cp311-win32.whl", hash = "sha256:47d8927e5ac927fdd34c99cc617938abb3624b06ff86e8e219740a86507eb961"}, - {file = "simplejson-3.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:ba4edf3be8e97e4713d06c3d302cba1ff5c49d16e9d24c209884ac1b8455520c"}, - {file = "simplejson-3.20.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4376d5acae0d1e91e78baeba4ee3cf22fbf6509d81539d01b94e0951d28ec2b6"}, - {file = "simplejson-3.20.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f8fe6de652fcddae6dec8f281cc1e77e4e8f3575249e1800090aab48f73b4259"}, - {file = "simplejson-3.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25ca2663d99328d51e5a138f22018e54c9162438d831e26cfc3458688616eca8"}, - {file = "simplejson-3.20.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12a6b2816b6cab6c3fd273d43b1948bc9acf708272074c8858f579c394f4cbc9"}, - {file = "simplejson-3.20.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac20dc3fcdfc7b8415bfc3d7d51beccd8695c3f4acb7f74e3a3b538e76672868"}, - {file = "simplejson-3.20.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db0804d04564e70862ef807f3e1ace2cc212ef0e22deb1b3d6f80c45e5882c6b"}, - {file = "simplejson-3.20.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:979ce23ea663895ae39106946ef3d78527822d918a136dbc77b9e2b7f006237e"}, - {file = "simplejson-3.20.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a2ba921b047bb029805726800819675249ef25d2f65fd0edb90639c5b1c3033c"}, - {file = "simplejson-3.20.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:12d3d4dc33770069b780cc8f5abef909fe4a3f071f18f55f6d896a370fd0f970"}, - {file = "simplejson-3.20.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aff032a59a201b3683a34be1169e71ddda683d9c3b43b261599c12055349251e"}, - {file = "simplejson-3.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:30e590e133b06773f0dc9c3f82e567463df40598b660b5adf53eb1c488202544"}, - {file = "simplejson-3.20.2-cp312-cp312-win32.whl", hash = "sha256:8d7be7c99939cc58e7c5bcf6bb52a842a58e6c65e1e9cdd2a94b697b24cddb54"}, - {file = "simplejson-3.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:2c0b4a67e75b945489052af6590e7dca0ed473ead5d0f3aad61fa584afe814ab"}, - {file = "simplejson-3.20.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90d311ba8fcd733a3677e0be21804827226a57144130ba01c3c6a325e887dd86"}, - {file = "simplejson-3.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:feed6806f614bdf7f5cb6d0123cb0c1c5f40407ef103aa935cffaa694e2e0c74"}, - {file = "simplejson-3.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b1d8d7c3e1a205c49e1aee6ba907dcb8ccea83651e6c3e2cb2062f1e52b0726"}, - {file = "simplejson-3.20.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:552f55745044a24c3cb7ec67e54234be56d5d6d0e054f2e4cf4fb3e297429be5"}, - {file = "simplejson-3.20.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2da97ac65165d66b0570c9e545786f0ac7b5de5854d3711a16cacbcaa8c472d"}, - {file = "simplejson-3.20.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f59a12966daa356bf68927fca5a67bebac0033cd18b96de9c2d426cd11756cd0"}, - {file = "simplejson-3.20.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133ae2098a8e162c71da97cdab1f383afdd91373b7ff5fe65169b04167da976b"}, - {file = "simplejson-3.20.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7977640af7b7d5e6a852d26622057d428706a550f7f5083e7c4dd010a84d941f"}, - {file = "simplejson-3.20.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b530ad6d55e71fa9e93e1109cf8182f427a6355848a4ffa09f69cc44e1512522"}, - {file = "simplejson-3.20.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bd96a7d981bf64f0e42345584768da4435c05b24fd3c364663f5fbc8fabf82e3"}, - {file = "simplejson-3.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f28ee755fadb426ba2e464d6fcf25d3f152a05eb6b38e0b4f790352f5540c769"}, - {file = "simplejson-3.20.2-cp313-cp313-win32.whl", hash = "sha256:472785b52e48e3eed9b78b95e26a256f59bb1ee38339be3075dad799e2e1e661"}, - {file = "simplejson-3.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:a1a85013eb33e4820286139540accbe2c98d2da894b2dcefd280209db508e608"}, - {file = "simplejson-3.20.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a135941a50795c934bdc9acc74e172b126e3694fe26de3c0c1bc0b33ea17e6ce"}, - {file = "simplejson-3.20.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ba488decb18738f5d6bd082018409689ed8e74bc6c4d33a0b81af6edf1c9f4"}, - {file = "simplejson-3.20.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d81f8e982923d5e9841622ff6568be89756428f98a82c16e4158ac32b92a3787"}, - {file = "simplejson-3.20.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdad497ccb1edc5020bef209e9c3e062a923e8e6fca5b8a39f0fb34380c8a66c"}, - {file = "simplejson-3.20.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a3f1db97bcd9fb592928159af7a405b18df7e847cbcc5682a209c5b2ad5d6b1"}, - {file = "simplejson-3.20.2-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:215b65b0dc2c432ab79c430aa4f1e595f37b07a83c1e4c4928d7e22e6b49a748"}, - {file = "simplejson-3.20.2-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:ece4863171ba53f086a3bfd87f02ec3d6abc586f413babfc6cf4de4d84894620"}, - {file = "simplejson-3.20.2-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:4a76d7c47d959afe6c41c88005f3041f583a4b9a1783cf341887a3628a77baa0"}, - {file = "simplejson-3.20.2-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:e9b0523582a57d9ea74f83ecefdffe18b2b0a907df1a9cef06955883341930d8"}, - {file = "simplejson-3.20.2-cp36-cp36m-win32.whl", hash = "sha256:16366591c8e08a4ac76b81d76a3fc97bf2bcc234c9c097b48d32ea6bfe2be2fe"}, - {file = "simplejson-3.20.2-cp36-cp36m-win_amd64.whl", hash = "sha256:732cf4c4ac1a258b4e9334e1e40a38303689f432497d3caeb491428b7547e782"}, - {file = "simplejson-3.20.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6c3a98e21e5f098e4f982ef302ebb1e681ff16a5d530cfce36296bea58fe2396"}, - {file = "simplejson-3.20.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10cf9ca1363dc3711c72f4ec7c1caed2bbd9aaa29a8d9122e31106022dc175c6"}, - {file = "simplejson-3.20.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:106762f8aedf3fc3364649bfe8dc9a40bf5104f872a4d2d86bae001b1af30d30"}, - {file = "simplejson-3.20.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b21659898b7496322e99674739193f81052e588afa8b31b6a1c7733d8829b925"}, - {file = "simplejson-3.20.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78fa1db6a02bca88829f2b2057c76a1d2dc2fccb8c5ff1199e352f213e9ec719"}, - {file = "simplejson-3.20.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:156139d94b660448ec8a4ea89f77ec476597f752c2ff66432d3656704c66b40e"}, - {file = "simplejson-3.20.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:b2620ac40be04dff08854baf6f4df10272f67079f61ed1b6274c0e840f2e2ae1"}, - {file = "simplejson-3.20.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:9ccef5b5d3e3ac5d9da0a0ca1d2de8cf2b0fb56b06aa0ab79325fa4bcc5a1d60"}, - {file = "simplejson-3.20.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:f526304c2cc9fd8b8d18afacb75bc171650f83a7097b2c92ad6a431b5d7c1b72"}, - {file = "simplejson-3.20.2-cp37-cp37m-win32.whl", hash = "sha256:e0f661105398121dd48d9987a2a8f7825b8297b3b2a7fe5b0d247370396119d5"}, - {file = "simplejson-3.20.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dab98625b3d6821e77ea59c4d0e71059f8063825a0885b50ed410e5c8bd5cb66"}, - {file = "simplejson-3.20.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b8205f113082e7d8f667d6cd37d019a7ee5ef30b48463f9de48e1853726c6127"}, - {file = "simplejson-3.20.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fc8da64929ef0ff16448b602394a76fd9968a39afff0692e5ab53669df1f047f"}, - {file = "simplejson-3.20.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfe704864b5fead4f21c8d448a89ee101c9b0fc92a5f40b674111da9272b3a90"}, - {file = "simplejson-3.20.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40ca7cbe7d2f423b97ed4e70989ef357f027a7e487606628c11b79667639dc84"}, - {file = "simplejson-3.20.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cec1868b237fe9fb2d466d6ce0c7b772e005aadeeda582d867f6f1ec9710cad"}, - {file = "simplejson-3.20.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:792debfba68d8dd61085ffb332d72b9f5b38269cda0c99f92c7a054382f55246"}, - {file = "simplejson-3.20.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e022b2c4c54cb4855e555f64aa3377e3e5ca912c372fa9e3edcc90ebbad93dce"}, - {file = "simplejson-3.20.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:5de26f11d5aca575d3825dddc65f69fdcba18f6ca2b4db5cef16f41f969cef15"}, - {file = "simplejson-3.20.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:e2162b2a43614727ec3df75baeda8881ab129824aa1b49410d4b6c64f55a45b4"}, - {file = "simplejson-3.20.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e11a1d6b2f7e72ca546bdb4e6374b237ebae9220e764051b867111df83acbd13"}, - {file = "simplejson-3.20.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:daf7cd18fe99eb427fa6ddb6b437cfde65125a96dc27b93a8969b6fe90a1dbea"}, - {file = "simplejson-3.20.2-cp38-cp38-win32.whl", hash = "sha256:da795ea5f440052f4f497b496010e2c4e05940d449ea7b5c417794ec1be55d01"}, - {file = "simplejson-3.20.2-cp38-cp38-win_amd64.whl", hash = "sha256:6a4b5e7864f952fcce4244a70166797d7b8fd6069b4286d3e8403c14b88656b6"}, - {file = "simplejson-3.20.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b3bf76512ccb07d47944ebdca44c65b781612d38b9098566b4bb40f713fc4047"}, - {file = "simplejson-3.20.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:214e26acf2dfb9ff3314e65c4e168a6b125bced0e2d99a65ea7b0f169db1e562"}, - {file = "simplejson-3.20.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2fb1259ca9c385b0395bad59cdbf79535a5a84fb1988f339a49bfbc57455a35a"}, - {file = "simplejson-3.20.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c34e028a2ba8553a208ded1da5fa8501833875078c4c00a50dffc33622057881"}, - {file = "simplejson-3.20.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b538f9d9e503b0dd43af60496780cb50755e4d8e5b34e5647b887675c1ae9fee"}, - {file = "simplejson-3.20.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab998e416ded6c58f549a22b6a8847e75a9e1ef98eb9fbb2863e1f9e61a4105b"}, - {file = "simplejson-3.20.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a8f1c307edf5fbf0c6db3396c5d3471409c4a40c7a2a466fbc762f20d46601a"}, - {file = "simplejson-3.20.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5a7bbac80bdb82a44303f5630baee140aee208e5a4618e8b9fde3fc400a42671"}, - {file = "simplejson-3.20.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5ef70ec8fe1569872e5a3e4720c1e1dcb823879a3c78bc02589eb88fab920b1f"}, - {file = "simplejson-3.20.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:cb11c09c99253a74c36925d461c86ea25f0140f3b98ff678322734ddc0f038d7"}, - {file = "simplejson-3.20.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:66f7c78c6ef776f8bd9afaad455e88b8197a51e95617bcc44b50dd974a7825ba"}, - {file = "simplejson-3.20.2-cp39-cp39-win32.whl", hash = "sha256:619ada86bfe3a5aa02b8222ca6bfc5aa3e1075c1fb5b3263d24ba579382df472"}, - {file = "simplejson-3.20.2-cp39-cp39-win_amd64.whl", hash = "sha256:44a6235e09ca5cc41aa5870a952489c06aa4aee3361ae46daa947d8398e57502"}, - {file = "simplejson-3.20.2-py3-none-any.whl", hash = "sha256:3b6bb7fb96efd673eac2e4235200bfffdc2353ad12c54117e1e4e2fc485ac017"}, - {file = "simplejson-3.20.2.tar.gz", hash = "sha256:5fe7a6ce14d1c300d80d08695b7f7e633de6cd72c80644021874d985b3393649"}, -] - -[[package]] -name = "snowballstemmer" -version = "3.0.1" -description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*" -files = [ - {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, - {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, -] - -[[package]] -name = "sphinx" -version = "7.4.7" -description = "Python documentation generator" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, - {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, -] - -[package.dependencies] -alabaster = ">=0.7.14,<0.8.0" -babel = ">=2.13" -colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} -docutils = ">=0.20,<0.22" -imagesize = ">=1.3" -importlib-metadata = {version = ">=6.0", markers = "python_version < \"3.10\""} -Jinja2 = ">=3.1" -packaging = ">=23.0" -Pygments = ">=2.17" -requests = ">=2.30.0" -snowballstemmer = ">=2.2" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = ">=2.0.0" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.9" -tomli = {version = ">=2", markers = "python_version < \"3.11\""} - -[package.extras] -docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=6.0)", "importlib-metadata (>=6.0)", "mypy (==1.10.1)", "pytest (>=6.0)", "ruff (==0.5.2)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-docutils (==0.21.0.20240711)", "types-requests (>=2.30.0)"] -test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] - -[[package]] -name = "sphinx-autodoc-typehints" -version = "1.25.3" -description = "Type hints (PEP 484) support for the Sphinx autodoc extension" -optional = false -python-versions = ">=3.8" -files = [ - {file = "sphinx_autodoc_typehints-1.25.3-py3-none-any.whl", hash = "sha256:d3da7fa9a9761eff6ff09f8b1956ae3090a2d4f4ad54aebcade8e458d6340835"}, - {file = "sphinx_autodoc_typehints-1.25.3.tar.gz", hash = "sha256:70db10b391acf4e772019765991d2de0ff30ec0899b9ba137706dc0b3c4835e0"}, -] - -[package.dependencies] -sphinx = ">=7.1.2" - -[package.extras] -docs = ["furo (>=2023.9.10)"] -numpy = ["nptyping (>=2.5)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.8)"] - -[[package]] -name = "sphinx-rtd-theme" -version = "2.0.0" -description = "Read the Docs theme for Sphinx" -optional = false -python-versions = ">=3.6" -files = [ - {file = "sphinx_rtd_theme-2.0.0-py2.py3-none-any.whl", hash = "sha256:ec93d0856dc280cf3aee9a4c9807c60e027c7f7b461b77aeffed682e68f0e586"}, - {file = "sphinx_rtd_theme-2.0.0.tar.gz", hash = "sha256:bd5d7b80622406762073a04ef8fadc5f9151261563d47027de09910ce03afe6b"}, -] - -[package.dependencies] -docutils = "<0.21" -sphinx = ">=5,<8" -sphinxcontrib-jquery = ">=4,<5" - -[package.extras] -dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "2.0.0" -description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, - {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "2.0.0" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, - {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.1.0" -description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, - {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["html5lib", "pytest"] - -[[package]] -name = "sphinxcontrib-jquery" -version = "4.1" -description = "Extension to include jQuery on newer Sphinx releases" -optional = false -python-versions = ">=2.7" -files = [ - {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, - {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, -] - -[package.dependencies] -Sphinx = ">=1.8" - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -description = "A sphinx extension which renders display math in HTML via JavaScript" -optional = false -python-versions = ">=3.5" -files = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] - -[package.extras] -test = ["flake8", "mypy", "pytest"] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "2.0.0" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, - {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["defusedxml (>=0.7.1)", "pytest"] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "2.0.0" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, - {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "tomli" -version = "2.4.0" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -files = [ - {file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"}, - {file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"}, - {file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"}, - {file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"}, - {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"}, - {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"}, - {file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"}, - {file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"}, - {file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"}, - {file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"}, - {file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"}, - {file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"}, - {file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"}, - {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"}, - {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"}, - {file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"}, - {file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"}, - {file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"}, - {file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"}, - {file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"}, - {file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"}, - {file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"}, - {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"}, - {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"}, - {file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"}, - {file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"}, - {file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"}, - {file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"}, - {file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"}, - {file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"}, - {file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"}, - {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"}, - {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"}, - {file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"}, - {file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"}, - {file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"}, - {file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"}, - {file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"}, - {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"}, - {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"}, - {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"}, - {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"}, - {file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"}, - {file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"}, - {file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"}, - {file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"}, - {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}, -] - -[[package]] -name = "types-docutils" -version = "0.17.7" -description = "Typing stubs for docutils" -optional = false -python-versions = "*" -files = [ - {file = "types-docutils-0.17.7.tar.gz", hash = "sha256:3d856ea26551a998c8e2c99a0bafe5e4d391811955f17dab6c9be73b0fc67b66"}, - {file = "types_docutils-0.17.7-py3-none-any.whl", hash = "sha256:d35acc7e3308b464b82b54a2e641b9f132dfe0b14f199f220c58a696ce585428"}, -] - -[[package]] -name = "types-requests" -version = "2.32.4.20260107" -description = "Typing stubs for requests" -optional = false -python-versions = ">=3.9" -files = [ - {file = "types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d"}, - {file = "types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f"}, -] - -[package.dependencies] -urllib3 = ">=2" - -[[package]] -name = "types-simplejson" -version = "3.20.0.20250822" -description = "Typing stubs for simplejson" -optional = false -python-versions = ">=3.9" -files = [ - {file = "types_simplejson-3.20.0.20250822-py3-none-any.whl", hash = "sha256:b5e63ae220ac7a1b0bb9af43b9cb8652237c947981b2708b0c776d3b5d8fa169"}, - {file = "types_simplejson-3.20.0.20250822.tar.gz", hash = "sha256:2b0bfd57a6beed3b932fd2c3c7f8e2f48a7df3978c9bba43023a32b3741a95b0"}, -] - -[[package]] -name = "types-toml" -version = "0.10.8.20240310" -description = "Typing stubs for toml" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-toml-0.10.8.20240310.tar.gz", hash = "sha256:3d41501302972436a6b8b239c850b26689657e25281b48ff0ec06345b8830331"}, - {file = "types_toml-0.10.8.20240310-py3-none-any.whl", hash = "sha256:627b47775d25fa29977d9c70dc0cbab3f314f32c8d8d0c012f2ef5de7aaec05d"}, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -description = "Runtime typing introspection tools" -optional = false -python-versions = ">=3.9" -files = [ - {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, - {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, -] - -[package.dependencies] -typing-extensions = ">=4.12.0" - -[[package]] -name = "url-normalize" -version = "2.2.1" -description = "URL normalization for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "url_normalize-2.2.1-py3-none-any.whl", hash = "sha256:3deb687587dc91f7b25c9ae5162ffc0f057ae85d22b1e15cf5698311247f567b"}, - {file = "url_normalize-2.2.1.tar.gz", hash = "sha256:74a540a3b6eba1d95bdc610c24f2c0141639f3ba903501e61a52a8730247ff37"}, -] - -[package.dependencies] -idna = ">=3.3" - -[package.extras] -dev = ["mypy", "pre-commit", "pytest", "pytest-cov", "pytest-socket", "ruff"] - -[[package]] -name = "urllib3" -version = "2.6.3" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -files = [ - {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, - {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, -] - -[package.extras] -brotli = ["brotli (>=1.2.0)", "brotlicffi (>=1.2.0.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["backports-zstd (>=1.0.0)"] - -[[package]] -name = "virtualenv" -version = "21.2.0" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.8" -files = [ - {file = "virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f"}, - {file = "virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = [ - {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""}, - {version = ">=3.16.1,<=3.19.1", markers = "python_version < \"3.10\""}, -] -platformdirs = ">=3.9.1,<5" -python-discovery = ">=1" -typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} - -[[package]] -name = "websockets" -version = "15.0.1" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -optional = false -python-versions = ">=3.9" -files = [ - {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, - {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, - {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, - {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, - {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, - {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, - {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, - {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, - {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, - {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, - {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, - {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, - {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, - {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, - {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, - {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, - {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, - {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, - {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, - {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, - {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, - {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, - {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, - {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, -] - -[[package]] -name = "yarl" -version = "1.22.0" -description = "Yet another URL library" -optional = false -python-versions = ">=3.9" -files = [ - {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e"}, - {file = "yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f"}, - {file = "yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb"}, - {file = "yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737"}, - {file = "yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467"}, - {file = "yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea"}, - {file = "yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca"}, - {file = "yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b"}, - {file = "yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511"}, - {file = "yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6"}, - {file = "yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e"}, - {file = "yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6"}, - {file = "yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e"}, - {file = "yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca"}, - {file = "yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b"}, - {file = "yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376"}, - {file = "yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f"}, - {file = "yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2"}, - {file = "yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82"}, - {file = "yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d"}, - {file = "yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520"}, - {file = "yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8"}, - {file = "yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c"}, - {file = "yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74"}, - {file = "yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53"}, - {file = "yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a"}, - {file = "yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2"}, - {file = "yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02"}, - {file = "yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67"}, - {file = "yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95"}, - {file = "yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d"}, - {file = "yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b"}, - {file = "yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10"}, - {file = "yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3"}, - {file = "yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708"}, - {file = "yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f"}, - {file = "yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62"}, - {file = "yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03"}, - {file = "yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249"}, - {file = "yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b"}, - {file = "yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4"}, - {file = "yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683"}, - {file = "yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da"}, - {file = "yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd"}, - {file = "yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da"}, - {file = "yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2"}, - {file = "yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79"}, - {file = "yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33"}, - {file = "yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1"}, - {file = "yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca"}, - {file = "yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b"}, - {file = "yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093"}, - {file = "yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c"}, - {file = "yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e"}, - {file = "yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27"}, - {file = "yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1"}, - {file = "yarl-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748"}, - {file = "yarl-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859"}, - {file = "yarl-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890"}, - {file = "yarl-1.22.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e"}, - {file = "yarl-1.22.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8"}, - {file = "yarl-1.22.0-cp39-cp39-win32.whl", hash = "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b"}, - {file = "yarl-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed"}, - {file = "yarl-1.22.0-cp39-cp39-win_arm64.whl", hash = "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2"}, - {file = "yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff"}, - {file = "yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71"}, -] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" -propcache = ">=0.2.1" - -[[package]] -name = "zipp" -version = "3.23.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - -[metadata] -lock-version = "2.0" -python-versions = "^3.9,<4.0.0" -content-hash = "40a429f2d523620ce2bbc981f2ba90b38d7c96b10887221efcee583ff4de08c8" diff --git a/pyproject.toml b/pyproject.toml index 6e64d0a1..f16647ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,57 +1,47 @@ [build-system] -build-backend = "poetry.core.masonry.api" -requires = ["poetry-core>=1.0.0"] +build-backend = "hatchling.build" +requires = ["hatchling"] -[tool.poetry] -authors = ["GrandMoff100 "] -description = "Python Wrapper for Homeassistant's REST API" -documentation = "https://homeassistantapi.readthedocs.io" -homepage = "https://github.com/GrandMoff100/HomeAssistantAPI" -license = "GPL-3.0-or-later" +[project] name = "HomeAssistant-API" +version = "6.0.0" +description = "Python Wrapper for Homeassistant's REST API" readme = "README.md" -include = ["homeassistant_api/py.typed"] -repository = "https://github.com/GrandMoff100/HomeAssistantAPI" -version = "5.0.3" -packages = [{ include = "homeassistant_api" }] - -[tool.poetry.dependencies] -aiohttp = "^3" -aiohttp-client-cache = "^0" -pydantic = "^2" -python = "^3.9,<4.0.0" -requests = "^2" -requests-cache = "^1" -simplejson = "^3" -websockets = "^15" - -[tool.poetry.group.docs] -optional = true - -[tool.poetry.group.docs.dependencies] -sphinx-autodoc-typehints = "^1.25.2" -sphinx-rtd-theme = "^2.0.0" -autodoc-pydantic = "^2.0.1" - -[tool.poetry.group.styling] -optional = true +license = "GPL-3.0-or-later" +requires-python = ">=3.11,<4.0" +authors = [ + { name = "GrandMoff100", email = "minecraftcrusher100@gmail.com" }, +] +dependencies = [ + "niquests[ws]>=3", + "pydantic>=2,<3", + "simplejson>=3,<4", +] -[tool.poetry.group.styling.dependencies] -pre-commit = "^2.17.0" -types-docutils = "^0.17.5" -types-requests = "^2.27.9" -types-simplejson = "^3.17.3" -types-toml = "^0.10.4" -mypy = "^1.8.0" -ruff = "^0.15" +[project.urls] +Documentation = "https://homeassistantapi.readthedocs.io" +Homepage = "https://github.com/GrandMoff100/HomeAssistantAPI" +Repository = "https://github.com/GrandMoff100/HomeAssistantAPI" -[tool.poetry.group.testing] -optional = true -[tool.poetry.group.testing.dependencies] -pytest-asyncio = "^1" -pytest-cov = "^7" -pytest = "^8" -aiosqlite = "^0.22" +[dependency-groups] +docs = [ + "sphinx-autodoc-typehints>=3,<4", + "sphinx-rtd-theme>=3,<4", + "autodoc-pydantic>=2,<3", +] +dev = [ + "types-docutils>=0.22", + "types-simplejson>=3.20", + "types-toml>=0.10", + "zuban>=0.7", + "ruff>=0.15", + "pytest-asyncio>=1", + "pytest-cov>=7", + "pytest>=8", + "aiosqlite>=0.22", + "prek>=0.3.8", + "pre-commit>=4.5.1", +] [tool.pytest.ini_options] asyncio_mode = "auto" @@ -64,37 +54,60 @@ log_cli_date_format = "%Y-%m-%d %H:%M:%S" [tool.coverage.report] exclude_lines = [ + "pragma: no cover", "if TYPE_CHECKING:", "def __repr__\\(self\\) -> str:", "raise RequestTimeoutError\\(", - "except \\(json\\.decoder\\.JSONDecodeError, simplejson.decoder\\.JSONDecodeError\\) as err:", "pass", - "last_updated < \\(now := datetime\\.now\\(\\)\\) - wait", - "pragma: no cover" + "msg = r?f?\".+\"", + "raise \\w+Error\\(msg\\)", + "if not isinstance\\(", + "except RequestsConnectionError: # noqa: PERF203", ] + +[tool.ruff] +target-version = "py310" + [tool.ruff.lint] exclude = [ '.git', '__pycache__', '.github', ] -ignore = ['E501'] -select = [ - 'E', - 'F', - 'W', +select = ['ALL'] +ignore = [ + 'E501', + "D", + "G004", + "ANN401", + "FBT", + "TD", + "FIX", ] [tool.ruff.lint.per-file-ignores] "__init__.py" = ['F401'] "conf.py" = ['E402'] +"docs/*" = ['ALL'] +"examples/*" = ['INP001'] +"tests/*" = [ + "S101", + "ANN202", + "SLF001", + "PLR2004", + "INP001", +] -[tool.isort] -profile = "black" +[tool.ruff.lint.isort] +force-single-line = true -[tool.mypy] -disable_error_code = [ - "no-untyped-def", - "name-defined", -] +[tool.zuban] +exclude = ["^docs/"] +untyped_function_return_mode = "inferred" + +[tool.hatch.build.targets.wheel] +packages = ["homeassistant_api"] + +[tool.hatch.build.targets.sdist] +include = ["homeassistant_api", "homeassistant_api/py.typed"] diff --git a/tests/conftest.py b/tests/conftest.py index 78ef2373..85a6439a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,72 +1,66 @@ import logging import os -from typing import AsyncGenerator, Generator, Literal, cast +import time +from collections.abc import AsyncGenerator +from collections.abc import Generator +from http import HTTPMethod import pytest import pytest_asyncio +from niquests.exceptions import ConnectionError as RequestsConnectionError -from homeassistant_api import Client, WebsocketClient - -logging.basicConfig(level=logging.INFO) +from homeassistant_api import AsyncClient +from homeassistant_api import AsyncWebsocketClient +from homeassistant_api import Client +from homeassistant_api import WebsocketClient +logger = logging.getLogger(__name__) TIMEOUT = 300 +HA_URL = os.environ["HOMEASSISTANTAPI_URL"] +HA_WS_URL = os.environ["HOMEASSISTANTAPI_WS_URL"] +HA_TOKEN = os.environ["HOMEASSISTANTAPI_TOKEN"] -@pytest.fixture(name="wait_for_server", scope="session") -def wait_for_server_fixture() -> None: - """Waits for the server to be ready.""" - client = Client( - os.environ["HOMEASSISTANTAPI_URL"], - os.environ["HOMEASSISTANTAPI_TOKEN"], - ) - logging.info("Waiting for server to be ready...") - client.request(method="get", path="", timeout=TIMEOUT) - logging.info("Server is ready.") +def pytest_sessionstart() -> None: + """Wait for the HA server to be ready before running any tests.""" + client = Client(HA_URL, HA_TOKEN) + deadline = time.monotonic() + TIMEOUT + while time.monotonic() < deadline: + try: + client.request(method=HTTPMethod.GET, path="", timeout=5) + logger.info("Server is ready.") + except RequestsConnectionError: # noqa: PERF203 + logger.info("Server not ready, retrying...") + time.sleep(2) + else: + return + msg = f"Server at {HA_URL} not ready after {TIMEOUT}s" + raise TimeoutError(msg) @pytest.fixture(name="cached_client", scope="session") -def setup_cached_client(wait_for_server) -> Generator[Client, None, None]: +def setup_cached_client() -> Generator[Client, None, None]: """Initializes the Client and enters a cached session.""" - with Client( - os.environ["HOMEASSISTANTAPI_URL"], - os.environ["HOMEASSISTANTAPI_TOKEN"], - ) as client: - yield cast(Client, client) + with Client(HA_URL, HA_TOKEN) as client: + yield client @pytest_asyncio.fixture(name="async_cached_client", scope="session") -async def setup_async_cached_client( - wait_for_server: Literal[None], -) -> AsyncGenerator[Client, None]: - """Initializes the Client and enters an async cached session.""" - async with Client( - os.environ["HOMEASSISTANTAPI_URL"], - os.environ["HOMEASSISTANTAPI_TOKEN"], - use_async=True, - ) as client: +async def setup_async_cached_client() -> AsyncGenerator[AsyncClient, None]: + """Initializes the AsyncClient and enters an async cached session.""" + async with AsyncClient(HA_URL, HA_TOKEN) as client: yield client @pytest.fixture(name="websocket_client", scope="session") -def setup_websocket_client( - wait_for_server: Literal[None], -) -> Generator[WebsocketClient, None, None]: +def setup_websocket_client() -> Generator[WebsocketClient, None, None]: """Initializes the Client and enters a WebSocket session.""" - with WebsocketClient( - os.environ["HOMEASSISTANTAPI_WS_URL"], - os.environ["HOMEASSISTANTAPI_TOKEN"], - ) as client: + with WebsocketClient(HA_WS_URL, HA_TOKEN) as client: yield client -@pytest.fixture(name="async_websocket_client", scope="session") -async def setup_async_websocket_client( - wait_for_server: Literal[None], -) -> AsyncGenerator[WebsocketClient, None]: - """Initializes the Client and enters an async WebSocket session.""" - async with WebsocketClient( - os.environ["HOMEASSISTANTAPI_WS_URL"], - os.environ["HOMEASSISTANTAPI_TOKEN"], - use_async=True, - ) as client: +@pytest_asyncio.fixture(name="async_websocket_client", scope="session") +async def setup_async_websocket_client() -> AsyncGenerator[AsyncWebsocketClient, None]: + """Initializes the AsyncWebsocketClient and enters an async WebSocket session.""" + async with AsyncWebsocketClient(HA_WS_URL, HA_TOKEN) as client: yield client diff --git a/tests/test_client.py b/tests/test_client.py index 6a11939b..b4748eda 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,16 +1,20 @@ import os +from datetime import datetime -import aiohttp_client_cache.session -import requests_cache +import niquests -from homeassistant_api import Client, WebsocketClient +from homeassistant_api import AsyncClient +from homeassistant_api import AsyncWebsocketClient +from homeassistant_api import Client +from homeassistant_api import WebsocketClient +from homeassistant_api.baseclient import BaseClient -def test_custom_cached_session() -> None: +def test_custom_session() -> None: with Client( os.environ["HOMEASSISTANTAPI_URL"], os.environ["HOMEASSISTANTAPI_TOKEN"], - cache_session=requests_cache.CachedSession(), + session=niquests.Session(), ): pass @@ -19,32 +23,23 @@ def test_default_session() -> None: with Client( os.environ["HOMEASSISTANTAPI_URL"], os.environ["HOMEASSISTANTAPI_TOKEN"], - cache_session=False, ): pass -async def test_custom_async_cached_session() -> None: - async with Client( +async def test_custom_async_session() -> None: + async with AsyncClient( os.environ["HOMEASSISTANTAPI_URL"], os.environ["HOMEASSISTANTAPI_TOKEN"], - async_cache_session=aiohttp_client_cache.session.CachedSession( - cache=aiohttp_client_cache.SQLiteBackend( - cache_name="test_custom_async_cached_session.sqlite", - expire_after=10, - ), - ), - use_async=True, + session=niquests.AsyncSession(), ): pass async def test_default_async_session() -> None: - async with Client( + async with AsyncClient( os.environ["HOMEASSISTANTAPI_URL"], os.environ["HOMEASSISTANTAPI_TOKEN"], - async_cache_session=False, - use_async=True, ): pass @@ -58,9 +53,52 @@ def test_websocket_client_ping() -> None: async def test_async_websocket_client_ping() -> None: - async with WebsocketClient( + async with AsyncWebsocketClient( os.environ["HOMEASSISTANTAPI_WS_URL"], os.environ["HOMEASSISTANTAPI_TOKEN"], - use_async=True, ) as client: - assert (await client.async_ping_latency()) > 0 + assert (await client.ping_latency()) > 0 + + +# --- BaseClient: prepare_get_entity_histories_params with naive timestamps --- + + +def test_prepare_entity_histories_naive_timestamps() -> None: + """Tests that naive (tzinfo=None) timestamps are converted to local timezone.""" + naive_start = datetime(2024, 1, 1, 12, 0, 0) # noqa: DTZ001 + naive_end = datetime(2024, 6, 1, 12, 0, 0) # noqa: DTZ001 + params, url = BaseClient.prepare_get_entity_histories_params( + start_timestamp=naive_start, + end_timestamp=naive_end, + ) + # Naive timestamps should get a timezone attached + start_time = datetime.fromisoformat(url.split("/")[-1]) + assert start_time.tzinfo is not None, "start_timestamp should have timezone offset" + end_time_str = params["end_time"] + assert end_time_str is not None + end_time = datetime.fromisoformat(end_time_str) + assert end_time.tzinfo is not None, "end_time should have timezone offset" + + +# --- BaseClient: prepare_get_logbook_entry_params --- + + +def test_prepare_logbook_entry_no_start_timestamp() -> None: + """Tests logbook params without a start_timestamp return base 'logbook' path.""" + params, url = BaseClient.prepare_get_logbook_entry_params( + filter_entities=["light.kitchen", "light.bedroom"], + end_timestamp=datetime(2024, 6, 1, 12, 0, 0), # noqa: DTZ001 + ) + assert url == "logbook" + assert "light.kitchen,light.bedroom" in params["entity"] + assert "end_time" in params + + +def test_prepare_logbook_entry_string_timestamps() -> None: + """Tests logbook params with string timestamps pass through unchanged.""" + params, url = BaseClient.prepare_get_logbook_entry_params( + start_timestamp="2024-01-01T00:00:00", + end_timestamp="2024-06-01T00:00:00", + ) + assert "2024-01-01T00:00:00" in url + assert params["end_time"] == "2024-06-01T00:00:00" diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index 0745600d..98ad65cb 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -1,16 +1,22 @@ """Module for making sure endpoints that should succeed, do indeed succeed.""" import logging +from datetime import UTC from datetime import datetime import pytest +from homeassistant_api import AsyncClient +from homeassistant_api import AsyncWebsocketClient from homeassistant_api import Client -from homeassistant_api.errors import RequestError +from homeassistant_api import WebsocketClient +from homeassistant_api.errors import ResponseError from homeassistant_api.models import ConfigEntryDisabler +from homeassistant_api.models.events import AsyncEvent from homeassistant_api.models.events import Event from homeassistant_api.models.states import State -from homeassistant_api.websocket import WebsocketClient + +logger = logging.getLogger(__name__) def test_get_error_log(cached_client: Client) -> None: @@ -18,9 +24,9 @@ def test_get_error_log(cached_client: Client) -> None: assert cached_client.get_error_log() -async def test_async_get_error_log(async_cached_client: Client) -> None: +async def test_async_get_error_log(async_cached_client: AsyncClient) -> None: """Tests the `GET /api/error_log` endpoint.""" - assert await async_cached_client.async_get_error_log() + assert await async_cached_client.get_error_log() def test_get_config(cached_client: Client) -> None: @@ -28,9 +34,9 @@ def test_get_config(cached_client: Client) -> None: assert cached_client.get_config().get("state") in {"RUNNING", "NOT_RUNNING"} -async def test_async_get_config(async_cached_client: Client) -> None: +async def test_async_get_config(async_cached_client: AsyncClient) -> None: """Tests the `GET /api/config` endpoint.""" - assert (await async_cached_client.async_get_config()).get("state") in { + assert (await async_cached_client.get_config()).get("state") in { "RUNNING", "NOT_RUNNING", } @@ -40,18 +46,18 @@ def test_get_logbook_entries(cached_client: Client) -> None: """Tests the `GET /api/logbook/` endpoint.""" for entry in cached_client.get_logbook_entries( filter_entities="sun.red_sun", - start_timestamp=datetime(2020, 1, 1), - end_timestamp=datetime.now(), + start_timestamp=datetime(2020, 1, 1, tzinfo=UTC), + end_timestamp=datetime.now(UTC), ): assert entry -async def test_async_get_logbook_entries(async_cached_client: Client) -> None: +async def test_async_get_logbook_entries(async_cached_client: AsyncClient) -> None: """Tests the `GET /api/logbook/` endpoint.""" - async for entry in async_cached_client.async_get_logbook_entries( + async for entry in async_cached_client.get_logbook_entries( filter_entities="sun.red_sun", - start_timestamp=datetime(2020, 1, 1), - end_timestamp=datetime.now(), + start_timestamp=datetime(2020, 1, 1, tzinfo=UTC), + end_timestamp=datetime.now(UTC), ): assert entry @@ -61,9 +67,9 @@ def test_get_entity(cached_client: Client) -> None: assert cached_client.get_entity(entity_id="sun.sun") -async def test_async_get_entity(async_cached_client: Client) -> None: +async def test_async_get_entity(async_cached_client: AsyncClient) -> None: """Tests the `GET /api/states/` endpoint.""" - assert await async_cached_client.async_get_entity(entity_id="sun.sun") + assert await async_cached_client.get_entity(entity_id="sun.sun") def test_get_entity_histories(cached_client: Client) -> None: @@ -73,23 +79,22 @@ def test_get_entity_histories(cached_client: Client) -> None: histories = list( cached_client.get_entity_histories( (sun,), - end_timestamp=datetime.now(), - start_timestamp=datetime(2020, 1, 1), + end_timestamp=datetime.now(tz=UTC), + start_timestamp=datetime(2020, 1, 1, tzinfo=UTC), significant_changes_only=True, - ) + ), ) assert histories, "No history found." assert histories[0].states, "No states in entity history found." assert isinstance(histories[0].states[0], State) -async def test_async_get_entity_histories(async_cached_client: Client) -> None: +async def test_async_get_entity_histories(async_cached_client: AsyncClient) -> None: """Tests the `GET /api/history/period/` endpoint.""" - sun = await async_cached_client.async_get_entity(entity_id="sun.sun") + sun = await async_cached_client.get_entity(entity_id="sun.sun") assert sun is not None histories = [ - history - async for history in async_cached_client.async_get_entity_histories((sun,)) + history async for history in async_cached_client.get_entity_histories((sun,)) ] assert histories, "No history found." assert histories[0].states, "No states in entity history found." @@ -99,7 +104,7 @@ async def test_async_get_entity_histories(async_cached_client: Client) -> None: def test_get_rendered_template(cached_client: Client) -> None: """Tests the `POST /api/template` endpoint.""" rendered_template = cached_client.get_rendered_template( - 'The sun is {{ states("sun.sun").replace("_", " the ") }}.' + 'The sun is {{ states("sun.sun").replace("_", " the ") }}.', ) assert rendered_template in { "The sun is above the horizon.", @@ -107,10 +112,10 @@ def test_get_rendered_template(cached_client: Client) -> None: } -async def test_async_get_rendered_template(async_cached_client: Client) -> None: +async def test_async_get_rendered_template(async_cached_client: AsyncClient) -> None: """Tests the `POST /api/template` endpoint.""" - rendered_template = await async_cached_client.async_get_rendered_template( - 'The sun is {{ states("sun.sun").replace("_", " the ") }}.' + rendered_template = await async_cached_client.get_rendered_template( + 'The sun is {{ states("sun.sun").replace("_", " the ") }}.', ) assert rendered_template in { "The sun is above the horizon.", @@ -121,7 +126,7 @@ async def test_async_get_rendered_template(async_cached_client: Client) -> None: def test_websocket_get_rendered_template(websocket_client: WebsocketClient) -> None: """Tests the `"type": "render_template"` websocket command.""" rendered_template = websocket_client.get_rendered_template( - 'The sun is {{ states("sun.sun").replace("_", " the ") }}.' + 'The sun is {{ states("sun.sun").replace("_", " the ") }}.', ) assert rendered_template in { "The sun is above the horizon.", @@ -130,11 +135,11 @@ def test_websocket_get_rendered_template(websocket_client: WebsocketClient) -> N async def test_async_websocket_get_rendered_template( - async_websocket_client: WebsocketClient, + async_websocket_client: AsyncWebsocketClient, ) -> None: """Tests the `"type": "render_template"` websocket command.""" - rendered_template = await async_websocket_client.async_get_rendered_template( - 'The sun is {{ states("sun.sun").replace("_", " the ") }}.' + rendered_template = await async_websocket_client.get_rendered_template( + 'The sun is {{ states("sun.sun").replace("_", " the ") }}.', ) assert rendered_template in { "The sun is above the horizon.", @@ -147,9 +152,9 @@ def test_check_api_config(cached_client: Client) -> None: assert cached_client.check_api_config() -async def test_async_check_api_config(async_cached_client: Client) -> None: +async def test_async_check_api_config(async_cached_client: AsyncClient) -> None: """Tests the `POST /api/config/core/check_config` endpoint.""" - assert await async_cached_client.async_check_api_config() + assert await async_cached_client.check_api_config() def test_get_entities(cached_client: Client) -> None: @@ -158,9 +163,9 @@ def test_get_entities(cached_client: Client) -> None: assert "sun" in entities -async def test_async_get_entities(async_cached_client: Client) -> None: +async def test_async_get_entities(async_cached_client: AsyncClient) -> None: """Tests the `GET /api/states` endpoint.""" - entities = await async_cached_client.async_get_entities() + entities = await async_cached_client.get_entities() assert "sun" in entities @@ -172,10 +177,10 @@ def test_websocket_get_config(websocket_client: WebsocketClient) -> None: async def test_async_websocket_get_config( - async_websocket_client: WebsocketClient, + async_websocket_client: AsyncWebsocketClient, ) -> None: """Tests the `"type": "get_config"` websocket command.""" - config = await async_websocket_client.async_get_config() + config = await async_websocket_client.get_config() assert isinstance(config, dict) assert config.get("state") in {"RUNNING", "NOT_RUNNING"} @@ -204,55 +209,57 @@ def test_websocket_get_entity_by_entity_id(websocket_client: WebsocketClient) -> def test_websocket_get_entity_no_args(websocket_client: WebsocketClient) -> None: """Tests WebsocketClient.get_entity raises ValueError with no arguments.""" with pytest.raises( - ValueError, match="Neither group_id and slug or entity_id provided" + ValueError, + match="Neither group_id and slug or entity_id provided", ): websocket_client.get_entity() async def test_async_websocket_get_state( - async_websocket_client: WebsocketClient, + async_websocket_client: AsyncWebsocketClient, ) -> None: - """Tests async WebsocketClient.async_get_state with entity_id.""" - state = await async_websocket_client.async_get_state(entity_id="sun.sun") + """Tests AsyncWebsocketClient.get_state with entity_id.""" + state = await async_websocket_client.get_state(entity_id="sun.sun") assert state.entity_id == "sun.sun" assert state.state in {"above_horizon", "below_horizon"} async def test_async_websocket_get_entity_by_group_slug( - async_websocket_client: WebsocketClient, + async_websocket_client: AsyncWebsocketClient, ) -> None: - """Tests async WebsocketClient.async_get_entity with group_id and slug.""" - entity = await async_websocket_client.async_get_entity(group_id="sun", slug="sun") + """Tests AsyncWebsocketClient.get_entity with group_id and slug.""" + entity = await async_websocket_client.get_entity(group_id="sun", slug="sun") assert entity is not None assert entity.entity_id == "sun.sun" async def test_async_websocket_get_entity_by_entity_id( - async_websocket_client: WebsocketClient, + async_websocket_client: AsyncWebsocketClient, ) -> None: - """Tests async WebsocketClient.async_get_entity with entity_id.""" - entity = await async_websocket_client.async_get_entity(entity_id="sun.sun") + """Tests AsyncWebsocketClient.get_entity with entity_id.""" + entity = await async_websocket_client.get_entity(entity_id="sun.sun") assert entity is not None assert entity.entity_id == "sun.sun" async def test_async_websocket_get_entity_no_args( - async_websocket_client: WebsocketClient, + async_websocket_client: AsyncWebsocketClient, ) -> None: - """Tests async WebsocketClient.async_get_entity raises ValueError with no arguments.""" + """Tests AsyncWebsocketClient.get_entity raises ValueError with no arguments.""" with pytest.raises( - ValueError, match="Neither group_id and slug or entity_id provided" + ValueError, + match="Neither group_id and slug or entity_id provided", ): - await async_websocket_client.async_get_entity() + await async_websocket_client.get_entity() async def test_async_websocket_get_state_not_found( - async_websocket_client: WebsocketClient, + async_websocket_client: AsyncWebsocketClient, ) -> None: - """Tests async WebsocketClient.async_get_state raises ValueError for nonexistent entity.""" + """Tests AsyncWebsocketClient.get_state raises ValueError for nonexistent entity.""" with pytest.raises(ValueError, match="not found"): - await async_websocket_client.async_get_state( - entity_id="fake.nonexistent_entity_12345" + await async_websocket_client.get_state( + entity_id="fake.nonexistent_entity_12345", ) @@ -269,10 +276,10 @@ def test_websocket_get_entities(websocket_client: WebsocketClient) -> None: async def test_async_websocket_get_entities( - async_websocket_client: WebsocketClient, + async_websocket_client: AsyncWebsocketClient, ) -> None: """Tests the `"type": "get_entities"` websocket command.""" - entities = await async_websocket_client.async_get_entities() + entities = await async_websocket_client.get_entities() assert "sun" in entities @@ -282,9 +289,9 @@ def test_get_domains(cached_client: Client) -> None: assert "homeassistant" in domains -async def test_async_get_domains(async_cached_client: Client) -> None: +async def test_async_get_domains(async_cached_client: AsyncClient) -> None: """Tests the `GET /api/services` endpoint.""" - domains = await async_cached_client.async_get_domains() + domains = await async_cached_client.get_domains() assert "homeassistant" in domains @@ -295,10 +302,10 @@ def test_websocket_get_domains(websocket_client: WebsocketClient) -> None: async def test_async_websocket_get_domains( - async_websocket_client: WebsocketClient, + async_websocket_client: AsyncWebsocketClient, ) -> None: """Tests the `"type": "get_domains"` websocket command.""" - domains = await async_websocket_client.async_get_domains() + domains = await async_websocket_client.get_domains() assert "homeassistant" in domains @@ -309,9 +316,9 @@ def test_get_domain(cached_client: Client) -> None: assert domain.services -async def test_async_get_domain(async_cached_client: Client) -> None: +async def test_async_get_domain(async_cached_client: AsyncClient) -> None: """Tests the `GET /api/services` endpoint.""" - domain = await async_cached_client.async_get_domain("homeassistant") + domain = await async_cached_client.get_domain("homeassistant") assert domain is not None assert domain.services @@ -324,10 +331,10 @@ def test_websocket_get_domain(websocket_client: WebsocketClient) -> None: async def test_async_websocket_get_domain( - async_websocket_client: WebsocketClient, + async_websocket_client: AsyncWebsocketClient, ) -> None: """Tests the `"type": "get_domain"` websocket command.""" - domain = await async_websocket_client.async_get_domain("homeassistant") + domain = await async_websocket_client.get_domain("homeassistant") assert domain is not None assert domain.services @@ -340,10 +347,10 @@ def test_get_nonuser_flows_in_progress(websocket_client: WebsocketClient) -> Non async def test_async_get_nonuser_flows_in_progress( - async_websocket_client: WebsocketClient, + async_websocket_client: AsyncWebsocketClient, ) -> None: """Tests the `"type": "config_entries/flow/progress"` websocket command.""" - flows = await async_websocket_client.async_get_nonuser_flows_in_progress() + flows = await async_websocket_client.get_nonuser_flows_in_progress() assert not flows @@ -353,15 +360,16 @@ def test_disable_enable_config_entry(websocket_client: WebsocketClient) -> None: entry = websocket_client.get_config_entries()[0] assert entry.disabled_by is None - # Disable entry - websocket_client.disable_config_entry(entry.entry_id) - - # Check that it was disabled - disabled_entry = websocket_client.get_config_entries()[0] - assert disabled_entry.disabled_by is ConfigEntryDisabler.USER + try: + # Disable entry + websocket_client.disable_config_entry(entry.entry_id) - # Re-enable - websocket_client.enable_config_entry(entry.entry_id) + # Check that it was disabled + disabled_entry = websocket_client.get_config_entries()[0] + assert disabled_entry.disabled_by is ConfigEntryDisabler.USER + finally: + # Always re-enable to avoid breaking other tests + websocket_client.enable_config_entry(entry.entry_id) # Check that it was enabled enabled_entry = websocket_client.get_config_entries()[0] @@ -369,36 +377,37 @@ def test_disable_enable_config_entry(websocket_client: WebsocketClient) -> None: async def test_async_disable_enable_config_entry( - async_websocket_client: WebsocketClient, + async_websocket_client: AsyncWebsocketClient, ) -> None: """Tests the `"type": "config_entries/disable"` websocket command.""" - entry = (await async_websocket_client.async_get_config_entries())[0] + entry = (await async_websocket_client.get_config_entries())[0] assert entry.disabled_by is None - await async_websocket_client.async_disable_config_entry(entry.entry_id) + try: + await async_websocket_client.disable_config_entry(entry.entry_id) - disabled_entry = (await async_websocket_client.async_get_config_entries())[0] - assert disabled_entry.disabled_by is ConfigEntryDisabler.USER + disabled_entry = (await async_websocket_client.get_config_entries())[0] + assert disabled_entry.disabled_by is ConfigEntryDisabler.USER + finally: + await async_websocket_client.enable_config_entry(entry.entry_id) - await async_websocket_client.async_enable_config_entry(entry.entry_id) - - enabled_entry = (await async_websocket_client.async_get_config_entries())[0] + enabled_entry = (await async_websocket_client.get_config_entries())[0] assert enabled_entry.disabled_by is None def test_ignore_config_flow(websocket_client: WebsocketClient) -> None: """Tests the `"type": "config_entries/ignore_flow"` websocket command.""" # Currently not able to test as no flows are in progress. Send invalid parameters and handle that error - with pytest.raises(RequestError, match="Config entry not found"): + with pytest.raises(ResponseError, match="Config entry not found"): websocket_client.ignore_config_flow("", "") async def test_async_ignore_config_flow( - async_websocket_client: WebsocketClient, + async_websocket_client: AsyncWebsocketClient, ) -> None: """Tests the `"type": "config_entries/ignore_flow"` websocket command.""" - with pytest.raises(RequestError, match="Config entry not found"): - await async_websocket_client.async_ignore_config_flow("", "") + with pytest.raises(ResponseError, match="Config entry not found"): + await async_websocket_client.ignore_config_flow("", "") def test_get_config_entries(websocket_client: WebsocketClient) -> None: @@ -431,10 +440,10 @@ def test_get_config_entries(websocket_client: WebsocketClient) -> None: async def test_async_get_config_entries( - async_websocket_client: WebsocketClient, + async_websocket_client: AsyncWebsocketClient, ) -> None: """Tests the `"type": "config_entries/get"` websocket command.""" - entries = await async_websocket_client.async_get_config_entries() + entries = await async_websocket_client.get_config_entries() assert len(entries) == 4 sun = entries[0] @@ -456,46 +465,46 @@ def test_get_entry_subentries(websocket_client: WebsocketClient) -> None: async def test_async_get_entry_subentries( - async_websocket_client: WebsocketClient, + async_websocket_client: AsyncWebsocketClient, ) -> None: """Tests the `"type": "config_entries/subentries/list"` websocket command.""" - sun = (await async_websocket_client.async_get_config_entries())[0] + sun = (await async_websocket_client.get_config_entries())[0] assert sun - assert not await async_websocket_client.async_get_entry_subentries(sun.entry_id) + assert not await async_websocket_client.get_entry_subentries(sun.entry_id) def test_delete_entry_subentry(websocket_client: WebsocketClient) -> None: """Tests the `"type": "config_entries/subentries/delete"` websocket command.""" # Currently not able to test as no entries with subentries available. Send invalid parameters and handle that error - with pytest.raises(RequestError, match="Config entry not found"): + with pytest.raises(ResponseError, match="Config entry not found"): websocket_client.delete_entry_subentry("", "") async def test_async_delete_entry_subentry( - async_websocket_client: WebsocketClient, + async_websocket_client: AsyncWebsocketClient, ) -> None: """Tests the `"type": "config_entries/subentries/delete"` websocket command.""" - with pytest.raises(RequestError, match="Config entry not found"): - await async_websocket_client.async_delete_entry_subentry("", "") + with pytest.raises(ResponseError, match="Config entry not found"): + await async_websocket_client.delete_entry_subentry("", "") def test_trigger_service(cached_client: Client) -> None: """Tests the `POST /api/services//` endpoint.""" notify = cached_client.get_domain("notify") assert notify is not None - resp = notify.persistent_notification( + resp = notify.persistent_notification.trigger( message="Your API Test Suite just said hello!", title="Test Suite Notifcation", ) - logging.info(resp) + logger.info(resp) assert isinstance(resp, tuple) -async def test_async_trigger_service(async_cached_client: Client) -> None: +async def test_async_trigger_service(async_cached_client: AsyncClient) -> None: """Tests the `POST /api/services//` endpoint.""" - notify = await async_cached_client.async_get_domain("notify") + notify = await async_cached_client.get_domain("notify") assert notify is not None - resp = await notify.persistent_notification( + resp = await notify.persistent_notification.trigger( message="Your API Test Suite just said hello!", title="Test Suite Notifcation (Async)", ) @@ -506,21 +515,23 @@ def test_websocket_trigger_service(websocket_client: WebsocketClient) -> None: """Tests the `"type": "trigger_service"` websocket command.""" notify = websocket_client.get_domain("notify") assert notify is not None - resp = notify.persistent_notification( - message="Your API Test Suite just said hello!", title="Test Suite Notifcation" + resp = notify.persistent_notification.trigger( + message="Your API Test Suite just said hello!", + title="Test Suite Notifcation", ) # Websocket API doesnt return changed states so we check for None assert resp is None async def test_async_websocket_trigger_service( - async_websocket_client: WebsocketClient, + async_websocket_client: AsyncWebsocketClient, ) -> None: """Tests the `"type": "trigger_service"` websocket command.""" - notify = await async_websocket_client.async_get_domain("notify") + notify = await async_websocket_client.get_domain("notify") assert notify is not None - resp = await notify.persistent_notification( - message="Your API Test Suite just said hello!", title="Test Suite Notifcation" + resp = await notify.persistent_notification.trigger( + message="Your API Test Suite just said hello!", + title="Test Suite Notifcation", ) # Websocket API doesnt return changed states so we check for None assert resp is None @@ -546,18 +557,20 @@ def test_trigger_service_with_response(cached_client: Client) -> None: """Tests the `POST /api/services//?return_response` endpoint.""" weather = cached_client.get_domain("weather") assert weather is not None - changed_states, data = weather.get_forecasts( + _changed_states, data = weather.get_forecasts.trigger( entity_id="weather.forecast_home", type="hourly", ) assert data is not None -async def test_async_trigger_service_with_response(async_cached_client: Client) -> None: +async def test_async_trigger_service_with_response( + async_cached_client: AsyncClient, +) -> None: """Tests the `POST /api/services//?return_response` endpoint.""" - weather = await async_cached_client.async_get_domain("weather") + weather = await async_cached_client.get_domain("weather") assert weather is not None - changed_states, data = await weather.get_forecasts( + _changed_states, data = await weather.get_forecasts.trigger( entity_id="weather.forecast_home", type="hourly", ) @@ -570,7 +583,7 @@ def test_websocket_trigger_service_with_response( """Tests the `"type": "trigger_service_with_response"` websocket command.""" weather = websocket_client.get_domain("weather") assert weather is not None - data = weather.get_forecasts( + data = weather.get_forecasts.trigger( entity_id="weather.forecast_home", type="hourly", ) @@ -579,12 +592,12 @@ def test_websocket_trigger_service_with_response( async def test_async_websocket_trigger_service_with_response( - async_websocket_client: WebsocketClient, + async_websocket_client: AsyncWebsocketClient, ) -> None: """Tests the `"type": "trigger_service_with_response"` websocket command.""" - weather = await async_websocket_client.async_get_domain("weather") + weather = await async_websocket_client.get_domain("weather") assert weather is not None - data = await weather.get_forecasts( + data = await weather.get_forecasts.trigger( entity_id="weather.forecast_home", type="hourly", ) @@ -599,9 +612,9 @@ def test_get_states(cached_client: Client) -> None: assert isinstance(state, State) -async def test_async_get_states(async_cached_client: Client) -> None: +async def test_async_get_states(async_cached_client: AsyncClient) -> None: """Tests the `GET /api/states` endpoint.""" - states = await async_cached_client.async_get_states() + states = await async_cached_client.get_states() for state in states: assert isinstance(state, State) @@ -614,10 +627,10 @@ def test_websocket_get_states(websocket_client: WebsocketClient) -> None: async def test_async_websocket_get_states( - async_websocket_client: WebsocketClient, + async_websocket_client: AsyncWebsocketClient, ) -> None: """Tests the `"type": "get_states"` websocket command.""" - states = await async_websocket_client.async_get_states() + states = await async_websocket_client.get_states() for state in states: assert isinstance(state, State) @@ -628,24 +641,24 @@ def test_get_state(cached_client: Client) -> None: assert state.state in {"above_horizon", "below_horizon"} -async def test_async_get_state(async_cached_client: Client) -> None: +async def test_async_get_state(async_cached_client: AsyncClient) -> None: """Tests the `GET /api/states/` endpoint.""" - state = await async_cached_client.async_get_state(entity_id="sun.sun") + state = await async_cached_client.get_state(entity_id="sun.sun") assert state.state in {"above_horizon", "below_horizon"} def test_set_state(cached_client: Client) -> None: """Tests the `POST /api/states/` endpoint.""" state = cached_client.set_state( - State(state="beyond_our_solar_system", entity_id="sun.red_sun") + State(state="beyond_our_solar_system", entity_id="sun.red_sun"), ) assert state.state == "beyond_our_solar_system" -async def test_async_set_state(async_cached_client: Client) -> None: +async def test_async_set_state(async_cached_client: AsyncClient) -> None: """Tests the `POST /api/states/` endpoint.""" - state = await async_cached_client.async_set_state( - State(state="beyond_our_solar_system", entity_id="sun.red_sun") + state = await async_cached_client.set_state( + State(state="beyond_our_solar_system", entity_id="sun.red_sun"), ) assert state.state == "beyond_our_solar_system" @@ -657,11 +670,11 @@ def test_get_events(cached_client: Client) -> None: assert isinstance(event, Event) -async def test_async_get_events(async_cached_client: Client) -> None: +async def test_async_get_events(async_cached_client: AsyncClient) -> None: """Tests the `GET /api/events` endpoint.""" - events = await async_cached_client.async_get_events() + events = await async_cached_client.get_events() for event in events: - assert isinstance(event, Event) + assert isinstance(event, AsyncEvent) def test_fire_event(cached_client: Client) -> None: @@ -670,9 +683,9 @@ def test_fire_event(cached_client: Client) -> None: assert data == "Event my_new_event fired." -async def test_async_fire_event(async_cached_client: Client) -> None: +async def test_async_fire_event(async_cached_client: AsyncClient) -> None: """Tests the `POST /api/events/` endpoint.""" - data = await async_cached_client.async_fire_event("my_new_event", parameter="123") + data = await async_cached_client.fire_event("my_new_event", parameter="123") assert data == "Event my_new_event fired." @@ -682,7 +695,7 @@ def test_get_components(cached_client: Client) -> None: assert "person" in components -async def test_async_get_components(async_cached_client: Client) -> None: +async def test_async_get_components(async_cached_client: AsyncClient) -> None: """Tests the `GET /api/components` endpoint.""" - components = await async_cached_client.async_get_components() + components = await async_cached_client.get_components() assert "person" in components diff --git a/tests/test_entity_registry.py b/tests/test_entity_registry.py new file mode 100644 index 00000000..116ab182 --- /dev/null +++ b/tests/test_entity_registry.py @@ -0,0 +1,113 @@ +"""Integration tests for entity registry WebSocket methods.""" + +from homeassistant_api import AsyncWebsocketClient +from homeassistant_api import WebsocketClient +from homeassistant_api.models.entity_registry import EntityRegistryEntry +from homeassistant_api.models.entity_registry import EntityRegistryEntryExtended +from homeassistant_api.models.entity_registry import EntityRegistryUpdateParams +from homeassistant_api.models.entity_registry import EntityRegistryUpdateResult + +# A stable, always-present entity for read/update tests +_TEST_ENTITY_ID = "sensor.sun_next_dawn" + +# Dedicated input_booleans for remove tests — HA re-adds them on restart +_REMOVE_ENTITY_ID = "input_boolean.smoke_registry_remove_test" +_ASYNC_REMOVE_ENTITY_ID = "input_boolean.smoke_registry_remove_test_async" + + +def test_list_entity_registry(websocket_client: WebsocketClient) -> None: + entries = websocket_client.list_entity_registry() + assert isinstance(entries, tuple) + assert len(entries) > 0 + assert all(isinstance(e, EntityRegistryEntry) for e in entries) + + +def test_get_entity_registry_entry(websocket_client: WebsocketClient) -> None: + entry = websocket_client.get_entity_registry_entry(_TEST_ENTITY_ID) + assert isinstance(entry, EntityRegistryEntryExtended) + assert entry.entity_id == _TEST_ENTITY_ID + + +def test_update_entity_registry_entry(websocket_client: WebsocketClient) -> None: + result = websocket_client.update_entity_registry_entry( + EntityRegistryUpdateParams( + entity_id=_TEST_ENTITY_ID, + name="Test Name", + ), + ) + assert isinstance(result, EntityRegistryUpdateResult) + assert result.entity_entry.entity_id == _TEST_ENTITY_ID + assert result.entity_entry.name == "Test Name" + # Restore original state + websocket_client.update_entity_registry_entry( + EntityRegistryUpdateParams( + entity_id=_TEST_ENTITY_ID, + name=None, + ), + ) + + +def test_remove_entity_registry_entry(websocket_client: WebsocketClient) -> None: + websocket_client.recv( + websocket_client.send( + "input_boolean/create", + name="smoke_registry_remove_test", + ), + ) + websocket_client.remove_entity_registry_entry(_REMOVE_ENTITY_ID) + entity_ids = {e.entity_id for e in websocket_client.list_entity_registry()} + assert _REMOVE_ENTITY_ID not in entity_ids + + +async def test_async_list_entity_registry( + async_websocket_client: AsyncWebsocketClient, +) -> None: + entries = await async_websocket_client.list_entity_registry() + assert isinstance(entries, tuple) + assert len(entries) > 0 + assert all(isinstance(e, EntityRegistryEntry) for e in entries) + + +async def test_async_get_entity_registry_entry( + async_websocket_client: AsyncWebsocketClient, +) -> None: + entry = await async_websocket_client.get_entity_registry_entry(_TEST_ENTITY_ID) + assert isinstance(entry, EntityRegistryEntryExtended) + assert entry.entity_id == _TEST_ENTITY_ID + + +async def test_async_update_entity_registry_entry( + async_websocket_client: AsyncWebsocketClient, +) -> None: + result = await async_websocket_client.update_entity_registry_entry( + EntityRegistryUpdateParams( + entity_id=_TEST_ENTITY_ID, + name="Async Test Name", + ), + ) + assert isinstance(result, EntityRegistryUpdateResult) + assert result.entity_entry.entity_id == _TEST_ENTITY_ID + assert result.entity_entry.name == "Async Test Name" + # Restore original state + await async_websocket_client.update_entity_registry_entry( + EntityRegistryUpdateParams( + entity_id=_TEST_ENTITY_ID, + name=None, + ), + ) + + +async def test_async_remove_entity_registry_entry( + async_websocket_client: AsyncWebsocketClient, +) -> None: + await async_websocket_client.recv( + await async_websocket_client.send( + "input_boolean/create", + name="smoke_registry_remove_test_async", + ), + ) + await async_websocket_client.remove_entity_registry_entry(_ASYNC_REMOVE_ENTITY_ID) + entity_ids = { + e.entity_id for e in await async_websocket_client.list_entity_registry() + } + assert _ASYNC_REMOVE_ENTITY_ID not in entity_ids diff --git a/tests/test_errors.py b/tests/test_errors.py index 471716d4..34475195 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -3,72 +3,66 @@ import json import os import unittest.mock -from typing import Dict +from http import HTTPMethod -import aiohttp +import niquests import pytest -import requests -from multidict import CIMultiDict, CIMultiDictProxy - -from homeassistant_api import Client, Domain -from homeassistant_api.errors import ( - APIConfigurationError, - BadTemplateError, - EndpointNotFoundError, - InternalServerError, - MalformedDataError, - MethodNotAllowedError, - ProcessorNotFoundError, - RequestError, - RequestTimeoutError, - ResponseError, - UnauthorizedError, - UnexpectedStatusCodeError, -) +from niquests.structures import CaseInsensitiveDict + +from homeassistant_api import AsyncClient +from homeassistant_api import AsyncWebsocketClient +from homeassistant_api import Client +from homeassistant_api import Domain +from homeassistant_api import WebsocketClient +from homeassistant_api.errors import APIConfigurationError +from homeassistant_api.errors import BadTemplateError +from homeassistant_api.errors import EndpointNotFoundError +from homeassistant_api.errors import InternalServerError +from homeassistant_api.errors import MalformedDataError +from homeassistant_api.errors import MethodNotAllowedError +from homeassistant_api.errors import ProcessorNotFoundError +from homeassistant_api.errors import RequestError +from homeassistant_api.errors import RequestTimeoutError +from homeassistant_api.errors import ResponseError +from homeassistant_api.errors import UnauthorizedError +from homeassistant_api.errors import UnexpectedStatusCodeError +from homeassistant_api.models.states import State from homeassistant_api.models.websocket import Error -from homeassistant_api.processing import Processing +from homeassistant_api.processing import async_process_response +from homeassistant_api.processing import process_response from homeassistant_api.utils import prepare_entity_id -from homeassistant_api.websocket import WebsocketClient + +HA_URL = os.environ["HOMEASSISTANTAPI_URL"] +HA_WS_URL = os.environ["HOMEASSISTANTAPI_WS_URL"] +WRONG_TOKEN = "lolthisisawrongtokenforsure" # noqa: S105 def test_unauthorized() -> None: - with pytest.raises(UnauthorizedError): - with Client(os.environ["HOMEASSISTANTAPI_URL"], "lolthisisawrongtokenforsure"): - pass + with pytest.raises(UnauthorizedError), Client(HA_URL, WRONG_TOKEN): + pass def test_websocket_unauthorized() -> None: - with pytest.raises(UnauthorizedError): - with WebsocketClient( - os.environ["HOMEASSISTANTAPI_WS_URL"], "lolthisisawrongtokenforsure" - ): - pass + with pytest.raises(UnauthorizedError), WebsocketClient(HA_WS_URL, WRONG_TOKEN): + pass async def test_async_websocket_unauthorized() -> None: with pytest.raises(UnauthorizedError): - async with WebsocketClient( - os.environ["HOMEASSISTANTAPI_WS_URL"], - "lolthisisawrongtokenforsure", - use_async=True, - ): + async with AsyncWebsocketClient(HA_WS_URL, WRONG_TOKEN): pass async def test_async_unauthorized() -> None: with pytest.raises(UnauthorizedError): - async with Client( - os.environ["HOMEASSISTANTAPI_URL"], - "lolthisisawrongtokenforsure", - use_async=True, - ): + async with AsyncClient(HA_URL, WRONG_TOKEN): pass async def test_domain_missing_services_attribute(cached_client: Client) -> None: - with pytest.raises(ValueError): + with pytest.raises(NotImplementedError, match="does not support `from_json\\(\\)`"): Domain.from_json({"services": None}, client=cached_client) # Missing domain - with pytest.raises(ValueError): + with pytest.raises(NotImplementedError, match="does not support `from_json\\(\\)`"): Domain.from_json({"domain": None}, client=cached_client) # Missing services @@ -77,43 +71,49 @@ def test_endpoint_not_found_error(cached_client: Client) -> None: cached_client.request("qwertyuioasdfghjkzxcvbnm") -async def test_async_endpoint_not_found_error(async_cached_client: Client) -> None: +async def test_async_endpoint_not_found_error(async_cached_client: AsyncClient) -> None: with pytest.raises(EndpointNotFoundError): - await async_cached_client.async_request("qwertyuioasdfghjkzxcvbnm") + await async_cached_client.request("qwertyuioasdfghjkzxcvbnm") def test_method_not_allowed_error(cached_client: Client) -> None: with pytest.raises(MethodNotAllowedError): - cached_client.request("", method="DELETE") + cached_client.request("", method=HTTPMethod.DELETE) -async def test_async_method_not_allowed_error(async_cached_client: Client) -> None: +async def test_async_method_not_allowed_error(async_cached_client: AsyncClient) -> None: with pytest.raises(MethodNotAllowedError): - await async_cached_client.async_request("", method="DELETE") + await async_cached_client.request("", method=HTTPMethod.DELETE) def test_wrong_headers(cached_client: Client) -> None: - with pytest.raises(ValueError): + with pytest.raises(TypeError): cached_client.request("", headers=1234567890) # type: ignore[arg-type] -async def test_async_wrong_headers(async_cached_client: Client) -> None: - with pytest.raises(ValueError): - await async_cached_client.async_request("", headers=1234567890) # type: ignore[arg-type] +async def test_async_wrong_headers(async_cached_client: AsyncClient) -> None: + with pytest.raises(TypeError): + await async_cached_client.request("", headers=1234567890) # type: ignore[arg-type] def test_no_entity_information_provided(cached_client: Client) -> None: """Tests that the client raises an error if no entity information is provided.""" - with pytest.raises(ValueError): + with pytest.raises( + ValueError, + match="Neither group_id and slug or entity_id provided", + ): cached_client.get_entity() async def test_async_no_entity_information_provided( - async_cached_client: Client, + async_cached_client: AsyncClient, ) -> None: """Tests that the client raises an error if no entity information is provided.""" - with pytest.raises(ValueError): - await async_cached_client.async_get_entity() + with pytest.raises( + ValueError, + match=r"Neither group_id and slug or entity_id provided", + ): + await async_cached_client.get_entity() def test_invalid_template(cached_client: Client) -> None: @@ -121,44 +121,52 @@ def test_invalid_template(cached_client: Client) -> None: cached_client.get_rendered_template("{{ invalid_template lol") -async def test_async_invalid_template(async_cached_client: Client) -> None: +async def test_async_invalid_template(async_cached_client: AsyncClient) -> None: with pytest.raises(BadTemplateError): - await async_cached_client.async_get_rendered_template("{{ invalid_template lol") + await async_cached_client.get_rendered_template("{{ invalid_template lol") -def test_prepare_entity_id(cached_client: Client) -> None: +def test_prepare_entity_id() -> None: """Tests all cases for :py:meth:`Client.prepare_entity_id`.""" assert prepare_entity_id(group_id="person", slug="me") == "person.me" assert prepare_entity_id(entity_id="person.me") == "person.me" - assert "person.you" == prepare_entity_id( - group_id="person", - entity_id="person.you", + assert ( + prepare_entity_id( + group_id="person", + entity_id="person.you", + ) + == "person.you" ) - assert "person.you" == prepare_entity_id( - slug="me", - entity_id="person.you", + assert ( + prepare_entity_id( + slug="me", + entity_id="person.you", + ) + == "person.you" ) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="pass both, not just one"): prepare_entity_id(group_id="person") # No slug - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="pass both, not just one"): prepare_entity_id(slug="me") # No group - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="pass both, not just one"): prepare_entity_id() # No entity_id def make_response( status_code: int, content: str, - headers: Dict[str, str], -) -> requests.Response: - """Make a :py:class:`requests.Response` object from a status_code, headers, content.""" + headers: dict[str, str], +) -> niquests.Response: + """Make a :py:class:`niquests.Response` object from a status_code, headers, content.""" return unittest.mock.Mock( - spec=requests.Response, + spec=niquests.Response, status_code=status_code, text=content, - headers=CIMultiDictProxy(CIMultiDict(headers)), + url="http://localhost/api/test", + request=unittest.mock.Mock(method="GET"), + headers=CaseInsensitiveDict(headers), json=unittest.mock.Mock( - side_effect=json.JSONDecodeError("This is a fake message", "", 1) + side_effect=json.JSONDecodeError("This is a fake message", "", 1), ), ) @@ -166,79 +174,84 @@ def make_response( def make_async_response( status_code: int, content: str, - headers: Dict[str, str], -) -> aiohttp.ClientResponse: - """Make an :py:class:`aiohttp.ClientResponse` object from a status_code, headers, content.""" + headers: dict[str, str], +) -> niquests.Response: + """Make a :py:class:`niquests.Response` mock for async processing tests.""" return unittest.mock.Mock( - spec=aiohttp.ClientResponse, - status=status_code, - text=unittest.mock.AsyncMock(return_value=content), - content=unittest.mock.Mock(_buffer=[content.encode()]), - headers=CIMultiDictProxy(CIMultiDict(headers)), - json=unittest.mock.AsyncMock( - side_effect=json.JSONDecodeError("This is a fake message", "", 1) + spec=niquests.Response, + status_code=status_code, + text=content, + url="http://localhost/api/test", + request=unittest.mock.Mock(method="GET"), + headers=CaseInsensitiveDict(headers), + json=unittest.mock.Mock( + side_effect=json.JSONDecodeError("This is a fake message", "", 1), ), ) def test_exception_malformed_data_error() -> None: with pytest.raises(MalformedDataError): - Processing( + process_response( make_response( 200, "{this is not valid json}", {"Content-Type": "application/json"}, - ) - ).process() + ), + ) async def test_async_exception_malformed_data_error() -> None: with pytest.raises(MalformedDataError): - await Processing( + await async_process_response( make_async_response( 200, "{this is not valid json}", {"Content-Type": "application/json"}, - ) - ).process() + ), + ) def test_exception_internal_server_error() -> None: with pytest.raises(InternalServerError): - Processing(make_response(500, "", {})).process() + process_response(make_response(500, "", {})) def test_exception_processor_not_found_error() -> None: with pytest.raises(ProcessorNotFoundError): - Processing( - make_response(200, "", {"Content-Type": "this_type/does-not-exist"}) - ).process() + process_response( + make_response(200, "", {"Content-Type": "this_type/does-not-exist"}), + ) def test_exception_api_config_error() -> None: + msg = "(Fake) Server has invalid configuration.yaml" with pytest.raises(APIConfigurationError): - raise APIConfigurationError("(Fake) Server has invalid configuration.yaml") + raise APIConfigurationError(msg) def test_exception_response_error() -> None: + msg = "(Fake) Server returned a problematic response." with pytest.raises(ResponseError): - raise ResponseError("(Fake) Server returned a problematic response.") + raise ResponseError(msg) def test_exception_unexpected_status_code() -> None: with pytest.raises(UnexpectedStatusCodeError): - Processing(make_response(0, "", {})).process() + process_response(make_response(0, "", {})) def test_unkown_scheme() -> None: - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Unknown scheme"): Client("ftp://example.com", "token") def test_request_error_with_message_and_data() -> None: """Tests RequestError when both message and data are provided.""" err = RequestError( - "some_data", url="http://localhost/api", message="Custom message" + "some_data", + url="http://localhost/api", + message="Custom message", ) assert "Custom message" in str(err) assert "'http://localhost/api'" in str(err) @@ -248,8 +261,10 @@ def test_request_error_with_message_and_data() -> None: def test_request_error_no_data() -> None: """Tests RequestError when data is None and no message.""" err = RequestError(None, url="http://localhost/api") - assert "'http://localhost/api'" in str(err) - assert "data" not in str(err) + assert ( + str(err) + == "An error occurred while making the request to 'http://localhost/api'" + ) def test_request_timeout_error() -> None: @@ -261,9 +276,9 @@ def test_request_timeout_error() -> None: def test_websocket_invalid_scheme() -> None: - """Tests that WebsocketClient raises ValueError for non-ws schemes.""" + """Tests that WebsocketClient raises ValueError for unsupported schemes.""" with pytest.raises(ValueError, match="Unknown scheme"): - WebsocketClient("http://localhost", "token") + WebsocketClient("ftp://localhost", "token") def test_error_model_without_optional_fields() -> None: @@ -273,3 +288,96 @@ def test_error_model_without_optional_fields() -> None: assert error.translation_key is None assert error.translation_placeholders is None assert error.translation_domain is None + + +# --- Processing: async processor not found --- + + +async def test_async_exception_processor_not_found_error() -> None: + """Tests that async_process_response raises ProcessorNotFoundError for unknown MIME types.""" + with pytest.raises(ProcessorNotFoundError, match="this_type/does-not-exist"): + await async_process_response( + make_async_response(200, "", {"Content-Type": "this_type/does-not-exist"}), + ) + + +async def test_async_exception_bad_request() -> None: + """Tests that async_process_response raises RequestError for 400 responses.""" + with pytest.raises(RequestError): + await async_process_response( + make_async_response(400, "bad request data", {}), + ) + + +async def test_async_exception_internal_server_error() -> None: + """Tests that async_process_response raises InternalServerError for 500 responses.""" + with pytest.raises(InternalServerError): + await async_process_response(make_async_response(500, "server broke", {})) + + +async def test_async_exception_unexpected_status_code() -> None: + """Tests that async_process_response raises UnexpectedStatusCodeError for unknown status.""" + with pytest.raises(UnexpectedStatusCodeError): + await async_process_response(make_async_response(0, "", {})) + + +# --- WebSocket: NotImplementedError stubs --- + + +def test_websocket_set_state_not_supported(websocket_client: WebsocketClient) -> None: + """Tests that WebsocketClient.set_state raises NotImplementedError.""" + state = State(state="test", entity_id="sun.sun") + with pytest.raises( + NotImplementedError, + match="not supported over the WebSocket API", + ): + websocket_client.set_state(state) + + +def test_websocket_get_entity_histories_not_supported( + websocket_client: WebsocketClient, +) -> None: + """Tests that WebsocketClient.get_entity_histories raises NotImplementedError.""" + with pytest.raises( + NotImplementedError, + match="not supported over the WebSocket API", + ): + list(websocket_client.get_entity_histories()) + + +async def test_async_websocket_set_state_not_supported( + async_websocket_client: AsyncWebsocketClient, +) -> None: + """Tests that AsyncWebsocketClient.set_state raises NotImplementedError.""" + state = State(state="test", entity_id="sun.sun") + with pytest.raises( + NotImplementedError, + match="not supported over the WebSocket API", + ): + await async_websocket_client.set_state(state) + + +async def test_async_websocket_get_entity_histories_not_supported( + async_websocket_client: AsyncWebsocketClient, +) -> None: + """Tests that AsyncWebsocketClient.get_entity_histories raises NotImplementedError.""" + with pytest.raises( + NotImplementedError, + match="not supported over the WebSocket API", + ): + async for _ in async_websocket_client.get_entity_histories(): + pass + + +def test_client_default_session() -> None: + """Tests that Client creates a niquests.Session by default.""" + token = os.environ["HOMEASSISTANTAPI_TOKEN"] + client = Client(HA_URL, token) + assert isinstance(client._session, niquests.Session) + + +async def test_async_client_default_session() -> None: + """Tests that AsyncClient creates a niquests.AsyncSession by default.""" + token = os.environ["HOMEASSISTANTAPI_TOKEN"] + client = AsyncClient(HA_URL, token) + assert isinstance(client._session, niquests.AsyncSession) diff --git a/tests/test_events.py b/tests/test_events.py index a8bf6aa4..b7fbf231 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -2,33 +2,39 @@ import pytest -from homeassistant_api.models import ( - ConfigEntryChange, - ConfigEntryDisabler, - ConfigEntryState, -) -from homeassistant_api.websocket import WebsocketClient +from homeassistant_api import AsyncWebsocketClient +from homeassistant_api import WebsocketClient +from homeassistant_api.models import ConfigEntryChange +from homeassistant_api.models import ConfigEntryDisabler +from homeassistant_api.models import ConfigEntryState +from homeassistant_api.models.websocket import FiredEvent def test_listen_events(websocket_client: WebsocketClient) -> None: with websocket_client.listen_events("test_event") as events: websocket_client.fire_event( - "test_event", message="Triggered by websocket client" + "test_event", + message="Triggered by websocket client", ) for event in events: + assert isinstance(event, FiredEvent) assert event.origin == "LOCAL" assert event.event_type == "test_event" assert event.data["message"] == "Triggered by websocket client" break -async def test_async_listen_events(async_websocket_client: WebsocketClient) -> None: - async with async_websocket_client.async_listen_events("async_test_event") as events: - await async_websocket_client.async_fire_event( - "async_test_event", message="Triggered by async websocket client" +async def test_async_listen_events( + async_websocket_client: AsyncWebsocketClient, +) -> None: + async with async_websocket_client.listen_events("async_test_event") as events: + await async_websocket_client.fire_event( + "async_test_event", + message="Triggered by async websocket client", ) # Typing breaks when using zip in an async context, so break instead async for event in events: + assert isinstance(event, FiredEvent) assert event.origin == "LOCAL" assert event.event_type == "async_test_event" assert event.data["message"] == "Triggered by async websocket client" @@ -37,24 +43,25 @@ async def test_async_listen_events(async_websocket_client: WebsocketClient) -> N def test_listen_trigger(websocket_client: WebsocketClient) -> None: future = datetime.fromisoformat( - websocket_client.get_rendered_template("{{ (now() + timedelta(seconds=1)) }}") + websocket_client.get_rendered_template("{{ (now() + timedelta(seconds=1)) }}"), ) with websocket_client.listen_trigger( - "time", at=future.strftime("%H:%M:%S") + "time", + at=future.strftime("%H:%M:%S"), ) as triggers: for trigger in triggers: assert trigger["trigger"]["platform"] == "time" assert datetime.fromisoformat( - trigger["trigger"]["now"] + trigger["trigger"]["now"], ).timestamp() == pytest.approx(future.timestamp(), abs=1) break def test_listen_config_entries(websocket_client: WebsocketClient) -> None: with websocket_client.listen_config_entries() as flows: - for i, flow in zip(range(5), flows): - # The first "events" are currently available entries + for i, flow in enumerate(flows): if i == 0: + # The first "events" are currently available entries # Assumes that the first entry (sun.sun?) is enabled assert flow[0].type is None assert flow[0].entry.disabled_by is None @@ -85,12 +92,13 @@ def test_listen_config_entries(websocket_client: WebsocketClient) -> None: assert flow[0].type == ConfigEntryChange.UPDATED assert flow[0].entry.disabled_by is None assert flow[0].entry.state == ConfigEntryState.LOADED + break async def test_async_listen_config_entries( - async_websocket_client: WebsocketClient, + async_websocket_client: AsyncWebsocketClient, ) -> None: - async with async_websocket_client.async_listen_config_entries() as flows: + async with async_websocket_client.listen_config_entries() as flows: i = 0 async for flow in flows: if i == 0: @@ -100,8 +108,8 @@ async def test_async_listen_config_entries( assert flow[0].entry.state == ConfigEntryState.LOADED # Trigger an "updated" event - await async_websocket_client.async_disable_config_entry( - flow[0].entry.entry_id + await async_websocket_client.disable_config_entry( + flow[0].entry.entry_id, ) if i == 1: @@ -115,9 +123,7 @@ async def test_async_listen_config_entries( assert flow[0].entry.state == ConfigEntryState.NOT_LOADED # Restore original state - await async_websocket_client.async_enable_config_entry( - flow[0].entry.entry_id - ) + await async_websocket_client.enable_config_entry(flow[0].entry.entry_id) if i == 3: assert flow[0].type == ConfigEntryChange.UPDATED @@ -133,19 +139,22 @@ async def test_async_listen_config_entries( i += 1 -async def test_async_listen_trigger(async_websocket_client: WebsocketClient) -> None: +async def test_async_listen_trigger( + async_websocket_client: AsyncWebsocketClient, +) -> None: future = datetime.fromisoformat( - await async_websocket_client.async_get_rendered_template( - "{{ (now() + timedelta(seconds=1)) }}" - ) + await async_websocket_client.get_rendered_template( + "{{ (now() + timedelta(seconds=1)) }}", + ), ) - async with async_websocket_client.async_listen_trigger( - "time", at=future.strftime("%H:%M:%S") + async with async_websocket_client.listen_trigger( + "time", + at=future.strftime("%H:%M:%S"), ) as triggers: # Typing breaks when using zip in an async context, so break instead async for trigger in triggers: assert trigger["trigger"]["platform"] == "time" assert datetime.fromisoformat( - trigger["trigger"]["now"] + trigger["trigger"]["now"], ).timestamp() == pytest.approx(future.timestamp(), abs=1) break diff --git a/tests/test_models.py b/tests/test_models.py index 6d1f8d58..28afd784 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,12 +1,18 @@ """Module that tests model methods.""" import copy +from datetime import UTC from datetime import datetime import pytest -from homeassistant_api import Client, Domain +from homeassistant_api import AsyncClient +from homeassistant_api import Client +from homeassistant_api import Domain +from homeassistant_api.models.domains import BaseDomain +from homeassistant_api.models.entity import BaseGroup from homeassistant_api.models.events import Event +from homeassistant_api.models.history import History from homeassistant_api.models.states import State @@ -21,15 +27,15 @@ def test_entity_get_entity(cached_client: Client) -> None: assert person.thispersondoesnotexistplease -async def test_async_entity_get_entity(async_cached_client: Client) -> None: - person_test_suite = await async_cached_client.async_get_entity( +async def test_async_entity_get_entity(async_cached_client: AsyncClient) -> None: + person_test_suite = await async_cached_client.get_entity( group_id="person", slug="test_user", ) assert person_test_suite is not None state = copy.copy(person_test_suite.state) person = person_test_suite.group - assert state.state == (await person_test_suite.async_get_state()).state + assert state.state == (await person_test_suite.get_state()).state assert getattr(person, person_test_suite.slug) == person_test_suite with pytest.raises(AttributeError): assert person.thispersondoesnotexistplease @@ -44,11 +50,11 @@ def test_entity_update_state(cached_client: Client) -> None: assert new_state.state == "In the palm of your hand." -async def test_async_entity_update_state(async_cached_client: Client) -> None: - entity = await async_cached_client.async_get_entity(group_id="sun", slug="red_sun") +async def test_async_entity_update_state(async_cached_client: AsyncClient) -> None: + entity = await async_cached_client.get_entity(group_id="sun", slug="red_sun") assert entity is not None entity.state.state = "In the palm of my hand." - new_state = await entity.async_update_state() + new_state = await entity.update_state() assert new_state is not None assert new_state.state == "In the palm of my hand." @@ -58,10 +64,8 @@ def test_get_event(cached_client: Client) -> None: assert event is None -async def test_async_get_event(async_cached_client: Client) -> None: - event = await async_cached_client.async_get_event( - "my_favorite_candy_is_mike_and_ikes" - ) +async def test_async_get_event(async_cached_client: AsyncClient) -> None: + event = await async_cached_client.get_event("my_favorite_candy_is_mike_and_ikes") assert event is None @@ -71,10 +75,10 @@ def test_fire_event(cached_client: Client) -> None: assert event.fire() == "Event core_config_updated fired." -async def test_async_fire_event(async_cached_client: Client) -> None: - event = await async_cached_client.async_get_event("core_config_updated") +async def test_async_fire_event(async_cached_client: AsyncClient) -> None: + event = await async_cached_client.get_event("core_config_updated") assert event is not None - assert await event.async_fire() == "Event core_config_updated fired." + assert await event.fire() == "Event core_config_updated fired." def test_get_domain(cached_client: Client) -> None: @@ -85,8 +89,8 @@ def test_get_domain(cached_client: Client) -> None: assert notify.thisservicedoesnotexistplease -async def test_async_get_domains(async_cached_client: Client) -> None: - notify = await async_cached_client.async_get_domain("notify") +async def test_async_get_domains(async_cached_client: AsyncClient) -> None: + notify = await async_cached_client.get_domain("notify") assert notify is not None assert notify.domain_id == "notify" with pytest.raises(AttributeError): @@ -102,10 +106,10 @@ def test_entity_get_history(cached_client: Client) -> None: assert isinstance(state, State) -async def test_async_entity_get_history(async_cached_client: Client) -> None: - entity = await async_cached_client.async_get_entity(group_id="sun", slug="sun") +async def test_async_entity_get_history(async_cached_client: AsyncClient) -> None: + entity = await async_cached_client.get_entity(group_id="sun", slug="sun") assert entity is not None - history = await entity.async_get_history() + history = await entity.get_history() assert history is not None for state in history.states: assert isinstance(state, State) @@ -115,14 +119,15 @@ def test_entity_get_history_none(cached_client: Client) -> None: entity = cached_client.get_entity(group_id="sun", slug="red_sun") assert entity is not None history = entity.get_history( - start_timestamp=datetime(2015, 1, 1), end_timestamp=datetime(2020, 1, 1) + start_timestamp=datetime(2015, 1, 1, tzinfo=UTC), + end_timestamp=datetime(2020, 1, 1, tzinfo=UTC), ) assert history is None def test_event_from_json_raises() -> None: - """Tests that Event.from_json raises ValueError directing to from_json_with_client.""" - with pytest.raises(ValueError, match="does not support `from_json\\(\\)`"): + """Tests that Event.from_json raises NotImplementedError directing to from_json_with_client.""" + with pytest.raises(NotImplementedError, match="does not support `from_json\\(\\)`"): Event.from_json({}) @@ -132,10 +137,84 @@ def test_domain_from_json_with_client_missing_keys(cached_client: Client) -> Non Domain.from_json_with_client({"foo": "bar"}, cached_client) -async def test_async_entity_get_history_none(async_cached_client: Client) -> None: - entity = await async_cached_client.async_get_entity(group_id="sun", slug="red_sun") +async def test_async_entity_get_history_none(async_cached_client: AsyncClient) -> None: + entity = await async_cached_client.get_entity(group_id="sun", slug="red_sun") assert entity is not None - history = await entity.async_get_history( - start_timestamp=datetime(2015, 1, 1), end_timestamp=datetime(2020, 1, 1) + history = await entity.get_history( + start_timestamp=datetime(2015, 1, 1, tzinfo=UTC), + end_timestamp=datetime(2020, 1, 1, tzinfo=UTC), ) assert history is None + + +# --- BaseGroup: __getattr__ for nonexistent key --- + + +def test_base_group_getattr_nonexistent() -> None: + """Tests that BaseGroup.__getattr__ raises AttributeError for unknown attributes.""" + group = BaseGroup(group_id="test") + assert group.get_entity("nonexistent") is None + with pytest.raises(AttributeError): + _ = group.nonexistent_entity + + +def test_base_group_add_entity_not_implemented() -> None: + """Tests that BaseGroup._add_entity raises NotImplementedError.""" + group = BaseGroup(group_id="test") + with pytest.raises(NotImplementedError): + group._add_entity("slug", State(state="on", entity_id="test.slug")) + + +# --- BaseDomain: _add_service not implemented --- + + +def test_base_domain_add_service_not_implemented() -> None: + """Tests that BaseDomain._add_service raises NotImplementedError.""" + domain = BaseDomain(domain_id="test") + with pytest.raises(NotImplementedError): + domain._add_service("svc") + + +def test_base_domain_from_json_invalid_services_type(cached_client: Client) -> None: + """Tests that Domain.from_json_with_client raises TypeError when services is not a dict.""" + with pytest.raises(TypeError, match="Expected dict for services"): + Domain.from_json_with_client( + {"domain": "test", "services": "not_a_dict"}, + cached_client, + ) + + +# --- History: repr and entity_id --- + + +def test_history_repr() -> None: + """Tests that History has a meaningful repr with entity_id.""" + states = ( + State(state="on", entity_id="light.kitchen"), + State(state="off", entity_id="light.kitchen"), + ) + history = History(states=states) + assert history.entity_id == "light.kitchen" + assert "light.kitchen" in repr(history) + + +def test_history_entity_id_from_states() -> None: + """Tests that History.entity_id is derived from the states' entity_ids.""" + states = ( + State(state="on", entity_id="light.kitchen"), + State(state="off", entity_id="light.kitchen"), + ) + history = History(states=states) + assert history.entity_id == "light.kitchen" + + +# --- Domain: service access via attribute --- + + +def test_domain_service_attribute_access(cached_client: Client) -> None: + """Tests that Domain services are accessible as attributes.""" + notify = cached_client.get_domain("notify") + assert notify is not None + svc = notify.get_service("persistent_notification") + assert svc is not None + assert notify.persistent_notification == svc diff --git a/tests/test_websocket.py b/tests/test_websocket.py index e86f4d09..da96302b 100644 --- a/tests/test_websocket.py +++ b/tests/test_websocket.py @@ -1,84 +1,91 @@ -"""Unit tests for RawWebsocketClient, RawAsyncWebsocketClient, and WebsocketClient error paths.""" +"""Unit tests for WebsocketClient, AsyncWebsocketClient error paths.""" + +from typing import Any import pytest +from _pytest.monkeypatch import MonkeyPatch -from homeassistant_api.errors import ReceivingError, RequestError, ResponseError -from homeassistant_api.rawasyncwebsocket import RawAsyncWebsocketClient -from homeassistant_api.rawwebsocket import RawWebsocketClient +from homeassistant_api.asyncwebsocket import AsyncWebsocketClient +from homeassistant_api.errors import ReceivingError +from homeassistant_api.errors import ResponseError from homeassistant_api.models import websocket as ws_models +from homeassistant_api.websocket import WebsocketClient -def make_raw_client() -> RawWebsocketClient: - """Create a RawWebsocketClient without connecting.""" - return RawWebsocketClient("ws://localhost:8123/api/websocket", "fake_token") +def make_sync_client() -> WebsocketClient: + """Create a WebsocketClient without connecting.""" + return WebsocketClient("ws://localhost:8123/api/websocket", "fake_token") -def make_raw_async_client() -> RawAsyncWebsocketClient: - """Create a RawAsyncWebsocketClient without connecting.""" - return RawAsyncWebsocketClient("ws://localhost:8123/api/websocket", "fake_token") +def make_async_client() -> AsyncWebsocketClient: + """Create an AsyncWebsocketClient without connecting.""" + return AsyncWebsocketClient("ws://localhost:8123/api/websocket", "fake_token") def test_exit_without_connection() -> None: - """Tests __exit__ raises ReceivingError when connection is not open.""" - client = make_raw_client() - with pytest.raises(ReceivingError, match="Connection is not open"): + """Tests __exit__ raises AttributeError when used outside context manager.""" + client = make_sync_client() + with pytest.raises(AttributeError): client.__exit__(None, None, None) def test_send_without_connection() -> None: - """Tests _send raises ReceivingError when connection is not open.""" - client = make_raw_client() - with pytest.raises(ReceivingError, match="Connection is not open"): + """Tests _send raises AttributeError when used outside context manager.""" + client = make_sync_client() + with pytest.raises(AttributeError): client._send({"type": "test"}) def test_recv_without_connection() -> None: - """Tests _recv raises ReceivingError when connection is not open.""" - client = make_raw_client() - with pytest.raises(ReceivingError, match="Connection is not open"): + """Tests _recv raises AttributeError when used outside context manager.""" + client = make_sync_client() + with pytest.raises(AttributeError): client._recv() def test_handle_recv_message_without_id() -> None: """Tests handle_recv raises ReceivingError for messages missing an id.""" - client = make_raw_client() + client = make_sync_client() with pytest.raises(ReceivingError, match="without an id"): client.handle_recv({"type": "result", "success": True}) def test_parse_response_error_result() -> None: - """Tests parse_response raises RequestError for failed result messages.""" - client = make_raw_client() + """Tests parse_response raises ResponseError for failed result messages.""" + client = make_sync_client() client._result_responses[1] = None - with pytest.raises(RequestError): + with pytest.raises(ResponseError): client.parse_response( { "id": 1, "type": "result", "success": False, "error": {"code": "not_found", "message": "Entity not found"}, - } + }, ) def test_parse_response_unexpected_type() -> None: """Tests parse_response raises ReceivingError for unknown message types.""" - client = make_raw_client() + client = make_sync_client() with pytest.raises(ReceivingError, match="unexpected message type"): client.parse_response({"id": 1, "type": "unknown_type"}) -def test_authentication_phase_invalid_welcome(monkeypatch) -> None: +def test_authentication_phase_invalid_welcome(monkeypatch: MonkeyPatch) -> None: """Tests authentication_phase raises ResponseError on invalid welcome message.""" - client = make_raw_client() + client = make_sync_client() monkeypatch.setattr(client, "_recv", lambda: {"type": "not_auth_required"}) with pytest.raises( - ResponseError, match="Unexpected response during authentication" + ResponseError, + match="Unexpected response during authentication", ): client.authentication_phase() -def test_authentication_phase_unexpected_auth_response(monkeypatch) -> None: +def test_authentication_phase_unexpected_auth_response( + monkeypatch: MonkeyPatch, +) -> None: """Tests authentication_phase raises ResponseError when AuthOk.model_validate raises a non-ValidationError.""" call_count = 0 @@ -89,62 +96,67 @@ def fake_recv(): return {"type": "auth_required", "ha_version": "2024.1.0"} return {"type": "auth_ok", "ha_version": "2024.1.0", "message": "unexpected"} - client = make_raw_client() + client = make_sync_client() monkeypatch.setattr(client, "_recv", fake_recv) - monkeypatch.setattr(client, "_send", lambda data: None) + monkeypatch.setattr(client, "_send", lambda _: None) # Patch AuthOk.model_validate to raise a non-ValidationError exception - def raise_runtime_error(*args, **kwargs): - raise RuntimeError("something went wrong") + def raise_runtime_error(*args: Any, **kwargs: Any): # noqa: ARG001 + msg = "something went wrong" + raise RuntimeError(msg) monkeypatch.setattr(ws_models.AuthOk, "model_validate", raise_runtime_error) with pytest.raises( - ResponseError, match="Unexpected response during authentication" + ResponseError, + match="Unexpected response during authentication", ): client.authentication_phase() async def test_async_aexit_without_connection() -> None: - """Tests __aexit__ raises ReceivingError when connection is not open.""" - client = make_raw_async_client() - with pytest.raises(ReceivingError, match="Connection is not open"): + """Tests __aexit__ raises AttributeError when used outside context manager.""" + client = make_async_client() + with pytest.raises(AttributeError): await client.__aexit__(None, None, None) async def test_async_send_without_connection() -> None: - """Tests _async_send raises ReceivingError when connection is not open.""" - client = make_raw_async_client() - with pytest.raises(ReceivingError, match="Connection is not open"): + """Tests _async_send raises AttributeError when used outside context manager.""" + client = make_async_client() + with pytest.raises(AttributeError): await client._async_send({"type": "test"}) async def test_async_recv_without_connection() -> None: - """Tests _async_recv raises ReceivingError when connection is not open.""" - client = make_raw_async_client() - with pytest.raises(ReceivingError, match="Connection is not open"): + """Tests _async_recv raises AttributeError when used outside context manager.""" + client = make_async_client() + with pytest.raises(AttributeError): await client._async_recv() -async def test_async_authentication_phase_invalid_welcome(monkeypatch) -> None: - """Tests async_authentication_phase raises ResponseError on invalid welcome message.""" - client = make_raw_async_client() +async def test_async_authentication_phase_invalid_welcome( + monkeypatch: MonkeyPatch, +) -> None: + """Tests authentication_phase raises ResponseError on invalid welcome message.""" + client = make_async_client() async def fake_recv(): return {"type": "not_auth_required"} monkeypatch.setattr(client, "_async_recv", fake_recv) with pytest.raises( - ResponseError, match="Unexpected response during authentication" + ResponseError, + match="Unexpected response during authentication", ): - await client.async_authentication_phase() + await client.authentication_phase() async def test_async_authentication_phase_unexpected_auth_response( - monkeypatch, + monkeypatch: MonkeyPatch, ) -> None: - """Tests async_authentication_phase raises ResponseError when AuthOk.model_validate raises a non-ValidationError.""" + """Tests authentication_phase raises ResponseError when AuthOk.model_validate raises a non-ValidationError.""" call_count = 0 async def fake_recv(): @@ -154,20 +166,22 @@ async def fake_recv(): return {"type": "auth_required", "ha_version": "2024.1.0"} return {"type": "auth_ok", "ha_version": "2024.1.0", "message": "unexpected"} - client = make_raw_async_client() + client = make_async_client() monkeypatch.setattr(client, "_async_recv", fake_recv) - async def fake_send(data): + async def fake_send(data: Any): pass monkeypatch.setattr(client, "_async_send", fake_send) - def raise_runtime_error(*args, **kwargs): - raise RuntimeError("something went wrong") + def raise_runtime_error(*args: Any, **kwargs: Any) -> None: # noqa: ARG001 + msg = "something went wrong" + raise RuntimeError(msg) monkeypatch.setattr(ws_models.AuthOk, "model_validate", raise_runtime_error) with pytest.raises( - ResponseError, match="Unexpected response during authentication" + ResponseError, + match="Unexpected response during authentication", ): - await client.async_authentication_phase() + await client.authentication_phase() diff --git a/volumes/config/.storage/core.restore_state b/volumes/config/.storage/core.restore_state index 74c6c01e..cc21ff18 100644 --- a/volumes/config/.storage/core.restore_state +++ b/volumes/config/.storage/core.restore_state @@ -4,17 +4,17 @@ "key": "core.restore_state", "data": [ { - "state": {"entity_id":"person.test_user","state":"unknown","attributes":{"editable":true,"id":"test_user","device_trackers":[],"user_id":"e85fc7b7b8924dc9b024ce90ad23799e","friendly_name":"Test User"},"last_changed":"2026-03-16T04:15:22.046235+00:00","last_reported":"2026-03-16T04:15:22.812267+00:00","last_updated":"2026-03-16T04:15:22.812267+00:00","context":{"id":"01KKTDP3NWBAH8N001JPZDXJG0","parent_id":null,"user_id":null}}, + "state": {"entity_id":"person.test_user","state":"unknown","attributes":{"editable":true,"id":"test_user","device_trackers":[],"user_id":"e85fc7b7b8924dc9b024ce90ad23799e","friendly_name":"Test User"},"last_changed":"2026-03-22T16:31:31.712211+00:00","last_reported":"2026-03-22T16:31:32.696411+00:00","last_updated":"2026-03-22T16:31:32.696411+00:00","context":{"id":"01KMB66CARJBA2W2SJVNFAHX3M","parent_id":null,"user_id":null}}, "extra_data": null, - "last_seen": "2026-03-16T04:30:22.813296+00:00" + "last_seen": "2026-03-22T22:01:32.720708+00:00" }, { - "state": {"entity_id":"event.backup_automatic_backup","state":"unknown","attributes":{"event_types":["completed","failed","in_progress"],"event_type":null,"friendly_name":"Backup Automatic backup"},"last_changed":"2026-03-16T04:15:22.173909+00:00","last_reported":"2026-03-16T04:15:22.173909+00:00","last_updated":"2026-03-16T04:15:22.173909+00:00","context":{"id":"01KKTDP31XEKR4ZT2TNHKVGE1G","parent_id":null,"user_id":null}}, + "state": {"entity_id":"event.backup_automatic_backup","state":"unknown","attributes":{"event_types":["completed","failed","in_progress"],"event_type":null,"friendly_name":"Backup Automatic backup"},"last_changed":"2026-03-22T16:31:32.709444+00:00","last_reported":"2026-03-22T16:31:32.709444+00:00","last_updated":"2026-03-22T16:31:32.709444+00:00","context":{"id":"01KMB66CB50RRBP5FEADY823M3","parent_id":null,"user_id":null}}, "extra_data": { "last_event_type": null, "last_event_attributes": null }, - "last_seen": "2026-03-16T04:30:22.813296+00:00" + "last_seen": "2026-03-22T22:01:32.720708+00:00" } ] } \ No newline at end of file