diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index c06a52f4..594c0c9a 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -3,8 +3,8 @@ ARG VARIANT="3" FROM mcr.microsoft.com/vscode/devcontainers/python:${VARIANT} ARG NODE_VERSION="24" -ARG POETRY_VERSION="1.8.2" -ARG POETRY_SRC="https://install.python-poetry.org" +ARG UV_VERSION="0.8.17" +ARG UV_SRC="https://astral.sh/uv" # https://github.com/microsoft/vscode-dev-containers/blob/main/containers/go/.devcontainer/base.Dockerfile ENV USERNAME=vscode @@ -25,9 +25,7 @@ RUN apt-get update -y \ USER vscode WORKDIR /home/vscode -RUN curl -fsSL -o install-poetry.py "${POETRY_SRC}" \ - && python install-poetry.py --version $POETRY_VERSION \ - && rm install-poetry.py +RUN curl -fsSL "${UV_SRC}/${UV_VERSION}/install.sh" | sh RUN mkdir -p .config/git \ && echo ".vscode/*" >> .config/git/ignore \ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0d6bb46e..2f9e5640 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,12 +5,11 @@ "dockerfile": "Dockerfile", "args": { "NODE_VERSION": "24", - "POETRY_VERSION": "1.8.2", - "VARIANT": "3.12" + "UV_VERSION": "0.8.17", + "VARIANT": "3.14" } }, "remoteEnv": { - "POETRY_VIRTUALENVS_IN_PROJECT": "true", "PATH": "${containerEnv:PATH}:/home/vscode/.local/bin" }, "extensions": [ @@ -19,6 +18,6 @@ "EditorConfig.EditorConfig", "esbenp.prettier-vscode" ], - "postCreateCommand": "poetry install && npm install", + "postCreateCommand": "uv sync && npm install", "remoteUser": "vscode" } diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 8f000cae..e6a5cbdb 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -6,11 +6,7 @@ inputs: python_version: description: The Python version. required: false - default: '3.12' - poetry_version: - description: The Poetry version. - required: false - default: '1.8.2' + default: '3.14' just_version: description: The just version. required: false @@ -23,46 +19,20 @@ inputs: runs: using: composite steps: - - name: Setup Poetry cache on Linux - uses: actions/cache@v4 - if: runner.os == 'Linux' - with: - key: poetry-${{ inputs.poetry_version }}-${{ inputs.python_version }}-${{ runner.os }}-${{ runner.arch }} - path: | - ~/.local/bin - ~/.local/share/pypoetry - - name: Setup Poetry cache on macOS - uses: actions/cache@v4 - if: runner.os == 'macOS' - with: - key: poetry-${{ inputs.poetry_version }}-${{ inputs.python_version }}-${{ runner.os }}-${{ runner.arch }} - path: | - ~/.local/bin - ~/.local/share/pypoetry - ~/Library/Application Support/pypoetry - name: Setup just uses: extractions/setup-just@v4 with: just-version: ${{ inputs.just_version }} - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: ${{ inputs.python_version }} - - name: Setup Poetry - uses: Gr1N/setup-poetry@v9 - with: - poetry-version: ${{ inputs.poetry_version }} - - name: Setup Python with cache - uses: actions/setup-python@v5 - if: inputs.install_dependencies == 'true' + - name: Setup uv + uses: astral-sh/setup-uv@v7 with: - cache: poetry python-version: ${{ inputs.python_version }} + enable-cache: true - name: Check lockfile if: inputs.install_dependencies == 'true' shell: bash - run: poetry check --lock + run: uv lock --check - name: Install dependencies if: inputs.install_dependencies == 'true' shell: bash - run: poetry install --sync + run: uv sync diff --git a/.github/workflows/_build.yml b/.github/workflows/_build.yml index 8324ef2f..7717e29d 100644 --- a/.github/workflows/_build.yml +++ b/.github/workflows/_build.yml @@ -8,7 +8,7 @@ on: description: The Python version. type: string required: false - default: '3.12' + default: '3.14' runs_on: description: The runner environment. type: string diff --git a/.github/workflows/_publish.yml b/.github/workflows/_publish.yml index cc6b0609..8be71cff 100644 --- a/.github/workflows/_publish.yml +++ b/.github/workflows/_publish.yml @@ -31,7 +31,6 @@ jobs: name: ${{ inputs.artifact_name }} path: dist/ - name: Publish - run: poetry publish --skip-existing -u $USERNAME -p $PASSWORD + run: uv publish --check-url https://pypi.org/simple/ env: - USERNAME: __token__ - PASSWORD: ${{ secrets.registry_token }} + UV_PUBLISH_TOKEN: ${{ secrets.registry_token }} diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index f756a8dd..bf7c3fb2 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -20,9 +20,10 @@ jobs: os: - ubuntu-latest python: - - '3.10' - '3.11' - '3.12' + - '3.13' + - '3.14' include: - os: ubuntu-latest os_name: Linux @@ -47,6 +48,8 @@ jobs: python: - '3.11' - '3.12' + - '3.13' + - '3.14' steps: - name: Checkout uses: actions/checkout@v7 @@ -69,9 +72,10 @@ jobs: os: - ubuntu-latest python: - - '3.10' - '3.11' - '3.12' + - '3.13' + - '3.14' include: - os: ubuntu-latest os_name: Linux @@ -86,9 +90,10 @@ jobs: os: - ubuntu-latest python: - - '3.10' - '3.11' - '3.12' + - '3.13' + - '3.14' include: - os: ubuntu-latest os_name: Linux diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml index 40f49dad..637aafca 100644 --- a/.github/workflows/generate.yml +++ b/.github/workflows/generate.yml @@ -35,12 +35,12 @@ jobs: uses: ./.github/actions/setup-node with: install_dependencies: 'false' - - name: Normalize poetry.lock - run: poetry lock --no-update + - name: Normalize uv.lock + run: uv lock - name: Normalize package-lock.json run: npm install - name: Install dependencies - run: poetry install --sync + run: uv sync - name: Generate code run: npm run generate - name: Commit diff --git a/.github/workflows/prune.yml b/.github/workflows/prune.yml index 562d16cc..1c484837 100644 --- a/.github/workflows/prune.yml +++ b/.github/workflows/prune.yml @@ -7,7 +7,7 @@ on: - cron: '0 15 * * 3' jobs: - tag: + branches: name: Prune Branches runs-on: 'ubuntu-latest' timeout-minutes: 30 diff --git a/.github/workflows/semantic-release.yml b/.github/workflows/semantic-release.yml index b2e3aab9..b6c2c920 100644 --- a/.github/workflows/semantic-release.yml +++ b/.github/workflows/semantic-release.yml @@ -25,6 +25,31 @@ jobs: uses: actions/checkout@v7 with: fetch-depth: 0 + - name: Mirror prerelease tags as semver + run: | + # Prerelease tags use PEP 440 (v3.0.0b1) to match the published + # package version, but semantic-release only understands semver and + # silently ignores tags it cannot parse. Without this, every run + # reports the first prerelease of the channel as the next version. + # Mirror each PEP 440 prerelease tag onto its semver equivalent + # (v3.0.0-beta.1). These are local to the run and are never pushed; + # the PEP 440 tags remain the only real ones. The channel notes the + # Version workflow records against the tagged commit apply to the + # mirrored tag too, since notes attach to commits, not tags. + for tag in $(git tag --list 'v*'); do + semver="$( + printf '%s' "${tag#v}" | sed -E \ + -e 's/^([0-9]+\.[0-9]+\.[0-9]+)a([0-9]+)$/\1-alpha.\2/' \ + -e 's/^([0-9]+\.[0-9]+\.[0-9]+)b([0-9]+)$/\1-beta.\2/' \ + -e 's/^([0-9]+\.[0-9]+\.[0-9]+)rc([0-9]+)$/\1-rc.\2/' + )" + if [ "$semver" = "${tag#v}" ]; then + continue + fi + + git tag --force "v$semver" "$tag^{commit}" + echo "Mirrored $tag as v$semver." + done - name: Semantic release id: release uses: cycjimmy/semantic-release-action@v6 diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index 9bde1873..723815d3 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -31,20 +31,28 @@ jobs: passphrase: ${{ secrets.GPG_PASSPHRASE }} - name: Setup uses: ./.github/actions/setup + # uv normalizes the semver version semantic-release computes + # (3.0.0-beta.1) to PEP 440 (3.0.0b1), which is what gets committed, + # tagged and published. The Semantic Release workflow mirrors that tag + # back to semver so it can pick up where the last prerelease left off. - name: Cut ${{ github.event.inputs.version }} version run: | - poetry version "${{ github.event.inputs.version }}" + uv version "${{ github.event.inputs.version }}" just version + # semantic-release only treats a prerelease tag as released when a note + # records the channel it went out on, so record it here, once the release + # exists. The note is keyed to the commit rather than the tag, which is + # what lets the mirrored semver tag pick it up. - name: Record prerelease channel - env: - VERSION: ${{ github.event.inputs.version }} run: | - case "$VERSION" in - *-*) channel="${VERSION#*-}"; channel="${channel%%.*}" ;; + tag="v$(uv version --short)" + case "$(uv version --short)" in + *.*.*a[0-9]*) channel=alpha ;; + *.*.*b[0-9]*) channel=beta ;; + *.*.*rc[0-9]*) channel=rc ;; *) echo "Stable release, no channel note required."; exit 0 ;; esac - git fetch origin "+refs/notes/semantic-release:refs/notes/semantic-release" || true - git notes --ref semantic-release add --force \ - --message "{\"channels\":[\"$channel\"]}" "v$VERSION^{commit}" - git push origin refs/notes/semantic-release - echo "Recorded v$VERSION on the '$channel' channel." + git notes --ref "semantic-release-$tag" add --force \ + --message "{\"channels\":[\"$channel\"]}" "$tag^{commit}" + git push origin "refs/notes/semantic-release-$tag" + echo "Recorded $tag on the '$channel' channel." diff --git a/.gitignore b/.gitignore index ff0aacf2..4a43d43e 100644 --- a/.gitignore +++ b/.gitignore @@ -110,12 +110,12 @@ ipython_config.py # install all needed dependencies. #Pipfile.lock -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# uv +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock +# https://docs.astral.sh/uv/concepts/projects/sync/#checking-the-lockfile +#uv.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. diff --git a/.python-version b/.python-version index 8531a3b7..6324d401 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.12.2 +3.14 diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 00000000..edbe6d99 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,259 @@ +# Migrating from seam v2 to v3 + +This guide covers upgrading from `seam` v2.x to v3 of the [Seam Python SDK](https://github.com/seamapi/python). + +Version 3 replaces the underlying HTTP library, adds client-side validation and explicit null support, and regenerates the API surface against the latest Seam API. Most application code — authentication, method names, resource models, action attempts, and pagination — works unchanged. The breaking changes are concentrated in client configuration and error handling. + +## Installation + +```sh +pip install --upgrade 'seam>=3,<4' +``` + +## Summary of breaking changes + +| Change | Affects you if... | +| ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| [Python 3.11+ required](#python-311-or-later-is-required) | You run Python 3.10 | +| [httpx replaces niquests](#httpx-replaces-niquests) | You pass `niquests_options`, catch `niquests` exceptions, or touch `seam.client` directly | +| [`retries` takes an `httpx_retries.Retry`](#retry-configuration-uses-httpx-retries) | You pass a custom `retries` option | +| [Endpoints validate parameters client-side](#client-side-parameter-validation) | You call endpoints with no parameters, or rely on the server's 400 response | +| [`lts_version` removed](#lts_version-is-removed) | You read `Seam.lts_version` or the `seam-lts-version` header | +| [Preferred HTTP methods and URL search params](#endpoints-use-preferred-http-methods) | You inspect traffic in a proxy, mock server, or firewall rules | + +## Python 3.11 or later is required + +Version 2 supported Python 3.10. Version 3 requires Python >= 3.11 and is tested on Python 3.11 through 3.14. + +## httpx replaces niquests + +The SDK's HTTP layer is now [httpx](https://www.python-httpx.org/) instead of [niquests](https://niquests.readthedocs.io/). This surfaces in three places. + +### The `niquests_options` option is renamed to `httpx_options` + +Options are now passed to the underlying `httpx.Client`, so both the option name and its contents change. For example, connection pool limits: + +```python +# v2 +seam = Seam( + api_key="your-api-key", + niquests_options={"pool_connections": 20, "pool_maxsize": 25}, +) + +# v3 +from httpx import Limits + +seam = Seam( + api_key="your-api-key", + httpx_options={ + "limits": Limits(max_connections=25, max_keepalive_connections=20), + }, +) +``` + +This applies to `Seam()`, `Seam.from_api_key()`, `Seam.from_personal_access_token()`, and `SeamWithoutWorkspace`. + +### Transport-level exceptions are httpx exceptions + +Requests that time out now raise `httpx.TimeoutException` instead of `niquests.exceptions.Timeout`, and connection failures raise httpx transport errors (`httpx.ConnectError`, etc.) instead of niquests/urllib3 ones. + +```python +# v2 +import niquests + +try: + seam.devices.list() +except niquests.exceptions.Timeout: + ... + +# v3 +import httpx + +try: + seam.devices.list() +except httpx.TimeoutException: + ... +``` + +Seam API errors are unchanged: `SeamHttpApiError`, `SeamHttpInvalidInputError`, and `SeamHttpUnauthorizedError` are raised exactly as in v2. + +### `seam.client` is an httpx.Client + +If you access the client directly, it is now an `httpx.Client` subclass rather than a niquests `Session`. Notably, response hooks are registered via `event_hooks` instead of `hooks`. + +## Retry configuration uses httpx-retries + +The `retries` option now takes a `Retry` object from [httpx-retries](https://will-ockmore.github.io/httpx-retries/) instead of `urllib3.util.retry.Retry`. The class is re-exported from `seam` for convenience: + +```python +# v2 +from urllib3.util.retry import Retry + +seam = Seam(api_key="your-api-key", retries=Retry(total=3)) + +# v3 +from seam import Seam, Retry + +seam = Seam( + api_key="your-api-key", + retries=Retry(total=3, backoff_factor=0.5, status_forcelist=[503]), +) +``` + +The default retry policy is now explicit and documented. Out of the box, the SDK makes up to three attempts: the initial request and two retries. Retries are limited to `GET`, `HEAD`, `OPTIONS`, `PUT`, and `DELETE` requests that fail because of a transport error, timeout, HTTP 429 response, or HTTP 5xx response. `POST` and `PATCH` requests are never retried. Retries use exponential backoff with jitter, and a `Retry-After` header is honored instead of the calculated backoff. + +In v2, the default was urllib3's implicit `Retry()` (connection-level retries only, with no retries on HTTP status codes such as 429 or 5xx). If you depended on requests never being retried on 429/5xx, pass an explicit policy, e.g. `retries=Retry(total=0)`. + +## Client-side parameter validation + +Endpoints that require at least one parameter now raise `ValueError` locally instead of sending the request and letting the server reject it: + +```python +# v2: raises SeamHttpInvalidInputError after a round trip to the server +# v3: raises ValueError("At least one parameter is required for /locks/get") +seam.locks.get() +``` + +`create_paginator` is validated the same way. It raises `ValueError` when given a non-paginated endpoint, and when given an endpoint that requires parameters without any: + +```python +# v3: raises ValueError - /devices/get is not paginated +seam.create_paginator(seam.devices.get) +``` + +If you catch `SeamHttpInvalidInputError` around calls that could be sent with no parameters, also handle `ValueError` (or fix the call site). + +## `lts_version` is removed + +The `Seam.lts_version` / `SeamWithoutWorkspace.lts_version` attribute and the `seam-lts-version` request header no longer exist. There is no replacement; use the package version instead: + +```python +from importlib.metadata import version + +version("seam") +``` + +## Endpoints use preferred HTTP methods + +In v2, every endpoint was called with `POST` and a JSON body. In v3, endpoints use the HTTP method the Seam API prefers: + +- Read endpoints (`get`, `list`, and friends) use `GET`, with parameters sent as URL search params serialized per [Seam's URL search params standard](https://github.com/seamapi/url-search-params-serializer). +- Update endpoints use `PATCH` or `PUT`. +- Delete endpoints use `DELETE`. +- Create and action endpoints (`create`, `lock_door`, etc.) remain `POST`. + +Method signatures, arguments, and return values are unchanged — this only matters if something outside your code observes the HTTP traffic: proxy or firewall rules that allowlist methods, request logging, or test mocks registered against `POST` routes. Note the interaction with the new retry defaults: because reads are now `GET`, they are retried by default, which they were not in v2 (as `POST`). + +If you call the Seam API with your own HTTP client, the serializer used for `GET` params is exported: + +```python +import httpx +from seam import serialize_url_search_params + +httpx.get( + "https://connect.getseam.com/devices/list", + params=serialize_url_search_params({"device_ids": ["device1", "device2"]}), + headers={"Authorization": "Bearer your-api-key"}, +) +``` + +## New in v3 + +These are additions, not breaking changes, but they are worth adopting while you migrate. + +### Explicit null with `NULL` + +The Seam API distinguishes an omitted parameter from one explicitly set to null: in an update request, an omitted parameter leaves the current value unchanged, while a null parameter unsets it. Version 2 had no way to send null — `None` always meant "omit". Version 3 keeps that behavior for `None` and adds a `NULL` sentinel for sending an explicit null: + +```python +from seam import NULL, Seam + +seam = Seam() + +# Leaves the name unchanged (same as v2). +seam.devices.update(device_id="your-device-id", name=None) + +# Unsets the name (new in v3). +seam.devices.update(device_id="your-device-id", name=NULL) +``` + +Only parameters the Seam API documents as nullable are typed to accept `NULL`, so a type checker will flag misuse. The sentinel's type is exported as `Null` for annotating your own code. + +### New exports + +`seam` now exports `NULL`, `Null`, `Retry` (from httpx-retries), `UrlSearchParams`, `serialize_url_search_params`, `update_url_search_params`, and `UnserializableParamError`, alongside everything exported in v2. + +## Migration checklist + +1. Upgrade your runtime to Python 3.11 or later. +2. Update the dependency: `seam>=3,<4`. +3. Rename `niquests_options` to `httpx_options` and translate its contents to `httpx.Client` options. +4. Replace `urllib3.util.retry.Retry` with `seam.Retry` (httpx-retries) in any `retries` argument, and review the new default retry policy. +5. Replace handling of `niquests`/`urllib3` exceptions with the `httpx` equivalents (`httpx.TimeoutException`, `httpx.ConnectError`, ...). Seam error classes are unchanged. +6. Remove any use of `lts_version` or the `seam-lts-version` header. +7. Handle `ValueError` from endpoints and `create_paginator` where calls might carry no parameters. +8. If proxies, firewalls, or test mocks assume all requests are `POST`, update them for `GET`/`PATCH`/`PUT`/`DELETE`. +9. Optionally, adopt `NULL` where you need to unset nullable values. + +# Migrating from seam v1 to v2 + +If you are still on v1.x, migrate to v2 first (or apply both guides together). Version 2 is a much smaller upgrade than v3: client configuration, authentication, endpoint methods, and error handling are all unchanged. The breaking changes are in resource objects and one class rename. + +## Summary of breaking changes + +| Change | Affects you if... | +| ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| [Nested resource properties are typed objects](#nested-resource-properties-are-typed-objects) | You treat nested properties as dicts, or rely on unknown-attribute reads | +| [`SeamMultiWorkspace` renamed to `SeamWithoutWorkspace`](#seammultiworkspace-is-renamed-to-seamwithoutworkspace) | You use `SeamMultiWorkspace` | + +## Nested resource properties are typed objects + +In v1, nested properties on resources — for example `device.properties` or `action_attempt.result` — were dict subclasses with attribute access layered on top. In v2, they hydrate as typed dataclasses scoped to their parent resource, such as `Device.Properties` and `ActionAttempt.Result`, so IDEs and type checkers can see their fields. + +Attribute access and dictionary-style _reads_ keep working: + +```python +device = seam.devices.get(device_id="your-device-id") + +device.properties.locked # still works +device.properties["locked"] # still works +device.properties.get("online") # still works +"locked" in device.properties # still works +``` + +What breaks: + +- **They are no longer dicts.** `isinstance(device.properties, dict)` is now `False`, and mutation (`device.properties["x"] = ...`) and dict-only methods such as `.items()` and `.values()` are gone. Iterate over `.keys()` and index instead. +- **Typoed attributes raise `AttributeError`.** In v1, reading an unknown attribute silently returned (and inserted) an empty mapping, so typos went unnoticed and were truthy-checked as empty dicts. In v2 they fail loudly — code that probed for optional fields via bare attribute access should use `.get("field")` or `hasattr`. +- **Undocumented nested fields are stripped.** API fields not (yet) in the SDK's generated types are dropped during hydration instead of being passed through. If you depend on a field the SDK does not model, upgrade the SDK to a version that includes it. + +Free-form record properties, such as `custom_metadata`, remain plain mappings and are not affected. + +## `SeamMultiWorkspace` is renamed to `SeamWithoutWorkspace` + +The client for personal access tokens without a workspace is renamed; there is no compatibility alias. Its constructor, options, and methods are otherwise identical: + +```python +# v1 +from seam import SeamMultiWorkspace + +seam = SeamMultiWorkspace(personal_access_token="your-personal-access-token") + +# v2 +from seam import SeamWithoutWorkspace + +seam = SeamWithoutWorkspace(personal_access_token="your-personal-access-token") +``` + +The abstract base class is likewise renamed from `AbstractSeamMultiWorkspace` to `AbstractSeamWithoutWorkspace`. + +## New in v2 + +Version 2.2 also reads authentication from the environment: `SEAM_PERSONAL_ACCESS_TOKEN` and `SEAM_WORKSPACE_ID` are picked up when no explicit credentials are passed (`SEAM_API_KEY` was already supported in v1). Setting both `SEAM_API_KEY` and `SEAM_PERSONAL_ACCESS_TOKEN` is an error. + +## Migration checklist + +1. Update the dependency: `seam>=2,<3`. +2. Rename `SeamMultiWorkspace` to `SeamWithoutWorkspace` (and `AbstractSeamMultiWorkspace` to `AbstractSeamWithoutWorkspace`). +3. Replace dict-style mutation and `.items()`/`.values()`/`isinstance(..., dict)` usage on nested resource properties with attribute access or `.keys()` iteration. +4. Replace bare attribute probes for optional nested fields with `.get()` or `hasattr` — unknown attributes now raise `AttributeError`. diff --git a/README.rst b/README.rst index 65cad433..d93f018a 100644 --- a/README.rst +++ b/README.rst @@ -47,9 +47,11 @@ Contents * `Action Attempts`_ + * `Setting a Param to Null`_ + * `Pagination`_ - * `Manually fetch pages with the nextPageCursor`_ + * `Manually fetch pages with the next_page_cursor`_ * `Resume pagination`_ @@ -69,7 +71,11 @@ Contents * `Setting the request timeout`_ - * `Configuring the niquests session`_ + * `Configuring retries`_ + + * `Configuring the httpx client`_ + + * `Serializing URL search params`_ * `Development and Testing`_ @@ -276,14 +282,64 @@ For example: except SeamActionAttemptTimeoutError as e: print("Door took too long to unlock") +Setting a Param to Null +~~~~~~~~~~~~~~~~~~~~~~~ + +The Seam API tells an omitted param apart from one explicitly set to null. +In an update request, an omitted param leaves the current value unchanged, +while a null param unsets it. + +Python has a single nil value `None` which represents an undefined parameter. This SDK provides an explicit null value to send in requests. +A param set to ``None`` is omitted, and a param set to ``NULL`` is sent as `null`: + +.. code-block:: python + + from seam import NULL, Seam + + seam = Seam() + + # Leaves the name unchanged. + seam.devices.update(device_id="your-device-id", name=None) + + # Unsets the name. + seam.devices.update(device_id="your-device-id", name=NULL) + +Because unsetting a value cannot be undone, ``None`` means the safe option of +omitting the param, and sending null is always explicit. +This is why a param is never sent as null by default, +even though ``None`` is the natural way to spell null in Python. + +``NULL`` behaves the same way in a request body and in a URL search param. +Its type is exported as ``Null`` for annotating your own code: + +.. code-block:: python + + from typing import Optional, Union + + from seam import NULL, Null + + name: Optional[Union[str, Null]] = NULL + +Only params the Seam API documents as nullable accept ``NULL``. +The generated method signatures say which ones those are, +so a type checker rejects ``NULL`` anywhere else: + +.. code-block:: python + + # name is nullable, so it may be unset. + seam.devices.update(device_id="your-device-id", name=NULL) + + # is_managed is not, so this fails the type check. + seam.devices.update(device_id="your-device-id", is_managed=NULL) + Pagination ~~~~~~~~~~ Some Seam API endpoints that return lists of resources support pagination. Use the ``SeamPaginator`` class to fetch and process resources across multiple pages. -Manually fetch pages with the nextPageCursor -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Manually fetch pages with the next_page_cursor +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: python @@ -470,22 +526,105 @@ Pass the ``timeout`` option, in seconds, to override this: Setting it to ``None`` disables the timeout entirely. -A request that exceeds the timeout raises ``niquests.exceptions.Timeout``. +A request that exceeds the timeout raises ``httpx.TimeoutException``. + +Configuring retries +^^^^^^^^^^^^^^^^^^^ + +By default, the SDK makes up to three attempts: the initial request and two +retries. Retries are limited to ``GET``, ``HEAD``, ``OPTIONS``, ``PUT``, and +``DELETE`` requests that fail because of a transport error, timeout, HTTP 429 +response, or HTTP 5xx response. ``POST`` and ``PATCH`` requests are not retried. + +Retries use exponential backoff with jitter: approximately 200–240 ms before +the first retry and 400–480 ms before the second. A ``Retry-After`` header is +honored instead of the calculated backoff. The request timeout is reset for +each attempt. -Configuring the niquests session -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Pass the ``retries`` option to configure retry behavior. +Retries are handled by `httpx-retries `_, +and its ``Retry`` class is re-exported from ``seam`` for convenience: -For control the options above do not cover, pass ``niquests_options``. -These are handed to the underlying niquests ``Session`` and take +.. code-block:: python + + from seam import Seam, Retry + + seam = Seam( + api_key="your-api-key", + retries=Retry(total=3, backoff_factor=0.5, status_forcelist=[503]), + ) + +Configuring the httpx client +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For control the options above do not cover, pass ``httpx_options``. +These are handed to the underlying httpx ``Client`` and take precedence over the defaults the SDK sets: .. code-block:: python + from httpx import Limits + seam = Seam( api_key="your-api-key", - niquests_options={"pool_connections": 20, "pool_maxsize": 25}, + httpx_options={ + "limits": Limits(max_connections=25, max_keepalive_connections=20), + }, ) +Serializing URL search params +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The Seam API parses URL search params as complex types. +If you call it with your own HTTP client, +``serialize_url_search_params`` is exported for that purpose: + +.. code-block:: python + + import httpx + from seam import serialize_url_search_params + + httpx.get( + "https://connect.getseam.com/devices/list", + params=serialize_url_search_params({"device_ids": ["device1", "device2"]}), + headers={"Authorization": "Bearer your-api-key"}, + ) + +The serialization defines the name and value of each search param, +where every value is a string. +``UrlSearchParams`` holds those pairs and renders the query string, +as `URLSearchParams`_ does for the `reference implementation`_: + +.. code-block:: python + + from seam import UrlSearchParams, update_url_search_params + + search_params = UrlSearchParams() + + update_url_search_params(search_params, {"device_ids": ["device1", "device2"]}) + + list(search_params) + # => [('device_ids', 'device1'), ('device_ids', 'device2')] + + str(search_params) + # => 'device_ids=device1&device_ids=device2' + +Pass either the query string or the pairs to your HTTP client. +A client may percent-encode a few characters differently than +``URLSearchParams`` does, e.g. httpx escapes ``*`` and unescapes ``~``, +which the Seam API reads as the same params either way. + +A param set to ``None`` is omitted, while a param set to ``NULL`` +is serialized to an empty value, which the Seam API reads as null, +as described in `Setting a Param to Null`_. +A param that cannot be represented raises a ``seam.UnserializableParamError``. + +The Seam API parses these params with the corresponding `parser`_. + +.. _URLSearchParams: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams +.. _reference implementation: https://github.com/seamapi/url-search-params-serializer +.. _parser: https://github.com/seamapi/url-search-params-parser + Development and Testing ----------------------- @@ -496,7 +635,7 @@ Quickstart $ git clone https://github.com/seamapi/python.git $ cd python - $ poetry install + $ uv sync Run each command below in a separate terminal window: @@ -521,19 +660,19 @@ Clone the project with Requirements ~~~~~~~~~~~~ -You will need `Python 3`_ and Poetry_ and Node.js_ with npm_ and just_. +You will need `Python 3`_ and uv_ and Node.js_ with npm_ and just_. Install the development dependencies with :: - $ poetry install + $ uv sync $ npm install .. _just: https://just.systems/ .. _Node.js: https://nodejs.org/ .. _npm: https://www.npmjs.com/ -.. _Poetry: https://poetry.eustace.io/ +.. _uv: https://docs.astral.sh/uv/ .. _Python 3: https://www.python.org/ Tests @@ -561,7 +700,7 @@ Run tests on changes with Publishing ~~~~~~~~~~ -New versions are created with `poetry version`_. +New versions are created with `uv version`_. Automatic ^^^^^^^^^ @@ -576,7 +715,7 @@ Manual ^^^^^^ Publish a new version by triggering a `version workflow_dispatch on GitHub Actions`_. -The ``version`` input will be passed as the first argument to `poetry version`_. +The ``version`` input will be passed as the first argument to `uv version`_. This may be done on the web or using the `GitHub CLI`_ with @@ -584,7 +723,7 @@ This may be done on the web or using the `GitHub CLI`_ with $ gh workflow run version.yml --raw-field version= -.. _Poetry version: https://python-poetry.org/docs/cli/#version +.. _uv version: https://docs.astral.sh/uv/reference/cli/#uv-version .. _GitHub CLI: https://cli.github.com/ .. _version workflow_dispatch on GitHub Actions: https://github.com/seamapi/python/actions?query=workflow%3Aversion diff --git a/codegen/layouts/partials/method-docstring.hbs b/codegen/layouts/partials/method-docstring.hbs index 1484936d..04c1f7eb 100644 --- a/codegen/layouts/partials/method-docstring.hbs +++ b/codegen/layouts/partials/method-docstring.hbs @@ -4,7 +4,9 @@ :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish.{{/if}}{{#unless (eq returnType "None")}} - :returns: {{{indent (pythonDoc responseDescription) 8}}}{{/unless}}{{#if isDeprecated}} + :returns: {{{indent (pythonDoc responseDescription) 8}}}{{/unless}}{{#if hasRequiredParameters}} + + :raises ValueError: At least one parameter must be provided.{{/if}}{{#if isDeprecated}} .. deprecated:: {{#if (pythonDoc deprecationMessage)}}{{{indent (pythonDoc deprecationMessage) 8}}}{{else}}This method is deprecated.{{/if}}{{/if}} \ No newline at end of file diff --git a/codegen/layouts/partials/method-signature.hbs b/codegen/layouts/partials/method-signature.hbs index 10977db0..4e58dbd1 100644 --- a/codegen/layouts/partials/method-signature.hbs +++ b/codegen/layouts/partials/method-signature.hbs @@ -1 +1 @@ -{{name}}(self{{#if params}}, *{{else}}{{#if (eq returnType "ActionAttempt")}}, *{{/if}}{{/if}}{{#each params}}, {{name}}: {{#if required}}{{type}}{{else}}Optional[{{type}}] = None{{/if}}{{/each}}{{#if (eq returnType "ActionAttempt")}}, wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None{{/if}}) -> {{returnType}} \ No newline at end of file +{{name}}(self{{#if params}}, *{{else}}{{#if (eq returnType "ActionAttempt")}}, *{{/if}}{{/if}}{{#each params}}, {{name}}: {{#if required}}{{nullableType type isNullable}}{{else}}Optional[{{nullableType type isNullable}}] = None{{/if}}{{/each}}{{#if (eq returnType "ActionAttempt")}}, wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None{{/if}}) -> {{returnType}} \ No newline at end of file diff --git a/codegen/layouts/partials/resource-dataclass.hbs b/codegen/layouts/partials/resource-dataclass.hbs index 8a923753..7375389b 100644 --- a/codegen/layouts/partials/resource-dataclass.hbs +++ b/codegen/layouts/partials/resource-dataclass.hbs @@ -16,9 +16,8 @@ {{/each}} {{memberIndent}}@classmethod -{{memberIndent}}def from_dict(cls, d: Dict[str, Any]): +{{memberIndent}}def from_dict(cls, d: Any): {{#unless properties}} -{{memberIndent}} # This shape documents no properties, so there is nothing to read. {{memberIndent}} # pylint: disable=unused-argument {{/unless}} {{memberIndent}} return cls( diff --git a/codegen/layouts/partials/route-method.hbs b/codegen/layouts/partials/route-method.hbs index c009161a..b615e7f8 100644 --- a/codegen/layouts/partials/route-method.hbs +++ b/codegen/layouts/partials/route-method.hbs @@ -1,13 +1,19 @@ + @route_metadata(path="{{path}}", has_required_parameters={{#if hasRequiredParameters}}True{{else}}False{{/if}}, has_pagination={{#if hasPagination}}True{{else}}False{{/if}}) def {{> method-signature}}: """{{> method-docstring}}""" - json_payload = {} + {{payloadVar}}: Dict[str, Any] = {} {{#each params}} if {{name}} is not None: - json_payload["{{name}}"] = {{name}} + {{../payloadVar}}["{{name}}"] = {{name}} {{/each}} +{{#if hasRequiredParameters}} - {{#unless (eq returnType "None")}}res = {{/unless}}self.client.post("{{path}}", json=json_payload) + if not {{payloadVar}}: + raise ValueError("At least one parameter is required for {{path}}") +{{/if}} + + {{#unless (eq returnType "None")}}res = {{/unless}}self.client.{{httpVerb}}("{{path}}", {{payloadArg}}={{payloadVar}}) {{#if (eq returnType "ActionAttempt")}} wait_for_action_attempt = ( diff --git a/codegen/layouts/route.hbs b/codegen/layouts/route.hbs index 1793adfb..32540f82 100644 --- a/codegen/layouts/route.hbs +++ b/codegen/layouts/route.hbs @@ -1,6 +1,10 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +{{#if importNull}} +from ..null import Null +{{/if}} {{#if resourceClasses}} from ..resources import ({{#each resourceClasses}}{{this}}{{#unless @last}},{{/unless}}{{/each}}) {{/if}} diff --git a/codegen/lib/class-model.ts b/codegen/lib/class-model.ts index 9937221a..d9333b65 100644 --- a/codegen/lib/class-model.ts +++ b/codegen/lib/class-model.ts @@ -4,6 +4,7 @@ export interface ClassMethodParameter { name: string type: string + isNullable: boolean description: string isDeprecated: boolean deprecationMessage: string @@ -14,6 +15,9 @@ export interface ClassMethodParameter { export interface ClassMethod { methodName: string path: string + preferredMethod: string + hasRequiredParameters: boolean + hasPagination: boolean description: string responseDescription: string isDeprecated: boolean diff --git a/codegen/lib/handlebars-helpers.ts b/codegen/lib/handlebars-helpers.ts index dd4abeda..4b8a4227 100644 --- a/codegen/lib/handlebars-helpers.ts +++ b/codegen/lib/handlebars-helpers.ts @@ -53,6 +53,12 @@ export const indent = (value: string, spaces: number): string => export const pythonIdentifier = (name: string): string => PYTHON_KEYWORDS.has(name) ? `${name}_` : name +// A param the API documents as nullable may be set to the NULL sentinel, which +// the client serializes to null. Params that are merely optional may not: they +// are omitted by passing None, and sending null would unset a value instead. +export const nullableType = (type: string, isNullable: boolean): string => + isNullable ? `Union[${type}, Null]` : type + export const isListType = (type: string): boolean => type.startsWith('List[') export const listItemType = (type: string): string => type.slice(5, -1) diff --git a/codegen/lib/layouts/resources.ts b/codegen/lib/layouts/resources.ts index 0463f8de..fc4d6397 100644 --- a/codegen/lib/layouts/resources.ts +++ b/codegen/lib/layouts/resources.ts @@ -6,7 +6,10 @@ import type { Blueprint, Property } from '@seamapi/blueprint' import { pascalCase, snakeCase } from 'change-case' import { convertCustomResourceName } from '../custom-resource-name-conversions.js' -import { mapPropertyToPythonType } from '../python-type.js' +import { + mapPropertyToPythonType, + mapRequiredPropertyToPythonType, +} from '../python-type.js' export interface ResourceLayoutContext extends ResourceClassLayoutContext { moduleName: string @@ -144,6 +147,9 @@ const mergeOccurrences = (occurrences: Property[], path: string): Property => { return { ...first, ...docs } } +const withOptionality = (property: Property, isOptional: boolean): Property => + isOptional ? { ...property, isOptional: true } : property + const mergePropertyLists = ( propertyLists: Property[][], path = '', @@ -161,7 +167,13 @@ const mergePropertyLists = ( } return [...occurrences.entries()].map(([name, group]) => - mergeOccurrences(group, path === '' ? name : `${path}.${name}`), + // A property only some variants carry is absent whenever the merged + // dataclass holds one of the variants that omits it, so it is optional on + // the merged shape no matter how each variant declares it. + withOptionality( + mergeOccurrences(group, path === '' ? name : `${path}.${name}`), + group.length < propertyLists.length, + ), ) } @@ -249,7 +261,17 @@ const buildClass = ( ) } - const type = mapPropertyToPythonType(property, nestedClassName) + const isObject = nestedClassName != null && property.format === 'object' + // A nested object is read as None whenever the payload omits it, and the + // schema is not a reliable guide to when that happens: an action attempt + // documents both error and result as required, yet a pending one carries + // neither. Constructing them unconditionally would fail on those payloads, + // so from_dict keeps its None fallback and the field stays Optional. + const type = mapPropertyToPythonType(property, nestedClassName, isObject) + const requiredType = mapRequiredPropertyToPythonType( + property, + nestedClassName, + ) return { name: property.name, description: property.description, @@ -259,8 +281,8 @@ const buildClass = ( // Nested classes are attributes of the class that owns them, so // from_dict reaches them through cls rather than a qualified path. nestedClassName: nestedClassName ?? '', - isDictParam: type.startsWith('Dict'), - isObject: nestedClassName != null && property.format === 'object', + isDictParam: requiredType.startsWith('Dict'), + isObject, isObjectList: nestedClassName != null && property.format === 'list', } }) diff --git a/codegen/lib/layouts/route.ts b/codegen/lib/layouts/route.ts index 8e57326c..00c57863 100644 --- a/codegen/lib/layouts/route.ts +++ b/codegen/lib/layouts/route.ts @@ -11,6 +11,11 @@ import { export interface MethodLayoutContext { name: string path: string + httpVerb: string + payloadVar: string + payloadArg: string + hasRequiredParameters: boolean + hasPagination: boolean description: string responseDescription: string isDeprecated: boolean @@ -18,6 +23,7 @@ export interface MethodLayoutContext { params: Array<{ name: string type: string + isNullable: boolean description: string isDeprecated: boolean deprecationMessage: string @@ -48,14 +54,30 @@ export interface RouteLayoutContext { module: string }> importResolveActionAttempt: boolean + importNull: boolean methods: MethodLayoutContext[] } +const getRequestLayoutContext = ( + preferredMethod: string, +): Pick => { + const httpVerb = preferredMethod.toLowerCase() + + if (preferredMethod === 'GET' || preferredMethod === 'DELETE') { + return { httpVerb, payloadVar: 'params', payloadArg: 'params' } + } + + return { httpVerb, payloadVar: 'json_payload', payloadArg: 'json' } +} + export const getMethodLayoutContext = ( method: ClassMethod, ): MethodLayoutContext => ({ name: method.methodName, path: method.path, + ...getRequestLayoutContext(method.preferredMethod), + hasRequiredParameters: method.hasRequiredParameters, + hasPagination: method.hasPagination, description: method.description, responseDescription: method.responseDescription, isDeprecated: method.isDeprecated, @@ -63,6 +85,7 @@ export const getMethodLayoutContext = ( params: sortClassMethodParameters(method.parameters).map((parameter) => ({ name: parameter.name, type: parameter.type, + isNullable: parameter.isNullable, description: parameter.description, isDeprecated: parameter.isDeprecated, deprecationMessage: parameter.deprecationMessage, @@ -88,6 +111,10 @@ export const setRouteLayoutContext = (cls: ClassModel): RouteLayoutContext => { const abstractClassName = `Abstract${cls.name}` const methods = cls.methods.map(getMethodLayoutContext) + const importNull = methods.some(({ params }) => + params.some(({ isNullable }) => isNullable), + ) + return { className: cls.name, abstractClassName, @@ -111,6 +138,7 @@ export const setRouteLayoutContext = (cls: ClassModel): RouteLayoutContext => { module: `${cls.namespace}_${identifier.namespace}`, })), importResolveActionAttempt, + importNull, methods, } } diff --git a/codegen/lib/python-type.ts b/codegen/lib/python-type.ts index feed09b5..fadcd5d7 100644 --- a/codegen/lib/python-type.ts +++ b/codegen/lib/python-type.ts @@ -21,9 +21,25 @@ export const mapParameterToPythonType = (parameter: Parameter): string => { return mapScalarFormatToPythonType(parameter.format) } +// from_dict reads every property with dict.get, so a property the API may omit +// or send as null arrives as None. Declaring those fields Optional keeps the +// dataclass honest about what a caller can actually find on it. export const mapPropertyToPythonType = ( property: Property, nestedClassName?: string, + isOptional = false, +): string => { + const type = mapRequiredPropertyToPythonType(property, nestedClassName) + return isOptional || property.isOptional || property.isNullable + ? `Optional[${type}]` + : type +} + +// The type a property has before optionality is taken into account. Callers +// that match on the shape of the type, rather than render it, want this one. +export const mapRequiredPropertyToPythonType = ( + property: Property, + nestedClassName?: string, ): string => { if (property.format === 'list') { return `List[${ diff --git a/codegen/lib/routes.ts b/codegen/lib/routes.ts index 0cd065d2..cbc0a98a 100644 --- a/codegen/lib/routes.ts +++ b/codegen/lib/routes.ts @@ -89,6 +89,9 @@ export const routes = ( cls.methods.push({ methodName: endpoint.name, path: endpoint.path, + preferredMethod: endpoint.request.preferredMethod, + hasRequiredParameters: endpoint.request.hasRequiredParameters, + hasPagination: endpoint.hasPagination, description: endpoint.description, responseDescription: endpoint.response.description, isDeprecated: endpoint.isDeprecated, @@ -96,6 +99,7 @@ export const routes = ( parameters: endpoint.request.parameters.map((parameter) => ({ name: parameter.name, type: mapParameterToPythonType(parameter), + isNullable: parameter.isNullable, description: parameter.description, isDeprecated: parameter.isDeprecated, deprecationMessage: parameter.deprecationMessage, diff --git a/justfile b/justfile index d55b9a0b..ed397262 100644 --- a/justfile +++ b/justfile @@ -2,25 +2,25 @@ default: build @build: rm -rf dist - poetry build + uv build @format: - poetry run black . + uv run black . @lint: - poetry run pylint ./seam ./test - poetry run black --check . - poetry run rstcheck README.rst - poetry run mypy seam/resources --disable-error-code=arg-type --disable-error-code=import-not-found + uv run pylint ./seam ./test + uv run black --check . + uv run rstcheck README.rst + uv run mypy seam test @test: - poetry run pytest --cov=./seam + uv run pytest --cov=./seam @watch: - poetry run ptw + uv run ptw @version: - git add pyproject.toml - git commit -m "$(poetry version -s)" - git tag --sign "v$(poetry version -s)" -m "$(poetry version -s)" + git add pyproject.toml uv.lock + git commit -m "$(uv version --short)" + git tag --sign "v$(uv version --short)" -m "$(uv version --short)" git push --follow-tags diff --git a/package-lock.json b/package-lock.json index 10183c3d..3e6a6f45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,10 +6,10 @@ "": { "name": "@seamapi/python", "devDependencies": { - "@seamapi/blueprint": "^1.1.0", + "@seamapi/blueprint": "^1.5.1", "@seamapi/fake-seam-connect": "1.86.0", "@seamapi/smith": "^1.1.0", - "@seamapi/types": "1.983.0", + "@seamapi/types": "1.1001.0", "change-case": "^5.4.4", "prettier": "^3.2.5" }, @@ -19,9 +19,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ "ppc64" ], @@ -37,9 +37,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], @@ -55,9 +55,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], @@ -73,9 +73,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], @@ -91,9 +91,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], @@ -109,9 +109,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], @@ -127,9 +127,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], @@ -145,9 +145,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ "x64" ], @@ -163,9 +163,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ "arm" ], @@ -181,9 +181,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], @@ -199,9 +199,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ "ia32" ], @@ -217,9 +217,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ "loong64" ], @@ -235,9 +235,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ "mips64el" ], @@ -253,9 +253,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ "ppc64" ], @@ -271,9 +271,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ "riscv64" ], @@ -289,9 +289,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], @@ -307,9 +307,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", "cpu": [ "x64" ], @@ -325,9 +325,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], @@ -343,9 +343,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], @@ -361,9 +361,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", "cpu": [ "arm64" ], @@ -379,9 +379,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], @@ -397,9 +397,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], @@ -415,9 +415,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], @@ -433,9 +433,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], @@ -451,9 +451,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], @@ -469,9 +469,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], @@ -787,9 +787,9 @@ "license": "MIT" }, "node_modules/@seamapi/blueprint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@seamapi/blueprint/-/blueprint-1.1.0.tgz", - "integrity": "sha512-wX1HZkA/IK9hDQ6Qdxw5Mo+Ysfh82p9IEXQJafakO9VMbszW6n1U02eEhZHVY3CfzN/duk6t9h1veX0zRlhWBQ==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@seamapi/blueprint/-/blueprint-1.5.1.tgz", + "integrity": "sha512-rXHMNDGmaE7OK+tg3e64DnpMHQypN4BuSS+PkVqGUoYc9f+xBMONqPscsHAGi9iU/iyZOJZKEOKOXvMmWaLoLA==", "dev": true, "license": "MIT", "dependencies": { @@ -797,8 +797,8 @@ "zod": "^3.23.8" }, "engines": { - "node": ">=22.11.0", - "npm": ">=10.9.4" + "node": ">=22.12.0", + "npm": ">=10.0.0" } }, "node_modules/@seamapi/fake-devicedb": { @@ -871,14 +871,14 @@ } }, "node_modules/@seamapi/types": { - "version": "1.983.0", - "resolved": "https://registry.npmjs.org/@seamapi/types/-/types-1.983.0.tgz", - "integrity": "sha512-SMkfn1SVC70x67mtRAvLMJtpFh/0zaStLatb6LA+kz9n/rV1gBS/UlH8SzBFg7iStE22f/VPjkdltHTIY1paoA==", + "version": "1.1001.0", + "resolved": "https://registry.npmjs.org/@seamapi/types/-/types-1.1001.0.tgz", + "integrity": "sha512-pwIEqMYCdOLlIHUzzLlMq/4K6QwAM3kWXooSxNWOlPyCeWk21SvrbTN/joXwZtky504g3unLPKMbFad1mlQyfQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=22.11.0", - "npm": ">=10.9.4" + "node": ">=22.12.0", + "npm": ">=10.0.0" }, "peerDependencies": { "zod": "^3.24.0" @@ -965,17 +965,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", - "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz", + "integrity": "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.65.0", - "@typescript-eslint/type-utils": "8.65.0", - "@typescript-eslint/utils": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/type-utils": "8.67.0", + "@typescript-eslint/utils": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -988,7 +988,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.65.0", + "@typescript-eslint/parser": "^8.67.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -1004,16 +1004,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", - "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.67.0.tgz", + "integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3" }, "engines": { @@ -1029,14 +1029,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", - "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz", + "integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.65.0", - "@typescript-eslint/types": "^8.65.0", + "@typescript-eslint/tsconfig-utils": "^8.67.0", + "@typescript-eslint/types": "^8.67.0", "debug": "^4.4.3" }, "engines": { @@ -1051,14 +1051,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", - "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz", + "integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0" + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1069,9 +1069,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", - "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz", + "integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==", "dev": true, "license": "MIT", "engines": { @@ -1086,15 +1086,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", - "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz", + "integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0", - "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -1111,9 +1111,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", - "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz", + "integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==", "dev": true, "license": "MIT", "engines": { @@ -1125,16 +1125,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", - "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz", + "integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.65.0", - "@typescript-eslint/tsconfig-utils": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/visitor-keys": "8.65.0", + "@typescript-eslint/project-service": "8.67.0", + "@typescript-eslint/tsconfig-utils": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1163,9 +1163,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { @@ -1205,16 +1205,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", - "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz", + "integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.65.0", - "@typescript-eslint/types": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0" + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1229,13 +1229,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", - "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz", + "integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/types": "8.67.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1527,9 +1527,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -1886,9 +1886,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.24.4", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.4.tgz", - "integrity": "sha512-GVoi+ICHocoOIU7qVVM48wOJziRsqrsyqlI0Ce0LdowRn6v3bcH2zUa9kp85ncx0nwIb9/HOCOLS3fdThDG/XQ==", + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", "dev": true, "license": "MIT", "dependencies": { @@ -2099,9 +2099,9 @@ } }, "node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2113,32 +2113,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/escape-string-regexp": { @@ -2747,9 +2747,9 @@ } }, "node_modules/flatted": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz", - "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC", "peer": true @@ -2898,9 +2898,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", - "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.2.tgz", + "integrity": "sha512-XpwZALwwl/BaKTAyC6+c5T8y6kCg2jk+XGqOVrKIQmW49pNypYLMRjCUXqa28tQgJlhS2RlzP7sc+Rx7W6qsfw==", "dev": true, "license": "MIT", "dependencies": { @@ -2953,9 +2953,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { @@ -3101,9 +3101,9 @@ } }, "node_modules/gray-matter/node_modules/js-yaml": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", - "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", "dev": true, "license": "MIT", "peer": true, @@ -3817,9 +3817,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { @@ -4190,9 +4190,9 @@ } }, "node_modules/neostandard/node_modules/globals": { - "version": "17.8.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.8.0.tgz", - "integrity": "sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ==", + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz", + "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==", "dev": true, "license": "MIT", "engines": { @@ -5453,9 +5453,9 @@ } }, "node_modules/tsx": { - "version": "4.23.1", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", - "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", "dev": true, "license": "MIT", "peer": true, @@ -5580,16 +5580,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.65.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", - "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.67.0.tgz", + "integrity": "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.65.0", - "@typescript-eslint/parser": "8.65.0", - "@typescript-eslint/typescript-estree": "8.65.0", - "@typescript-eslint/utils": "8.65.0" + "@typescript-eslint/eslint-plugin": "8.67.0", + "@typescript-eslint/parser": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index 1a816343..83d09c74 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,10 @@ } }, "devDependencies": { - "@seamapi/blueprint": "^1.1.0", + "@seamapi/blueprint": "^1.5.1", "@seamapi/fake-seam-connect": "1.86.0", "@seamapi/smith": "^1.1.0", - "@seamapi/types": "1.983.0", + "@seamapi/types": "1.1001.0", "change-case": "^5.4.4", "prettier": "^3.2.5" } diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index f7cbcb72..00000000 --- a/poetry.lock +++ /dev/null @@ -1,1642 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. - -[[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 = "anyio" -version = "4.3.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.8" -files = [ - {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, - {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} - -[package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.23)"] - -[[package]] -name = "astroid" -version = "3.2.2" -description = "An abstract syntax tree for Python with inference support." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "astroid-3.2.2-py3-none-any.whl", hash = "sha256:e8a0083b4bb28fcffb6207a3bfc9e5d0a68be951dd7e336d5dcf639c682388c0"}, - {file = "astroid-3.2.2.tar.gz", hash = "sha256:8ead48e31b92b2e217b6c9733a21afafe479d52d6e164dd25fb1a770c7c3cf94"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} - -[[package]] -name = "attrs" -version = "23.2.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.7" -files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] - -[[package]] -name = "black" -version = "24.4.2" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.8" -files = [ - {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, - {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, - {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, - {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, - {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, - {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, - {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, - {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, - {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, - {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, - {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, - {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, - {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, - {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, - {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, - {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, - {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, - {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, - {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, - {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, - {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, - {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "certifi" -version = "2024.2.2" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.3.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, -] - -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[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.5.1" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "coverage-7.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e"}, - {file = "coverage-7.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f"}, - {file = "coverage-7.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a"}, - {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35"}, - {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e"}, - {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223"}, - {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e"}, - {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146"}, - {file = "coverage-7.5.1-cp310-cp310-win32.whl", hash = "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228"}, - {file = "coverage-7.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8"}, - {file = "coverage-7.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428"}, - {file = "coverage-7.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746"}, - {file = "coverage-7.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3"}, - {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2"}, - {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca"}, - {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8"}, - {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057"}, - {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987"}, - {file = "coverage-7.5.1-cp311-cp311-win32.whl", hash = "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136"}, - {file = "coverage-7.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd"}, - {file = "coverage-7.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206"}, - {file = "coverage-7.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34"}, - {file = "coverage-7.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d"}, - {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa"}, - {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e"}, - {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572"}, - {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07"}, - {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7"}, - {file = "coverage-7.5.1-cp312-cp312-win32.whl", hash = "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19"}, - {file = "coverage-7.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596"}, - {file = "coverage-7.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7"}, - {file = "coverage-7.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90"}, - {file = "coverage-7.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e"}, - {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5"}, - {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661"}, - {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8"}, - {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4"}, - {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d"}, - {file = "coverage-7.5.1-cp38-cp38-win32.whl", hash = "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41"}, - {file = "coverage-7.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de"}, - {file = "coverage-7.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1"}, - {file = "coverage-7.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece"}, - {file = "coverage-7.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26"}, - {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5"}, - {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601"}, - {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be"}, - {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f"}, - {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668"}, - {file = "coverage-7.5.1-cp39-cp39-win32.whl", hash = "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981"}, - {file = "coverage-7.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f"}, - {file = "coverage-7.5.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312"}, - {file = "coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c"}, -] - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "dataclasses-json" -version = "0.6.6" -description = "Easily serialize dataclasses to and from JSON." -optional = false -python-versions = "<4.0,>=3.7" -files = [ - {file = "dataclasses_json-0.6.6-py3-none-any.whl", hash = "sha256:e54c5c87497741ad454070ba0ed411523d46beb5da102e221efb873801b0ba85"}, - {file = "dataclasses_json-0.6.6.tar.gz", hash = "sha256:0c09827d26fffda27f1be2fed7a7a01a29c5ddcd2eb6393ad5ebf9d77e9deae8"}, -] - -[package.dependencies] -marshmallow = ">=3.18.0,<4.0.0" -typing-inspect = ">=0.4.0,<1" - -[[package]] -name = "deprecated" -version = "1.2.14" -description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, - {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, -] - -[package.dependencies] -wrapt = ">=1.10,<2" - -[package.extras] -dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] - -[[package]] -name = "dill" -version = "0.3.8" -description = "serialize all of Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, - {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, -] - -[package.extras] -graph = ["objgraph (>=1.7.2)"] -profile = ["gprof2dot (>=2022.7.29)"] - -[[package]] -name = "docopt" -version = "0.6.2" -description = "Pythonic argument parser, that will make you smile" -optional = false -python-versions = "*" -files = [ - {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, -] - -[[package]] -name = "docutils" -version = "0.21.2" -description = "Docutils -- Python Documentation Utilities" -optional = false -python-versions = ">=3.9" -files = [ - {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, - {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.2.1" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -files = [ - {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, - {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.7" -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] - -[[package]] -name = "httpcore" -version = "1.0.5" -description = "A minimal low-level HTTP client." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, - {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, -] - -[package.dependencies] -certifi = "*" -h11 = ">=0.13,<0.15" - -[package.extras] -asyncio = ["anyio (>=4.0,<5.0)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<0.26.0)"] - -[[package]] -name = "httpx" -version = "0.27.0" -description = "The next generation HTTP client." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, - {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, -] - -[package.dependencies] -anyio = "*" -certifi = "*" -httpcore = "==1.*" -idna = "*" -sniffio = "*" - -[package.extras] -brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] - -[[package]] -name = "idna" -version = "3.7" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.5" -files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, -] - -[[package]] -name = "iniconfig" -version = "2.0.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.7" -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "isort" -version = "5.13.2" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, - {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, -] - -[package.extras] -colors = ["colorama (>=0.4.6)"] - -[[package]] -name = "jh2" -version = "5.0.3" -description = "HTTP/2 State-Machine based protocol implementation" -optional = false -python-versions = ">=3.7" -files = [ - {file = "jh2-5.0.3-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:764acdd572413198eb7a1299d08d32b0819c33220604f76ba7ea722443c3b929"}, - {file = "jh2-5.0.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f83bdbbae9fac7766e2edcac9a275af5a70e8e7188296c84cbeb552e1f1f2e8d"}, - {file = "jh2-5.0.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a921112bbafaea4d5ef9e2a25f03cacdaa1795b6a961f0fe430b8de15b939b3a"}, - {file = "jh2-5.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18cdf52edf8e636f3a4a74d92eb62bc6692a2c78e288b0724341c82b078bb261"}, - {file = "jh2-5.0.3-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ccb5fc490722b5ca0966a2402ae90a7bc70ec8a4a9bce224948db211f5fa2a9"}, - {file = "jh2-5.0.3-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7c7dcecef260a792da2b7653011d09bcad5e1455e38fd194ee07fcb01a803fc3"}, - {file = "jh2-5.0.3-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aa0af8f8a1a27dc7e166840fbdda46abc673d5cd8e2319ac08a3c7d5e9e9920"}, - {file = "jh2-5.0.3-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78228b1a255581e93144d52feda6e8605fcfbfae7aa289def8879a7be6ca8a74"}, - {file = "jh2-5.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2be4f69c7c8d87d28948065a038d84555a0784acf3886e9c18707f34ceb7c1b"}, - {file = "jh2-5.0.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f83206f4e18d4836ec9de573c9c0c27c02e277b59685d9d490f7187968b780fa"}, - {file = "jh2-5.0.3-cp37-abi3-musllinux_1_1_armv7l.whl", hash = "sha256:be4e8c8ccb401cbd6386a406c21a87f690d68f1fdcc1698dcc813429bf5a9ce3"}, - {file = "jh2-5.0.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b6facef13abe549d70fd722ab668fae822cfdcade6199a12e7ec06fe0ba44326"}, - {file = "jh2-5.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3d2e48899378c3026d3640f1be1a043038af74f4076f1fb0c1b7bc13fb9c0619"}, - {file = "jh2-5.0.3-cp37-abi3-win_arm64.whl", hash = "sha256:2faaf1792220ffd5dcae8e88dd8f3b2b72771589121dbabc92fb503f488021c2"}, - {file = "jh2-5.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:72db414df9a49a3498b7e083002cd9a3eefc4bce33789d7ae31d0e1c92229f0e"}, - {file = "jh2-5.0.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6fa5629d6e18c6be93cc17da6b31a21511405e443df0a6936a0795807bf949f2"}, - {file = "jh2-5.0.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a5d5326a15eed6b4dd64598522b741b65168093705e4276f964627ada3281f48"}, - {file = "jh2-5.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af39530c7173dd41383ac9910164695768418bd5910bc1e8e628383d82656881"}, - {file = "jh2-5.0.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c091dd40414b028793d3867968842a55c09e26bcbbba76e6daf8080b47384fd"}, - {file = "jh2-5.0.3-pp310-pypy310_pp73-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9e4b981ddce810d7a691f535c150212bef8a70c61081007c13b19ab30e44409b"}, - {file = "jh2-5.0.3-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57f8d46fac15bbe1fe6c576b00ef2aaf616c359cc2fb8a468d46b05e19495bf5"}, - {file = "jh2-5.0.3-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7b3476e3f13ce6751a45a82861a5fd64b38ef166c40974d0c97a0762293e12"}, - {file = "jh2-5.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498a7e463f2be67ec1085833e99582bcb1e3ea1c2933f640f7c4896e307673eb"}, - {file = "jh2-5.0.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:37e9d6f59e2c305f33793ea24fb588300828a83f9f51d47671e8335fc49a59e0"}, - {file = "jh2-5.0.3-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:09b89d2700e9530f13b10dd1b8cb0e5389e3da833b6aa7d0bef74ef39229c7ba"}, - {file = "jh2-5.0.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:a68cf6e7baf185f831b3eccdbfe91b7d48ad6af78b4b929d6df89d92d9a4ca62"}, - {file = "jh2-5.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7669f95cfac416a3e97b224d214c9838bb5b6b9e35a1892637337fe774d0406b"}, - {file = "jh2-5.0.3-pp37-pypy37_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:af5560b7856c6a1e17071d86e16e288f6ca3f465a29fd683347f84cf4ad8a99b"}, - {file = "jh2-5.0.3-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:769f569fd7dfbe027ddf28c5c73dda48494bb3ffef0be6f60d5661fa6c754da9"}, - {file = "jh2-5.0.3-pp37-pypy37_pp73-macosx_11_0_arm64.whl", hash = "sha256:48befaf5dd60aa3b623e8cfafdb097516820f9b5c2ae38d399b6e4eed1cfdb46"}, - {file = "jh2-5.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac6259ba5591120b3663089ac1ba9a521249794d753343c59447808b744e2755"}, - {file = "jh2-5.0.3-pp37-pypy37_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dc1f2442d6eb9ab8f54d328ec938918ca273290d1e99329387967dd74e1b054e"}, - {file = "jh2-5.0.3-pp37-pypy37_pp73-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:557c40acd62c51001cdad2674c5a9d400127796ce7fbe8d82698ff8bec478092"}, - {file = "jh2-5.0.3-pp37-pypy37_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e175649572bc2a7c60a7e03d8bd1d42d0f64634d31d0d7930b65e1b31f8e43d"}, - {file = "jh2-5.0.3-pp37-pypy37_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0125bbc6d41bd55f088e952fbfec0652eb0ac45632604d6644c45e2ccb83507b"}, - {file = "jh2-5.0.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ecdf049246a0bc01bd404737b1506770577e982802872539f7734368877623a"}, - {file = "jh2-5.0.3-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:70a401591577502f3efb2fb247378fa5a0adfd48578e15f7365d6db656447e66"}, - {file = "jh2-5.0.3-pp37-pypy37_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3ba6a00394c6cb5d1409fbb910eba2ff2a216bb553af198ccc8f64af63133f67"}, - {file = "jh2-5.0.3-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:4eb16b0f95181c7bb19115446b5987b694727c6afd618ee900e895a54d101188"}, - {file = "jh2-5.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:e967d4a1bdb7a8726e0349dea82eba530dbed8fed4cad118f81e867f84c0446e"}, - {file = "jh2-5.0.3-pp38-pypy38_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2fcc906639e5987ee0eacb12ba5a7678ac76677986dbeeb41dc7d1b44926ac43"}, - {file = "jh2-5.0.3-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:39dbcd14e8313370c83287080ddac77a42cace50ef223852d6b0bea73df8ac3e"}, - {file = "jh2-5.0.3-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0ceddd1be2a9153d5eec25ac3aca846f35bc171325e549509cac75ecd26930d"}, - {file = "jh2-5.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8aa14f5b3077526fed01464e4400860c9f88f73b0499f87e50e4cbf0d851def"}, - {file = "jh2-5.0.3-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:89cec07a5e15660c10c29537d6df2b61daf9673cbd014971664b50d3c5eedc34"}, - {file = "jh2-5.0.3-pp38-pypy38_pp73-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fc81fe2ef8a2c011a67468a169790973d0595731599c7c21d0d223b29c886d43"}, - {file = "jh2-5.0.3-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:024862bbaa795549823386bc30b0bb7c1807df5df75b4e5ce97f216812796fca"}, - {file = "jh2-5.0.3-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c7417bd01c897c5afe9da8270ca4fc0485378a05bee0d4c0ad435c48aee841ad"}, - {file = "jh2-5.0.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f67738cb9e669c3b28ab1f7c611df0c3e5cb6da06df5a7f303e8510b890764a"}, - {file = "jh2-5.0.3-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fa6d7060c225d2f1c888ecb8e834a4685d4468fe11968d56a5eb3376e45d287c"}, - {file = "jh2-5.0.3-pp38-pypy38_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:32f0700ecca8707b9561328705efa9ad1626ca829b2c4964e753d579337d53c2"}, - {file = "jh2-5.0.3-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:969f62d321bd82207be836f99bbbb854ec5596ca82a2c905c404fed5e4a9289c"}, - {file = "jh2-5.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:1d1194a66b25fea05f34e0bd1cf5ffc72ec67fd3e584d4c1293e9962815ec180"}, - {file = "jh2-5.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b1b47bba88666287ad998dd561292dbe38ad767072a4ecdfe90a98f2f2e95579"}, - {file = "jh2-5.0.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:97a57f39f6fbfb96966edf0d6879afed5784582573644cd5c2f58907f6e45759"}, - {file = "jh2-5.0.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0ab31da34be3c726acda1eed2fd9ad9daf14f4f45bb15fdce7ed877ce82f677"}, - {file = "jh2-5.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:090755e41a0c0b7021dfdfda46970e3920d6524a528d0456a0759310b2cd3a30"}, - {file = "jh2-5.0.3-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e8903186182de97107260540796f6579b7322cb82d7d3e8aca11fd09c2847fe"}, - {file = "jh2-5.0.3-pp39-pypy39_pp73-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:85714f220fbd7ab7d17ab83cbbdac4d43e0fc699738da06b018a9aa01adad5f0"}, - {file = "jh2-5.0.3-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d59abb4b1adfd9e7f0b6df22f4f6ea260128d7605110a39bab29dbed32a5ab28"}, - {file = "jh2-5.0.3-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe75dbd44d1c53341acd5505afd29af8172e7ce67a7d21fd4466d7db26be3766"}, - {file = "jh2-5.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c58aafe13f208769f16fa612095a64aed27b35a241eaffcef10c105b5c48c03"}, - {file = "jh2-5.0.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:83fa9a815edbe52b6c0c096a20c1bb3f9669b706e8a69775dae63c973a60b2ea"}, - {file = "jh2-5.0.3-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:b4d6d2e8a14526951b15cbeaae89625838570affb429ab90df32b6a34a7d417b"}, - {file = "jh2-5.0.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ea555676ce7eb72d5be9b5107286c83c72cc12bf7dbdcc9d2381672451c077a4"}, - {file = "jh2-5.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ac1e1db4a92fa1b4625e57de2ad36b6376dfcc53be6298afbd82ee96f172d80f"}, - {file = "jh2-5.0.3-py3-none-any.whl", hash = "sha256:4ac75f013a1600d8111306fa5d3b35ce08bbbd8fde2aca096a0ddf6a415e999f"}, - {file = "jh2-5.0.3.tar.gz", hash = "sha256:c13d97a3f82a02e6a2a89606f1ffe1771670266dc7746140e00e66c4dad12b14"}, -] - -[[package]] -name = "kiss-headers" -version = "2.4.3" -description = "Object-oriented HTTP and IMAP (structured) headers." -optional = false -python-versions = ">=3.7" -files = [ - {file = "kiss_headers-2.4.3-py3-none-any.whl", hash = "sha256:9d800b77532068e8748be9f96f30eaeb547cdc5345e4689ddf07b77071256239"}, - {file = "kiss_headers-2.4.3.tar.gz", hash = "sha256:70c689ce167ac83146f094ea916b40a3767d67c2e05a4cb95b0fd2e33bf243f1"}, -] - -[[package]] -name = "librt" -version = "0.13.0" -description = "Mypyc runtime library" -optional = false -python-versions = ">=3.9" -files = [ - {file = "librt-0.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:34e47058fcc69a313293d6dee94216a4f30c929ae6f2476e58c5ba635aa639d5"}, - {file = "librt-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dbdd5b6509d0c2a8fe72cf494c299a61dbd58142a90a4190664ae159e4a7b547"}, - {file = "librt-0.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e56ea4ee4df77585a6b5c138f6538680886024fa559f5b55bd14b12e98e67b2"}, - {file = "librt-0.13.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f1f9cc4d09a46d9cb3c2063ae100629d3f52a6517c3c08c2f4c9828261883929"}, - {file = "librt-0.13.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f125f5d46b20f89dc5587a55cc416b4ba2a5b2ffda36d048ee120e17598a653a"}, - {file = "librt-0.13.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2608d3b39f9e0b4a66a130d9150c615cba40a5090d25eeeaa225e0e46de8c0ac"}, - {file = "librt-0.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9fd35e95ab5e45c3901d37110263c7db85a961110f5460588fe37f8c131f88a7"}, - {file = "librt-0.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5f31b0aa13c9b04370d4da6be1ab7779776b3a075cceb6747a39a4be85fe1e40"}, - {file = "librt-0.13.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0b795f5fc70fbbb787ceaf79bb3a0d627bcc33c53de51741755263ec406b775a"}, - {file = "librt-0.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36b306a623aaad96fe4b378692b54f9c0789fccd833b9851753d5fbf6138cfde"}, - {file = "librt-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a3762e75fcac8c9e4dacaaf438bffd9003e2ca2c531b756f3c0035deefa674c8"}, - {file = "librt-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:d63bae12a8aeb51380be3438e4dc4bd27354d0f8e19166b2f44e3e94d6f552dc"}, - {file = "librt-0.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1b5a7bbff495baedbd9b916c367d66854008f8f3b575908ded477c499dc60082"}, - {file = "librt-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34bc7938b9fdf14fe32a406c19c71faf894c5cee7e7474bd0be2f17200b82d14"}, - {file = "librt-0.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f40e56b61b41be5f7dec938cfeffd660668cf4b5e72c78e7bd671d66b7bc2c79"}, - {file = "librt-0.13.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:9c5d02b89de5acd0379a51ec44a89476fb03df6145442e1c8ecd6bee2f91b176"}, - {file = "librt-0.13.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7db9a3ff32ef5f7d1703d93831a3316cdf0b537de6a1cc03cc8fdd09b9194e89"}, - {file = "librt-0.13.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3dbb2a31882456cadc7053378e81ad7ed7693db4ac9f98ab5f81ef034aa8ec9f"}, - {file = "librt-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c6014e3c80f9c1fe268ef8b0e0ef113bac672cc032f2f93866e7ddad4f3e663d"}, - {file = "librt-0.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:091b60a4d2174fc1ec5c34cdc0b72efb6224753d76b7da61ebeab7a191aec8bd"}, - {file = "librt-0.13.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:66cb1138f384a191a6d75f986064841fcfdc0cea98f7bd9c9ab9b38049917588"}, - {file = "librt-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:17221a7569f8f292aa0014226e48aa25b8c2b08da18088cd230953d0ea0f9cd1"}, - {file = "librt-0.13.0-cp311-cp311-win32.whl", hash = "sha256:fc67741da44c6eaa90e01eafb586bbba9b51eb5b6ed381ee6f5ae72eb3316d21"}, - {file = "librt-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:cc99dfb62b23c9207c33d0be8a2e2af7a42e21e6ea388b380a0c948c7b88953b"}, - {file = "librt-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:40ccd13c252d3fe473ffc8a57be7565abc8b64cf1b108344c859d5164f7f3e0c"}, - {file = "librt-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0"}, - {file = "librt-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5"}, - {file = "librt-0.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9"}, - {file = "librt-0.13.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b"}, - {file = "librt-0.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03"}, - {file = "librt-0.13.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e"}, - {file = "librt-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd"}, - {file = "librt-0.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348"}, - {file = "librt-0.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7"}, - {file = "librt-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82"}, - {file = "librt-0.13.0-cp312-cp312-win32.whl", hash = "sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3"}, - {file = "librt-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa"}, - {file = "librt-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1"}, - {file = "librt-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3"}, - {file = "librt-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c"}, - {file = "librt-0.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c"}, - {file = "librt-0.13.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b"}, - {file = "librt-0.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9"}, - {file = "librt-0.13.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db"}, - {file = "librt-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6"}, - {file = "librt-0.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7"}, - {file = "librt-0.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1"}, - {file = "librt-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a"}, - {file = "librt-0.13.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628"}, - {file = "librt-0.13.0-cp313-cp313-win32.whl", hash = "sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927"}, - {file = "librt-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650"}, - {file = "librt-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566"}, - {file = "librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71"}, - {file = "librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6"}, - {file = "librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f"}, - {file = "librt-0.13.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180"}, - {file = "librt-0.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6"}, - {file = "librt-0.13.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9"}, - {file = "librt-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007"}, - {file = "librt-0.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0"}, - {file = "librt-0.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1"}, - {file = "librt-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d"}, - {file = "librt-0.13.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16"}, - {file = "librt-0.13.0-cp314-cp314-win32.whl", hash = "sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37"}, - {file = "librt-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39"}, - {file = "librt-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6"}, - {file = "librt-0.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5"}, - {file = "librt-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46"}, - {file = "librt-0.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e"}, - {file = "librt-0.13.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a"}, - {file = "librt-0.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22"}, - {file = "librt-0.13.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c"}, - {file = "librt-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0"}, - {file = "librt-0.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04"}, - {file = "librt-0.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61"}, - {file = "librt-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9"}, - {file = "librt-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18"}, - {file = "librt-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259"}, - {file = "librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99"}, - {file = "librt-0.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f442e3954b1addc759faae22a7c9a3f1e16d7d1db3f484279dc27d62e06968fa"}, - {file = "librt-0.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9e786428f291dd2d2f1cbfc0e0caa45a2e395fab0ad3e2c9314daa8873414390"}, - {file = "librt-0.13.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21b7ac084f701a9cdff6139745a6620579d65a9379ac2d9d50a86368b109e63c"}, - {file = "librt-0.13.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a6e556d6aba31c93dd97ce661d66614d2429c0a3923f9dc8f0af7e8df10223a4"}, - {file = "librt-0.13.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3657346f867469e962549435aa05fd15330b1d6a92829f8e27988e194382d005"}, - {file = "librt-0.13.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:791aa18a373b90da8ac3c44fc77544f33fdf53ae403acdce9b39f1c26b4a3b94"}, - {file = "librt-0.13.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d6fb0eaa108814581c4d3bfbd068c3fb6757812a81415008d1bae08267cca360"}, - {file = "librt-0.13.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a001519c315d5db40710f2665d32c4791f1d4779fc96a9423fd18d92c8b9ac7b"}, - {file = "librt-0.13.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:d9188caac26e47671b52836a5e2a49873a7fc11c673b0c122d22515f98bc14e1"}, - {file = "librt-0.13.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:05d96b80b95d3a2721b619f8982b8558848b04875bb4772fd54842b59f61dd97"}, - {file = "librt-0.13.0-cp39-cp39-win32.whl", hash = "sha256:c3cd253cf32fe4f4662960d6bf7d55cb8be0c31a5d644a4d48aeafebaff3409a"}, - {file = "librt-0.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:b15e26cc0fe622d0c67e98bee6ef6bc8f792e20ee3006aa12627a00463d9399f"}, - {file = "librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781"}, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "marshmallow" -version = "3.21.2" -description = "A lightweight library for converting complex datatypes to and from native Python datatypes." -optional = false -python-versions = ">=3.8" -files = [ - {file = "marshmallow-3.21.2-py3-none-any.whl", hash = "sha256:70b54a6282f4704d12c0a41599682c5c5450e843b9ec406308653b47c59648a1"}, - {file = "marshmallow-3.21.2.tar.gz", hash = "sha256:82408deadd8b33d56338d2182d455db632c6313aa2af61916672146bb32edc56"}, -] - -[package.dependencies] -packaging = ">=17.0" - -[package.extras] -dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"] -docs = ["alabaster (==0.7.16)", "autodocsumm (==0.2.12)", "sphinx (==7.3.7)", "sphinx-issues (==4.1.0)", "sphinx-version-warning (==1.1.2)"] -tests = ["pytest", "pytz", "simplejson"] - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[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.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "niquests" -version = "3.6.5" -description = "Niquests is a simple, yet elegant, HTTP library. It is a drop-in replacement for Requests, which is under feature freeze." -optional = false -python-versions = ">=3.7" -files = [ - {file = "niquests-3.6.5-py3-none-any.whl", hash = "sha256:8a2334da25001ef0db044ab0198d39704409e4c2d442ca18c5abd7ddee2ffd6a"}, - {file = "niquests-3.6.5.tar.gz", hash = "sha256:3895ec88e96c3050e11f79b6a6855ede8147f6fa2080ef74b2cc0a3bbe379653"}, -] - -[package.dependencies] -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -kiss-headers = ">=2,<4" -urllib3-future = ">=2.7.905,<3" -wassima = ">=1.0.1,<2" - -[package.extras] -http3 = ["urllib3-future[qh3]"] -ocsp = ["urllib3-future[qh3]"] -socks = ["urllib3-future[socks]"] -speedups = ["orjson (>=3,<4)", "urllib3-future[brotli,zstd]"] - -[[package]] -name = "packaging" -version = "24.0" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.7" -files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "platformdirs" -version = "4.2.2" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] - -[[package]] -name = "pluggy" -version = "1.5.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pydantic" -version = "2.7.1" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"}, - {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"}, -] - -[package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.18.2" -typing-extensions = ">=4.6.1" - -[package.extras] -email = ["email-validator (>=2.0.0)"] - -[[package]] -name = "pydantic-core" -version = "2.18.2" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"}, - {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"}, - {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"}, - {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"}, - {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"}, - {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"}, - {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"}, - {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"}, - {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"}, - {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"}, - {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"}, - {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"}, - {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"}, - {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"}, - {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"}, - {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"}, - {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"}, - {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"}, - {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"}, - {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"}, - {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"}, - {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"}, - {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"}, - {file = "pydantic_core-2.18.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439"}, - {file = "pydantic_core-2.18.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70"}, - {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b"}, - {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761"}, - {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788"}, - {file = "pydantic_core-2.18.2-cp38-none-win32.whl", hash = "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350"}, - {file = "pydantic_core-2.18.2-cp38-none-win_amd64.whl", hash = "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e"}, - {file = "pydantic_core-2.18.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8"}, - {file = "pydantic_core-2.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0"}, - {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4"}, - {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399"}, - {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b"}, - {file = "pydantic_core-2.18.2-cp39-none-win32.whl", hash = "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e"}, - {file = "pydantic_core-2.18.2-cp39-none-win_amd64.whl", hash = "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"}, - {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"}, - {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"}, - {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - -[[package]] -name = "pygments" -version = "2.18.0" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pylint" -version = "3.2.2" -description = "python code static checker" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "pylint-3.2.2-py3-none-any.whl", hash = "sha256:3f8788ab20bb8383e06dd2233e50f8e08949cfd9574804564803441a4946eab4"}, - {file = "pylint-3.2.2.tar.gz", hash = "sha256:d068ca1dfd735fb92a07d33cb8f288adc0f6bc1287a139ca2425366f7cbe38f8"}, -] - -[package.dependencies] -astroid = ">=3.2.2,<=3.3.0-dev0" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = [ - {version = ">=0.2", markers = "python_version < \"3.11\""}, - {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, - {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, -] -isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" -mccabe = ">=0.6,<0.8" -platformdirs = ">=2.2.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -tomlkit = ">=0.10.1" - -[package.extras] -spelling = ["pyenchant (>=3.2,<4.0)"] -testutils = ["gitpython (>3)"] - -[[package]] -name = "pytest" -version = "8.2.1" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, - {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=1.5,<2.0" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-cov" -version = "5.0.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, - {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, -] - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] - -[[package]] -name = "pytest-runner" -version = "6.0.1" -description = "Invoke py.test as distutils command with dependency resolution" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pytest-runner-6.0.1.tar.gz", hash = "sha256:70d4739585a7008f37bf4933c013fdb327b8878a5a69fcbb3316c88882f0f49b"}, - {file = "pytest_runner-6.0.1-py3-none-any.whl", hash = "sha256:ea326ed6f6613992746062362efab70212089a4209c08d67177b3df1c52cd9f2"}, -] - -[package.extras] -docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] -testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-virtualenv", "types-setuptools"] - -[[package]] -name = "pytest-watch" -version = "4.2.0" -description = "Local continuous test runner with pytest and watchdog." -optional = false -python-versions = "*" -files = [ - {file = "pytest-watch-4.2.0.tar.gz", hash = "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9"}, -] - -[package.dependencies] -colorama = ">=0.3.3" -docopt = ">=0.4.0" -pytest = ">=2.6.4" -watchdog = ">=0.6.0" - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "qh3" -version = "1.0.7" -description = "A lightway and fast implementation of QUIC and HTTP/3" -optional = false -python-versions = ">=3.7" -files = [ - {file = "qh3-1.0.7-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:392cbfc95c832f78b840a5b17d743d4dcf8d47d7217d17370b939a8717939fa6"}, - {file = "qh3-1.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:75bea34952369975a646379aa2b6438f557b4da7a76ddb59973000d96ea8063e"}, - {file = "qh3-1.0.7-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:9d40552d28eaa89c819edfc3bc3752ed3d7da59119840d8fe09790b9c76f5819"}, - {file = "qh3-1.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f1283bbab24b26565fb9b90278ea96c7940396d31ee9fac169e9c7e1b36fd96"}, - {file = "qh3-1.0.7-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f7f910a1523b1d2f16e20034452b63335f90431868365bee0fe29d8e6473438"}, - {file = "qh3-1.0.7-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:92d87664b758bc1f8b4bcfa60cce866c1e96938d59af3de25189661e263cc510"}, - {file = "qh3-1.0.7-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6f56c34e8d7009596a61a800c80f478a9e9ceccfdc11b28544b59ad904a5ff6"}, - {file = "qh3-1.0.7-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4da2ba491c7fb240556a996b0a10cf7b6ce17816a9c2a53a02b08a40eb2fa37"}, - {file = "qh3-1.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586cfd89068fd44e5471b31bd7d4c4b01b80a85ac28e26b14da6ede4583e8017"}, - {file = "qh3-1.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:1835f149b1d359a9de915207bc0543004edcb3077390614bca5d52c637914ad3"}, - {file = "qh3-1.0.7-cp37-abi3-musllinux_1_1_armv7l.whl", hash = "sha256:1e7618c6059a5f838b858ea834ef6bc66d1e95bdeb8ea466217d74aedf471415"}, - {file = "qh3-1.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ebf612690cfefc66d19648f67bd0a7a83112230c322e21245a5536ca05aa9564"}, - {file = "qh3-1.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:7ef77f2663f60ac3b46fc13ad67076dcff8dfe37886c1793fa7411c50385b061"}, - {file = "qh3-1.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4fb6c73399313881b80708354af6ac5d837cf9733189476f919d7c9b88e5ccba"}, - {file = "qh3-1.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4bbd1a4a5b49cbcfd02a57b07eab729cec7b4a75f2180a03fa0d200d46833cbb"}, - {file = "qh3-1.0.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:0ccbcf47a1311deece090ac6c83918741be4b568f7018748d3f3762c70a45c3c"}, - {file = "qh3-1.0.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6e5871df72c6303e9c68e8ab50d25152fc4d4ee0d8ebdced20724922fbaeb53"}, - {file = "qh3-1.0.7-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:981725de5210eb8f902fa5015b029072a532c4dd44770bc6d2b45b569b91212a"}, - {file = "qh3-1.0.7-pp310-pypy310_pp73-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4018dc5023738b4da756297787e6606a771777b576d61da95226c2ec0d5d50ef"}, - {file = "qh3-1.0.7-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:debbcd38adf35bb2589095f78ef59690f507b1fd6aa71440c80ad17a15485fbc"}, - {file = "qh3-1.0.7-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c83a719c48dbd3d8d592193e83360c11657a5c1bda31e519a8b8d7b1f60ae627"}, - {file = "qh3-1.0.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6487edea20ac7bea0e820bafb9bd4957d80a341facd7d64163953adcf560bf95"}, - {file = "qh3-1.0.7-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e70de70904cca4f53dec7ddbc15e8d2fe0e4cad01c05063cddb1087460a367a"}, - {file = "qh3-1.0.7-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8e03f81f60ef0772e2a32e3e56fbcc907f111f4057909fbb013e2f65516b5799"}, - {file = "qh3-1.0.7-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:89ece443484baff18ce12d08cbcf5fa8bcfbfbc6517e1f2e020666c6adf003e9"}, - {file = "qh3-1.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0ce94a6fbd6bd2715e66d28560af8c41bd99bd419bb89abc45f761932dc8cb54"}, - {file = "qh3-1.0.7-pp37-pypy37_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6c8b427f31c486fc63898ec15b5d12636f29d09275e186da5cacbf4296143e7d"}, - {file = "qh3-1.0.7-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8bf1b4f50639ee5277e4baef3740c4790f59760c4a537cef9e8096b2c7c2ca69"}, - {file = "qh3-1.0.7-pp37-pypy37_pp73-macosx_11_0_arm64.whl", hash = "sha256:acb42fc6d6251ee9af39a4330c9fe3a4958517b76ee7f4c66787a8b099bdf0f4"}, - {file = "qh3-1.0.7-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5a807d4bac257e1e68e690464444361320ce730f68d8d1821a1e70a3cd795cf"}, - {file = "qh3-1.0.7-pp37-pypy37_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d51d9402d5544540363424cfe9c06f7c1e7e0b1bfaaa7128e5034d27a0f2cef"}, - {file = "qh3-1.0.7-pp37-pypy37_pp73-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fbcb8a29285b8a9d1587948b30f2520bc564d0341f860c2fc57a5d53a77a2e23"}, - {file = "qh3-1.0.7-pp37-pypy37_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0753e77e9bbb6140c05adb3fbd19dbba6b2f5f27598e608e2a543165ede4b337"}, - {file = "qh3-1.0.7-pp37-pypy37_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25a1e9a27efd7fe55e521f4f68b9bc7a4ef6b1a0f51cb8a98134fcf6e9bec6ab"}, - {file = "qh3-1.0.7-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f6ad8a4c31b5579b65482172e7e7f945ec7b29030046ce29d7fce396246fbb7"}, - {file = "qh3-1.0.7-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:43911c6f206ac2654b857bef14dda7394509f9746f145ccb4e496f32a9d67422"}, - {file = "qh3-1.0.7-pp37-pypy37_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:bda728efcf03e49ec16d1f0a8d3953fcc113df5d3055b63c838d7dc7834c357e"}, - {file = "qh3-1.0.7-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:03f7ea1cedacbfe6252f202ac65d0040a8942ce627df241bf74a0f2ad7c4a73f"}, - {file = "qh3-1.0.7-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:5f82a48b9c06b51ab482fedd4b0476b0c0ce0f4a0d47c5fef924adc8cd795a89"}, - {file = "qh3-1.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:091247b9bb24e6abb055772a502ea70857e7dabdf1dd993053db680c7bdd3718"}, - {file = "qh3-1.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c73458b6a79652a63a64b715d8047f07d7f61ab091a87f32c2303f9cb9d77bbd"}, - {file = "qh3-1.0.7-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:733553159d65b447bf7c7aa84cde6b158e057025b602192c43b41083f3e06b4a"}, - {file = "qh3-1.0.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308154b300b28436a415f2163b942e0c809f8cbeede1a940be563d64462e35b2"}, - {file = "qh3-1.0.7-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d5db32a2a9ea4210bb5a3c6c037e02d11f6709be38e077eee77cdf0e626dc7f7"}, - {file = "qh3-1.0.7-pp38-pypy38_pp73-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:57d3013bb1f03cf1a2c8a1bb7d536722eaa059c58d3f6918fc5b62f8469d4f6c"}, - {file = "qh3-1.0.7-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaca92cfbaabf2a8792325243e63cf7512c4760aed2371a5fc9ce32612245bfe"}, - {file = "qh3-1.0.7-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7da2e218fed81602d4f1034cd9698afee2c56270b1520cfde09e14d47dbdb2d2"}, - {file = "qh3-1.0.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04a0005c4ccf5d9510491d5e731021af1f764c23e2bb456f3b5674311a126e90"}, - {file = "qh3-1.0.7-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:15b54ed14df131266e65d7ef09b7bb3f1fb4d74d4fb632b51d233c58be050368"}, - {file = "qh3-1.0.7-pp38-pypy38_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:bf675b50dbf971d21e8fc4ccf6fc18c77942f6ac20358ac6ca3a98857e467f42"}, - {file = "qh3-1.0.7-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:a438f7cb5923ec519c3a1bd4e8db67067abe5d1d3d0499bffd19213b52ee654f"}, - {file = "qh3-1.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:53ccddf58ea50250609da3753977ad501783fa0cf2dc15960add5c15e2313b47"}, - {file = "qh3-1.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:168bd4f481c6b7f780e4f46f399d6c7faadebe4227b5130d0d7c335d10dc13e3"}, - {file = "qh3-1.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:188ef7bd33b1b85af93058ce365ff7b65377a99580fdc37bf9d18de98ac90ebd"}, - {file = "qh3-1.0.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:63407c2c680ac4a713f502a23658e9e26818133312b78765bb5efe21978feb02"}, - {file = "qh3-1.0.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:703775328761d10a6ba2e412a6c44304e28b4d05799765496c7a21569ac7f732"}, - {file = "qh3-1.0.7-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ffe51161a782c99cfe792100af43f8577ac3f9fcc247e80a860981cd8e4bc8f7"}, - {file = "qh3-1.0.7-pp39-pypy39_pp73-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:567a2a76b2c5ef42a2c9adef9596c8ebbdb614660363bcd020096ea38969bede"}, - {file = "qh3-1.0.7-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9aa47c11d2d4c2ed75506333fd83c481017f6da7f45002e24e391842d8dc989a"}, - {file = "qh3-1.0.7-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67433e71e5bf6a76f41f1bde9c69aaf017ae3b4ae5e9f3e4a123a45619dc5783"}, - {file = "qh3-1.0.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2569550be0faa50e723a1bbf3a9061b66a7546d514fea81bb9125e16f6a7d37"}, - {file = "qh3-1.0.7-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a5aea4ed5b8880210aa34957c33b4c576b2e68b9cbc48013974d34a41d71d17f"}, - {file = "qh3-1.0.7-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:be0675ced825a4a659888541060031621b575a99bee4efc1e435ffa7815a696d"}, - {file = "qh3-1.0.7-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ecf9eb27fcbbd9c8cf4a374c5e0d034ec0c3e576f1ed028f7ce84a3484fb06fa"}, - {file = "qh3-1.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:89f00a47cef7197ea6367bb0a1ecb9517aeaf8c8dd26202cfbf3e3346d0f4aeb"}, - {file = "qh3-1.0.7.tar.gz", hash = "sha256:eb527d8317746209509b9c575527577cdc9b3cfb0f49294fc1cd109b0570362c"}, -] - -[[package]] -name = "rich" -version = "13.7.1" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "rstcheck" -version = "6.2.1" -description = "Checks syntax of reStructuredText and code blocks nested within it" -optional = false -python-versions = ">=3.8" -files = [ - {file = "rstcheck-6.2.1-py3-none-any.whl", hash = "sha256:b450943707d8ca053f5c6b9f103ee595f4926a064203e5e579172aefb3fe2c12"}, - {file = "rstcheck-6.2.1.tar.gz", hash = "sha256:e4d173950b023eb12c2b9d2348a8c62bef46612bbc7b29e1e57d37320ed0a891"}, -] - -[package.dependencies] -rstcheck-core = ">=1.1" -typer = {version = ">=0.4.1", extras = ["all"]} - -[package.extras] -dev = ["rstcheck[docs,sphinx,testing,toml,type-check]", "tox (>=3.15)"] -docs = ["m2r2 (>=0.3.2)", "sphinx (>=5.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx-click (>=4.0.3)", "sphinx-rtd-theme (>=1.2)", "sphinxcontrib-spelling (>=7.3)"] -sphinx = ["sphinx (>=5.0)"] -testing = ["coverage-conditional-plugin (>=0.5)", "coverage[toml] (>=6.0)", "pytest (>=7.2)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.0)", "pytest-sugar (>=0.9.5)"] -toml = ["tomli (>=2.0)"] -type-check = ["mypy (>=1.0)"] - -[[package]] -name = "rstcheck-core" -version = "1.2.1" -description = "Checks syntax of reStructuredText and code blocks nested within it" -optional = false -python-versions = ">=3.8" -files = [ - {file = "rstcheck-core-1.2.1.tar.gz", hash = "sha256:9b330020d912e2864f23f332c1a0569463ca3b06b8fee7b7bdd201b055f7f831"}, - {file = "rstcheck_core-1.2.1-py3-none-any.whl", hash = "sha256:1c100de418b6c9e14d9cf6558644d0ab103fdc447f891313882d02df3a3c52ba"}, -] - -[package.dependencies] -docutils = ">=0.7" -pydantic = ">=2" - -[package.extras] -dev = ["rstcheck-core[docs,sphinx,testing,toml,type-check,yaml]", "tox (>=3.15)"] -docs = ["m2r2 (>=0.3.2)", "sphinx (>=5.0,!=7.2.5)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.15)", "sphinx-rtd-theme (>=1.2)", "sphinxcontrib-apidoc (>=0.3)", "sphinxcontrib-spelling (>=7.3)"] -sphinx = ["sphinx (>=5.0)"] -testing = ["coverage-conditional-plugin (>=0.5)", "coverage[toml] (>=6.0)", "pytest (>=7.2)", "pytest-cov (>=3.0)", "pytest-mock (>=3.7)", "pytest-randomly (>=3.0)", "pytest-sugar (>=0.9.5)"] -toml = ["tomli (>=2.0)"] -type-check = ["mypy (>=1.0)", "types-PyYAML (>=6.0.0)", "types-docutils (>=0.18)"] -yaml = ["pyyaml (>=6.0.0)"] - -[[package]] -name = "shellingham" -version = "1.5.4" -description = "Tool to Detect Surrounding Shell" -optional = false -python-versions = ">=3.7" -files = [ - {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, - {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, -] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "svix" -version = "1.24.0" -description = "Svix webhooks API client and webhook verification library" -optional = false -python-versions = ">=3.6" -files = [ - {file = "svix-1.24.0.tar.gz", hash = "sha256:02a5daf20123cfa20f5b9302e77df8d1f9e8223430af0ad51c2c544a0ba549b2"}, -] - -[package.dependencies] -attrs = ">=21.3.0" -Deprecated = "*" -httpx = ">=0.23.0" -python-dateutil = "*" -types-Deprecated = "*" -types-python-dateutil = "*" - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[[package]] -name = "tomlkit" -version = "0.12.5" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f"}, - {file = "tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c"}, -] - -[[package]] -name = "typer" -version = "0.12.3" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -optional = false -python-versions = ">=3.7" -files = [ - {file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"}, - {file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"}, -] - -[package.dependencies] -click = ">=8.0.0" -rich = ">=10.11.0" -shellingham = ">=1.3.0" -typing-extensions = ">=3.7.4.3" - -[[package]] -name = "types-deprecated" -version = "1.2.9.20240311" -description = "Typing stubs for Deprecated" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-Deprecated-1.2.9.20240311.tar.gz", hash = "sha256:0680e89989a8142707de8103f15d182445a533c1047fd9b7e8c5459101e9b90a"}, - {file = "types_Deprecated-1.2.9.20240311-py3-none-any.whl", hash = "sha256:d7793aaf32ff8f7e49a8ac781de4872248e0694c4b75a7a8a186c51167463f9d"}, -] - -[[package]] -name = "types-python-dateutil" -version = "2.9.0.20240316" -description = "Typing stubs for python-dateutil" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-python-dateutil-2.9.0.20240316.tar.gz", hash = "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202"}, - {file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"}, -] - -[[package]] -name = "typing-extensions" -version = "4.11.0" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, - {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, -] - -[[package]] -name = "typing-inspect" -version = "0.9.0" -description = "Runtime inspection utilities for typing module." -optional = false -python-versions = "*" -files = [ - {file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"}, - {file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"}, -] - -[package.dependencies] -mypy-extensions = ">=0.3.0" -typing-extensions = ">=3.7.4" - -[[package]] -name = "urllib3-future" -version = "2.7.910" -description = "urllib3.future is a powerful HTTP 1.1, 2, and 3 client with both sync and async interfaces" -optional = false -python-versions = ">=3.7" -files = [ - {file = "urllib3_future-2.7.910-py3-none-any.whl", hash = "sha256:364a42fb806242ada6ba55123a653a1e8fd72b9c6436422caec5f07cb8777507"}, - {file = "urllib3_future-2.7.910.tar.gz", hash = "sha256:480d04d20061878d3a275a212d708ab854f7a2bf4c98a2286861cff812954189"}, -] - -[package.dependencies] -h11 = ">=0.11.0,<1.0.0" -jh2 = ">=5.0.3,<6.0.0" -qh3 = {version = ">=1.0.3,<2.0.0", markers = "(platform_system == \"Darwin\" or platform_system == \"Windows\" or platform_system == \"Linux\") and (platform_machine == \"x86_64\" or platform_machine == \"s390x\" or platform_machine == \"aarch64\" or platform_machine == \"armv7l\" or platform_machine == \"ppc64le\" or platform_machine == \"ppc64\" or platform_machine == \"AMD64\" or platform_machine == \"arm64\") and (platform_python_implementation == \"CPython\" or (platform_python_implementation == \"PyPy\" and python_version < \"3.11\"))"} - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -qh3 = ["qh3 (>=1.0.3,<2.0.0)"] -socks = ["python-socks (>=2.0,<3.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "wassima" -version = "1.1.1" -description = "Access your OS root certificates with the atmost ease" -optional = false -python-versions = ">=3.7" -files = [ - {file = "wassima-1.1.1-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:1f6f5198ddb3d68d7b6fe9229c55a2c83cb56232b72dcdd4b2ebc7540138aa20"}, - {file = "wassima-1.1.1-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:583e9e510cb88f067e9a48b39ac58549a258623d1e07eb6bb0512280a10c0e8f"}, - {file = "wassima-1.1.1-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:59c73287561dcbd0102ff897b136af0ef5e5879192f0908c597d85b8269701b0"}, - {file = "wassima-1.1.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50afde20c335c18c39a47ff9c9d1c481864275e6ec1fb50b23ff4d693428b4fd"}, - {file = "wassima-1.1.1-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b2edfdc6ec4f07a35fd4b84c2b282f856062a832ced48eddce1a44c82525f275"}, - {file = "wassima-1.1.1-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f41980d3d83bc446e28822c2f4a395f787814273f623811e4ed1035dfde7b267"}, - {file = "wassima-1.1.1-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a04100cdd5ad8f7d1e85b1b679bc7b5db95c788d24aed6e8b63a8ad47fccf62e"}, - {file = "wassima-1.1.1-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af205bfad17f814a05341bc1889ff61c40f44b37139c58c9c47aa593e83e2f"}, - {file = "wassima-1.1.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdba17f5d0844468c8bb7b5103505985bdedcad5dd90cd722475386f115332d2"}, - {file = "wassima-1.1.1-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:32bca7ed733a94c32d6c78d10c902dd4dc2dcf5785b560d82915de1544e076ce"}, - {file = "wassima-1.1.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ae474c2458addbe1b657ee080dc73a52ef002504c055f71176a246aecf2b714f"}, - {file = "wassima-1.1.1-cp37-abi3-musllinux_1_1_armv7l.whl", hash = "sha256:c2baa1316b40044caecb9fd5f574527784673b809515668bb7d9631c29f03d39"}, - {file = "wassima-1.1.1-cp37-abi3-musllinux_1_1_i686.whl", hash = "sha256:bc2d94894c6c270787b010b12243ce4a2ffe6a62b1f5682c912ea4540fe5d6df"}, - {file = "wassima-1.1.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:8f81f8dc770e7988321003776b2d3fff65339de4216e6f9b39d831333390c728"}, - {file = "wassima-1.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:878ae83bb9130b0c86426fc7682b397e6c8fd7a0457c277485e96b792f7f12b1"}, - {file = "wassima-1.1.1-cp37-abi3-win_arm64.whl", hash = "sha256:491d0541b5995618efc85c9b249f1a9fa33744973e42bd523bf52c01adcd078e"}, - {file = "wassima-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:84a1607d508024febacc192414797daa0bce64bbf24e7ae93e182aecadaf200a"}, - {file = "wassima-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a63866dec44e5d0ef9606b43a33d3017d0de7d931a128a3d1b0c28ad5714e8ce"}, - {file = "wassima-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:85244bda65f8a39ad700aa941f1f0123f588933c79afbb4f65f600fa6c83863d"}, - {file = "wassima-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:665f0b4a424b56e907bd579d8144b339f1383a6dd53bd4ebdd6b6f2653cd6d3e"}, - {file = "wassima-1.1.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:41630a1f13b356f19cda2ab406ca93185a426fc65ba608d58000c125f07cdf2e"}, - {file = "wassima-1.1.1-pp310-pypy310_pp73-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:028f07252188a7b6a1e73c955959b3028d6f5633e798eb6b27ee96716c62ab31"}, - {file = "wassima-1.1.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec076533caed061bdec324b0536bf01b704edf4a6bfd022462bc64233d9660e2"}, - {file = "wassima-1.1.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c99e98dd7960d7dd7342f5fa569a4cfcd5e9333f74f1a9115a483fa6c03a5f8"}, - {file = "wassima-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1aa554e79832c8f94fbedc77a53ff4207cfce45ebd0c3a550d6c0eb8fbd82118"}, - {file = "wassima-1.1.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7cac5cc91a6dc12b675c6749e5e0a36f35b4976dd37df36d50246fd8c3707866"}, - {file = "wassima-1.1.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2763d8f5cc58c4a9ee986c39709d3af91a9dc215c8a8895b992783a828a35824"}, - {file = "wassima-1.1.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8d2d8cbec28a9f810207ca2647a0cf6c674b95162f2269d35846421d5948a1e6"}, - {file = "wassima-1.1.1-pp310-pypy310_pp73-musllinux_1_1_i686.whl", hash = "sha256:0353ba99c1db703902d084a81875ed3e06a3831f3eb039240607198e8f6bbb5c"}, - {file = "wassima-1.1.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:95d1b7144286c090b943fc42711da922384f7e4e629a5dd2e8f05b66d008cf76"}, - {file = "wassima-1.1.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bdb76ee185736ed98ed92b8200b380b732b4e3df08219ea9491c6c7c9a983778"}, - {file = "wassima-1.1.1-pp37-pypy37_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:1f7d14d3019f179b5747168282fcd4c0cc3a2363eb974bcbc532b546a4513c24"}, - {file = "wassima-1.1.1-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e26e96df5d5358f093314ac162aed33ae3c04d80e9bdd1c1c2cd07c8e0109f36"}, - {file = "wassima-1.1.1-pp37-pypy37_pp73-macosx_11_0_arm64.whl", hash = "sha256:18857ddbc6d257541027ffc59dc6e87c21bf0ba5a10abde5073d01cec0352faf"}, - {file = "wassima-1.1.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86fc536580e31c81717212751fab16ff962f16302f71a71a2786a4c743ac1e1a"}, - {file = "wassima-1.1.1-pp37-pypy37_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f2ce27271a38fd21e2dc3cc5167431a63473ebeec1c02975e4000c95d04e35a9"}, - {file = "wassima-1.1.1-pp37-pypy37_pp73-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4ed19ff2afc209ea837fefbfe9eec01eaa6b8af46fe8dbdb8a692a894646013b"}, - {file = "wassima-1.1.1-pp37-pypy37_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c08969d10347b71b25c31e9ce73416002ff9d27fa22aa1e652ecd8cffda0a92"}, - {file = "wassima-1.1.1-pp37-pypy37_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c24eea8459388db8ec524c32eca1e06fff2ea274b2ca92431f5a9a5b05b4b6e6"}, - {file = "wassima-1.1.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cb04b797b1cc0504d0c34625b1e939c236e001a73db0615825c0b1f9b926992"}, - {file = "wassima-1.1.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e289c87ef4a738593bc0ce455c564e1c17d4acffab27d010751cd60b47c235cd"}, - {file = "wassima-1.1.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:797200152716971b9d7f06bf1bcc3767b2998afa73bb1515c3bdc7828e70a711"}, - {file = "wassima-1.1.1-pp37-pypy37_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0b080a60c5019dcd11e4dd2870479a1876d09c1caa2f1d2cfb9645a10e7880b8"}, - {file = "wassima-1.1.1-pp37-pypy37_pp73-musllinux_1_1_i686.whl", hash = "sha256:b870df6e1166b86522298d5a3d945ce65743be2d16586b9d2eabbb49ec7dbf2c"}, - {file = "wassima-1.1.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:14d8395e06d0db4a54f99058bf00818420d4cfda8fbcc48b56e8ca73d5f2551a"}, - {file = "wassima-1.1.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:5166da190adff31bdc432d4675e75945e88341a2334e5420a50f0f9758fb55b9"}, - {file = "wassima-1.1.1-pp38-pypy38_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dfe2cc6069aa074f2262ef128ad31d4212d850ea3c5c793525ed93a29b356658"}, - {file = "wassima-1.1.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:829c11682c5ef7a90bc248a56ef50ef1290335b554626c1221f8a8c244d69cbb"}, - {file = "wassima-1.1.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:e22c247574d35c2f450ede7662e630d9fd07e2230434aea142afd3cf819a3f3b"}, - {file = "wassima-1.1.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e996e4772a41edffcce071360bcc2359deae3e6dd23ad1b101f4bb5168e7d0c6"}, - {file = "wassima-1.1.1-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49cf2bc1f7e4cd622600bfbbb709320d5b60469dfaeef5493cc464771ffd027a"}, - {file = "wassima-1.1.1-pp38-pypy38_pp73-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8866a5f401b790f47fb084227e6c8576c25f48b1e50161a50705d603b67147cb"}, - {file = "wassima-1.1.1-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9ab2757fdbf1ea4c6623c575866b8c6c0ea1c8c99470e2e3c67ffae63f73f85"}, - {file = "wassima-1.1.1-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a45b6986634634404da64c34386a71130666ac930e9a84be4050b8777ba8b9d"}, - {file = "wassima-1.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:890121a09213b98755eab091c72d9e2fdfe7f659d153051dc8a8adeef366e36f"}, - {file = "wassima-1.1.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:36194a67632b472f455a36b89efff7b9e1b521df4d24a3ba2789b2bb322ac81e"}, - {file = "wassima-1.1.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:856ffd4b95ae31518e7c7109796feef27801da79c40ee1c75f166c9655934cd2"}, - {file = "wassima-1.1.1-pp38-pypy38_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:6b0142f9121095b5807159621d6c37cf1defb409a6cd245d0f496d40072f67c1"}, - {file = "wassima-1.1.1-pp38-pypy38_pp73-musllinux_1_1_i686.whl", hash = "sha256:0612efb6f2e9339a3748fa8dc622f0be5f71b95b4471e1c1fbf5006a7e98a060"}, - {file = "wassima-1.1.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:18cdae934705ec6a8df947f25c5230d6da812717ac23b5f51ad570c63bf7e3d7"}, - {file = "wassima-1.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:21e87777709d5c7d60503c589deef6a214ff8b94fa3e75ad5ff19518aeb0d4b1"}, - {file = "wassima-1.1.1-pp39-pypy39_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:3565a0a206003acfa9eade388c7374add98800025bb04f568e57e11be27167ee"}, - {file = "wassima-1.1.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:29b55e43836bf64c7f2a022e8dd358abdab05894cb444a4ad5d550ac0f8869d3"}, - {file = "wassima-1.1.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:929216bccf041e21ad27e28aff46e9744f001792eb5ef2868a826a18fa2f58c7"}, - {file = "wassima-1.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd201e7ba92dfb919b5200b844536133015e8f231954160e2d57ebf6e54d3cc2"}, - {file = "wassima-1.1.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f6cb1b86329dd39d4e097d5d5abae9da47ce41485a4fd138c631b6e07389b8c"}, - {file = "wassima-1.1.1-pp39-pypy39_pp73-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:39511a31adbd988ec9706459f162b0258ef8be231837cf3e1adfce80858484ec"}, - {file = "wassima-1.1.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd1f0d44b4a9274c89faa2cb2dc193d7e71e263cdd82184cb34bcabd6acfdbfd"}, - {file = "wassima-1.1.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1736dd1a018dc4a9a71c51de7a3dd1bed76b31d35c7360de974c73542ea47e8"}, - {file = "wassima-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36a5b0eb6713803ff0034ee5317950a36f2edcde73be8ecdcc959c2fb5e1fc39"}, - {file = "wassima-1.1.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:83352eef88ce33fd7729215f4aea0bece6f05cb9226c710cf4fa4cd0a6a69e93"}, - {file = "wassima-1.1.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:824af089a281d6cfb15d12708eed3cae211a15bbf3a0925d52fa242c10672091"}, - {file = "wassima-1.1.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:5298e90f925205b037e44efcd843d2414aad720d6755fa8d604b1385272d0f9e"}, - {file = "wassima-1.1.1-pp39-pypy39_pp73-musllinux_1_1_i686.whl", hash = "sha256:eafece652771c9e7d1922348e67c57481a1a488faa98235ff0f3bdd6732a7fbe"}, - {file = "wassima-1.1.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b00e2db17d32c2b97ce1429fd25e868c2dc0380ecf55ffae9949ff866053364c"}, - {file = "wassima-1.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e8d67bb4ba6941fa16418e9af884aa2f3b002af1edb25c82dbdc0f96fc7ad594"}, - {file = "wassima-1.1.1-py3-none-any.whl", hash = "sha256:b5b67d9128d728d35a0dd5b0ed071a0feaf2c7e0a7416660864f180b752623df"}, - {file = "wassima-1.1.1.tar.gz", hash = "sha256:b673f31051fd1b9292bd6e05853016b401ac703c377a7d0657242eb41ce6121b"}, -] - -[[package]] -name = "watchdog" -version = "4.0.0" -description = "Filesystem events monitoring" -optional = false -python-versions = ">=3.8" -files = [ - {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:39cb34b1f1afbf23e9562501673e7146777efe95da24fab5707b88f7fb11649b"}, - {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c522392acc5e962bcac3b22b9592493ffd06d1fc5d755954e6be9f4990de932b"}, - {file = "watchdog-4.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c47bdd680009b11c9ac382163e05ca43baf4127954c5f6d0250e7d772d2b80c"}, - {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8350d4055505412a426b6ad8c521bc7d367d1637a762c70fdd93a3a0d595990b"}, - {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c17d98799f32e3f55f181f19dd2021d762eb38fdd381b4a748b9f5a36738e935"}, - {file = "watchdog-4.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4986db5e8880b0e6b7cd52ba36255d4793bf5cdc95bd6264806c233173b1ec0b"}, - {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:11e12fafb13372e18ca1bbf12d50f593e7280646687463dd47730fd4f4d5d257"}, - {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5369136a6474678e02426bd984466343924d1df8e2fd94a9b443cb7e3aa20d19"}, - {file = "watchdog-4.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76ad8484379695f3fe46228962017a7e1337e9acadafed67eb20aabb175df98b"}, - {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:45cc09cc4c3b43fb10b59ef4d07318d9a3ecdbff03abd2e36e77b6dd9f9a5c85"}, - {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eed82cdf79cd7f0232e2fdc1ad05b06a5e102a43e331f7d041e5f0e0a34a51c4"}, - {file = "watchdog-4.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba30a896166f0fee83183cec913298151b73164160d965af2e93a20bbd2ab605"}, - {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d18d7f18a47de6863cd480734613502904611730f8def45fc52a5d97503e5101"}, - {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2895bf0518361a9728773083908801a376743bcc37dfa252b801af8fd281b1ca"}, - {file = "watchdog-4.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87e9df830022488e235dd601478c15ad73a0389628588ba0b028cb74eb72fed8"}, - {file = "watchdog-4.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6e949a8a94186bced05b6508faa61b7adacc911115664ccb1923b9ad1f1ccf7b"}, - {file = "watchdog-4.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6a4db54edea37d1058b08947c789a2354ee02972ed5d1e0dca9b0b820f4c7f92"}, - {file = "watchdog-4.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d31481ccf4694a8416b681544c23bd271f5a123162ab603c7d7d2dd7dd901a07"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8fec441f5adcf81dd240a5fe78e3d83767999771630b5ddfc5867827a34fa3d3"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:6a9c71a0b02985b4b0b6d14b875a6c86ddea2fdbebd0c9a720a806a8bbffc69f"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:557ba04c816d23ce98a06e70af6abaa0485f6d94994ec78a42b05d1c03dcbd50"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0f9bd1fd919134d459d8abf954f63886745f4660ef66480b9d753a7c9d40927"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f9b2fdca47dc855516b2d66eef3c39f2672cbf7e7a42e7e67ad2cbfcd6ba107d"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:73c7a935e62033bd5e8f0da33a4dcb763da2361921a69a5a95aaf6c93aa03a87"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6a80d5cae8c265842c7419c560b9961561556c4361b297b4c431903f8c33b269"}, - {file = "watchdog-4.0.0-py3-none-win32.whl", hash = "sha256:8f9a542c979df62098ae9c58b19e03ad3df1c9d8c6895d96c0d51da17b243b1c"}, - {file = "watchdog-4.0.0-py3-none-win_amd64.whl", hash = "sha256:f970663fa4f7e80401a7b0cbeec00fa801bf0287d93d48368fc3e6fa32716245"}, - {file = "watchdog-4.0.0-py3-none-win_ia64.whl", hash = "sha256:9a03e16e55465177d416699331b0f3564138f1807ecc5f2de9d55d8f188d08c7"}, - {file = "watchdog-4.0.0.tar.gz", hash = "sha256:e3e7065cbdabe6183ab82199d7a4f6b3ba0a438c5a512a68559846ccb76a78ec"}, -] - -[package.extras] -watchmedo = ["PyYAML (>=3.10)"] - -[[package]] -name = "wrapt" -version = "1.16.0" -description = "Module for decorators, wrappers and monkey patching." -optional = false -python-versions = ">=3.6" -files = [ - {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, - {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, - {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, - {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, - {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, - {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, - {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, - {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, - {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, - {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, - {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, - {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, - {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, - {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, - {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, - {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, - {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, - {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, -] - -[metadata] -lock-version = "2.0" -python-versions = "^3.10.0" -content-hash = "5e6069a97d8f774c1413f8e09c6a4c3d639296a5f3797c0416cb23b38d407a77" diff --git a/pylintrc b/pylintrc index b7ab19c8..24f4bdb7 100644 --- a/pylintrc +++ b/pylintrc @@ -20,5 +20,6 @@ disable= line-too-long, too-many-lines, unnecessary-pass, + fixme, redefined-outer-name, duplicate-code diff --git a/pyproject.toml b/pyproject.toml index c644d3a8..9cdd4bd4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,34 +1,48 @@ -[tool.poetry] +[project] name = "seam" -version = "2.2.0" +version = "3.0.0b6" description = "SDK for the Seam API written in Python." -authors = ["Seam Labs, Inc. "] +authors = [{ name = "Seam Labs, Inc.", email = "engineering@getseam.com" }] license = "MIT" +license-files = ["LICENSE.txt"] readme = "README.rst" -homepage = "https://github.com/seamapi/python" -repository = "https://github.com/seamapi/python" -exclude = ["**/*_test.py"] -include = ["seam/py.typed"] +requires-python = ">=3.11" +dependencies = [ + "httpx>=0.23.0,<1", + "httpx-retries>=0.6.0,<1", + "svix>=1.24.0,<2", +] -[tool.poetry.dependencies] -python = "^3.10.0" -dataclasses-json = "^0.6.4" -niquests = "^3.6.4" -svix = "^1.24.0" +[project.urls] +Homepage = "https://github.com/seamapi/python" +Repository = "https://github.com/seamapi/python" -[tool.poetry.group.dev.dependencies] -black = "^24.3.0" -pylint = "^3.1.0" -pytest = "^8.1.1" -pytest-cov = "^5.0.0" -pytest-runner = "^6.0.0" -pytest-watch = "^4.2.0" -rstcheck = "^6.1.2" -mypy = "^1.17.0" +[dependency-groups] +dev = [ + "black>=26.5.1,<27", + "pylint>=4.0.7,<5", + "pytest>=9.1.1,<10", + "pytest-cov>=7.1.0,<8", + "pytest-runner>=6.0.1,<7", + "pytest-watch>=4.2.0,<5", + "rstcheck>=6.3.0,<7", + "mypy>=2.3.0,<3", +] [build-system] -requires = ["poetry>=1.8"] -build-backend = "poetry.masonry.api" +requires = ["uv_build>=0.12.0,<0.13.0"] +build-backend = "uv_build" + +[tool.uv] +required-version = ">=0.12.0,<0.13.0" + +[tool.uv.build-backend] +module-root = "" +source-exclude = ["**/*_test.py"] +wheel-exclude = ["**/*_test.py"] + +[tool.black] +target-version = ["py311"] [tool.pytest.ini_options] norecursedirs = [ diff --git a/seam/__init__.py b/seam/__init__.py index 6e62d98d..4c912626 100644 --- a/seam/__init__.py +++ b/seam/__init__.py @@ -1,8 +1,8 @@ # flake8: noqa -# type: ignore from .seam import Seam from .seam_without_workspace import SeamWithoutWorkspace +from httpx_retries import Retry from .options import SeamInvalidOptionsError from .auth import SeamInvalidTokenError from .exceptions import ( @@ -15,3 +15,10 @@ ) from .seam_webhook import SeamWebhook from svix.webhooks import WebhookVerificationError as SeamWebhookVerificationError +from .null import NULL, Null +from .url_search_params_serializer import ( + UnserializableParamError, + UrlSearchParams, + serialize_url_search_params, + update_url_search_params, +) diff --git a/seam/auth.py b/seam/auth.py index 3b30c071..6b8c09af 100644 --- a/seam/auth.py +++ b/seam/auth.py @@ -43,15 +43,18 @@ def get_auth_headers( api_key=api_key, personal_access_token=personal_access_token, ): - return get_auth_headers_for_api_key(api_key) + # The guard returns True only for a non-None api_key, which is not + # something the type checker can see through the call. + return get_auth_headers_for_api_key(api_key) # type: ignore[arg-type] if is_seam_options_with_personal_access_token( personal_access_token=personal_access_token, api_key=api_key, workspace_id=workspace_id, ): + # Likewise, the guard raises unless both of these are set. return get_auth_headers_for_personal_access_token( - personal_access_token, workspace_id + personal_access_token, workspace_id # type: ignore[arg-type] ) raise SeamInvalidOptionsError( diff --git a/seam/client.py b/seam/client.py index 644b7ad5..f78fefdf 100644 --- a/seam/client.py +++ b/seam/client.py @@ -1,28 +1,32 @@ +from collections.abc import Mapping from typing import Any, Dict, Optional -from urllib.parse import urljoin -import niquests as requests from importlib.metadata import version -from inspect import signature -from urllib3.util import Retry import abc -from .constants import DEFAULT_TIMEOUT, LTS_VERSION +import httpx +from httpx import Response +from httpx_retries import Retry, RetryTransport + +from .constants import DEFAULT_TIMEOUT from .exceptions import ( SeamHttpApiError, SeamHttpInvalidInputError, SeamHttpUnauthorizedError, ) +from .null import replace_null +from .url_search_params_serializer import serialize_url_search_params SDK_HEADERS = { "seam-sdk-name": "seamapi/python", "seam-sdk-version": version("seam"), - "seam-lts-version": LTS_VERSION, } -DEFAULT_RETRIES = Retry() - -NIQUESTS_TIMEOUT_DEFAULT = ( - signature(requests.Session.post).parameters["timeout"].default +DEFAULT_RETRIES = Retry( + total=2, + allowed_methods=["GET", "HEAD", "OPTIONS", "PUT", "DELETE"], + status_forcelist=[429, *range(500, 600)], + backoff_factor=0.12, + backoff_jitter=1 / 6, ) @@ -36,55 +40,82 @@ def request(self, method: str, url: str, *args, **kwargs): raise NotImplementedError @abc.abstractmethod - def _handle_response(self, response: requests.Response): + def _handle_response(self, response: Response): raise NotImplementedError @abc.abstractmethod - def _handle_error_response(self, response: requests.Response): + def _handle_error_response(self, response: Response): raise NotImplementedError -class SeamHttpClient(requests.Session, AbstractSeamHttpClient): +class SeamHttpClient(httpx.Client, AbstractSeamHttpClient): def __init__( self, base_url: str, auth_headers: Dict[str, str], retries: Optional[Retry] = DEFAULT_RETRIES, timeout: Optional[float] = DEFAULT_TIMEOUT, - niquests_options: Optional[Dict[str, Any]] = None, - **kwargs + httpx_options: Optional[Dict[str, Any]] = None, + **kwargs, ): - # niquests.Session mounts its adapters while initializing, so retries - # must be passed through here. Assigning self.retries afterwards leaves - # the mounted adapters on their default and the option has no effect. options = { - "retries": DEFAULT_RETRIES if retries is None else retries, + "base_url": base_url, + "timeout": timeout, **kwargs, - **(niquests_options or {}), + **(httpx_options or {}), } custom_headers = options.pop("headers", {}) + self._retry_policy = DEFAULT_RETRIES if retries is None else retries super().__init__(**options) - self.base_url = base_url - - self.timeout = timeout - headers = {**auth_headers, **custom_headers, **SDK_HEADERS} self.headers.update(headers) - def request(self, method, url, *args, **kwargs): - url = urljoin(self.base_url, url) + def _init_transport(self, *args, **kwargs) -> httpx.BaseTransport: + transport = super()._init_transport(*args, **kwargs) + + if kwargs.get("transport") is not None: + return transport + + return RetryTransport(transport=transport, retry=self._retry_policy) + + def _init_proxy_transport(self, *args, **kwargs) -> httpx.BaseTransport: + transport = super()._init_proxy_transport(*args, **kwargs) + return RetryTransport(transport=transport, retry=self._retry_policy) + + # request returns the decoded body rather than the Response that + # httpx.Client promises, so the verb helpers routed through it have to + # say so too. Without these overrides callers see the inherited Response + # type and indexing the returned payload does not type check. + def get(self, url, **kwargs) -> Any: + return self.request("GET", url, **kwargs) + + def post(self, url, data=None, json=None, **kwargs) -> Any: + return self.request("POST", url, data=data, json=json, **kwargs) - if kwargs.get("timeout", NIQUESTS_TIMEOUT_DEFAULT) == NIQUESTS_TIMEOUT_DEFAULT: - kwargs["timeout"] = self.timeout + def put(self, url, data=None, json=None, **kwargs) -> Any: + return self.request("PUT", url, data=data, json=json, **kwargs) + + def patch(self, url, data=None, json=None, **kwargs) -> Any: + return self.request("PATCH", url, data=data, json=json, **kwargs) + + def delete(self, url, json=None, **kwargs) -> Any: + return self.request("DELETE", url, json=json, **kwargs) + + def request(self, method, url, *args, **kwargs) -> Any: + if isinstance(kwargs.get("params"), Mapping): + url = with_search_params(url, kwargs.pop("params")) + + if "json" in kwargs: + kwargs["json"] = replace_null(kwargs["json"]) response = super().request(method, url, *args, **kwargs) return self._handle_response(response) - def _handle_response(self, response: requests.Response): + def _handle_response(self, response: Response): if not 200 <= response.status_code < 300: self._handle_error_response(response) @@ -93,7 +124,7 @@ def _handle_response(self, response: requests.Response): return response.text - def _handle_error_response(self, response: requests.Response): + def _handle_error_response(self, response: Response): status_code = response.status_code request_id = response.headers.get("seam-request-id") @@ -120,7 +151,16 @@ def _handle_error_response(self, response: requests.Response): raise SeamHttpApiError(error_details, status_code, request_id) -def is_api_error_response(response: requests.Response) -> bool: +def with_search_params(url: Any, params: Mapping[str, Any]) -> Any: + query = serialize_url_search_params(params) + + if not query: + return url + + return httpx.URL(url, query=query.encode()) + + +def is_api_error_response(response: Response) -> bool: try: content_type = response.headers.get("content-type", "") @@ -130,7 +170,7 @@ def is_api_error_response(response: requests.Response) -> bool: return False data = response.json() - except (ValueError, requests.exceptions.JSONDecodeError): + except ValueError: return False if not isinstance(data, dict): diff --git a/seam/constants.py b/seam/constants.py index 751b277b..ab59913d 100644 --- a/seam/constants.py +++ b/seam/constants.py @@ -1,5 +1,3 @@ -LTS_VERSION = "1.0.0" - DEFAULT_ENDPOINT = "https://connect.getseam.com" DEFAULT_TIMEOUT = 30 diff --git a/seam/exceptions.py b/seam/exceptions.py index 73fc3210..9a8367b7 100644 --- a/seam/exceptions.py +++ b/seam/exceptions.py @@ -1,4 +1,4 @@ -from typing import Any, Dict +from typing import Any, Dict, Optional from .resources import ActionAttempt @@ -15,20 +15,24 @@ class SeamHttpApiError(Exception): :vartype code: str :ivar status_code: The HTTP status code of the error response :vartype status_code: int - :ivar request_id: The unique identifier for the API request - :vartype request_id: str + :ivar request_id: The unique identifier for the API request, when the + response carried one + :vartype request_id: Optional[str] :ivar data: Additional error data, if provided by the API :vartype data: Dict[str, Any] """ - def __init__(self, error: Dict[str, Any], status_code: int, request_id: str): + def __init__( + self, error: Dict[str, Any], status_code: int, request_id: Optional[str] + ): """ :param error: Dictionary containing error details from the API response :type error: Dict[str, Any] :param status_code: HTTP status code of the error response :type status_code: int - :param request_id: Unique identifier for the API request - :type request_id: str + :param request_id: Unique identifier for the API request, when the + response carried one + :type request_id: Optional[str] """ super().__init__(error.get("message")) @@ -45,10 +49,11 @@ class SeamHttpUnauthorizedError(SeamHttpApiError): This exception is a specific type of SeamHttpApiError for 401 Unauthorized errors. """ - def __init__(self, request_id: str): + def __init__(self, request_id: Optional[str]): """ - :param request_id: Unique identifier for the API request - :type request_id: str + :param request_id: Unique identifier for the API request, when the + response carried one + :type request_id: Optional[str] """ super().__init__( @@ -66,14 +71,17 @@ class SeamHttpInvalidInputError(SeamHttpApiError): :vartype code: str """ - def __init__(self, error: Dict[str, Any], status_code: int, request_id: str): + def __init__( + self, error: Dict[str, Any], status_code: int, request_id: Optional[str] + ): """ :param error: Dictionary containing error details from the API response :type error: Dict[str, Any] :param status_code: HTTP status code of the error response :type status_code: int - :param request_id: Unique identifier for the API request - :type request_id: str + :param request_id: Unique identifier for the API request, when the + response carried one + :type request_id: Optional[str] """ super().__init__(error, status_code, request_id) @@ -120,9 +128,17 @@ def __init__(self, action_attempt: ActionAttempt): :type action_attempt: ActionAttempt """ - super().__init__(action_attempt.error.message, action_attempt) + # A failed action attempt carries an error, but reading through it + # unguarded would raise AttributeError over the actual failure if one + # ever arrives without it. + error = action_attempt.error + + super().__init__( + error.message if error is not None else "Action attempt failed", + action_attempt, + ) self.name = self.__class__.__name__ - self.code = action_attempt.error.type + self.code = error.type if error is not None else "unknown_error" class SeamActionAttemptTimeoutError(SeamActionAttemptError): @@ -136,12 +152,12 @@ class SeamActionAttemptTimeoutError(SeamActionAttemptError): :vartype name: str """ - def __init__(self, action_attempt: ActionAttempt, timeout: str): + def __init__(self, action_attempt: ActionAttempt, timeout: float): """ :param action_attempt: The ActionAttempt object associated with this error :type action_attempt: ActionAttempt :param timeout: The timeout duration in seconds - :type timeout: str + :type timeout: float """ message = f"Timed out waiting for action attempt after {timeout}s" diff --git a/seam/models.py b/seam/models.py index 98d96b26..288e2c0f 100644 --- a/seam/models.py +++ b/seam/models.py @@ -7,8 +7,6 @@ class AbstractSeam(AbstractRoutes): - lts_version: str - @abc.abstractmethod def __init__( self, @@ -66,7 +64,6 @@ def list( class AbstractSeamWithoutWorkspace: - lts_version: str wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] @abc.abstractmethod diff --git a/seam/modules/action_attempts.py b/seam/modules/action_attempts.py index 0f4da8b3..d764d3cb 100644 --- a/seam/modules/action_attempts.py +++ b/seam/modules/action_attempts.py @@ -21,8 +21,8 @@ def poll_until_ready( client: SeamHttpClient, *, action_attempt_id: str, - timeout: Optional[float] = TIMEOUT, - polling_interval: Optional[float] = POLLING_INTERVAL + timeout: float = TIMEOUT, + polling_interval: float = POLLING_INTERVAL, ) -> ActionAttempt: time_waiting = 0.0 @@ -47,7 +47,7 @@ def resolve_action_attempt( client: SeamHttpClient, *, action_attempt: ActionAttempt, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]], ) -> ActionAttempt: if wait_for_action_attempt is True: return poll_until_ready( diff --git a/seam/null.py b/seam/null.py new file mode 100644 index 00000000..66a1cbd6 --- /dev/null +++ b/seam/null.py @@ -0,0 +1,87 @@ +"""The explicit null sentinel used by request params. + +Python has a single absence value, ``None``, but the Seam API distinguishes +an omitted param from a param explicitly set to null. For example, in an +update request, an omitted param leaves the current value unchanged, +while a null param unsets the current value. + +Since sending null is rarely intended and unsetting a value cannot be undone, +``None`` means the safe option of omitting the param. +Sending null is explicit and always spelled :data:`NULL`. +""" + +from collections.abc import Mapping, Sequence +from typing import Any + + +class Null: + """Type of the :data:`NULL` sentinel.""" + + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __repr__(self): + return "NULL" + + def __bool__(self): + return False + + +NULL = Null() +"""Sentinel for a param explicitly set to null. + +Params set to this sentinel are serialized to null, +whereas params set to ``None`` are omitted: + +.. code-block:: python + + from seam import NULL, serialize_url_search_params + + serialize_url_search_params({"name": NULL, "limit": 20}) + # => 'limit=20&name=' + + serialize_url_search_params({"name": None, "limit": 20}) + # => 'limit=20' + +Use it wherever the Seam API documents null as a meaningful value, e.g., +to unset a value in an update request, or to filter by an unset value. +""" + + +def is_null(value: Any) -> bool: + """Returns whether a value is the :data:`NULL` sentinel. + + :param value: The value to check + :type value: Any + + :returns: Whether the value is the ``NULL`` sentinel""" + + return isinstance(value, Null) + + +def replace_null(value: Any) -> Any: + """Returns a copy of a value with every :data:`NULL` sentinel replaced by ``None``. + + The sentinel only distinguishes an explicit null from an omitted param + within this SDK. Once a request body is being serialized, the param is + known to be present, so the sentinel becomes the null that JSON has. + + :param value: The value to copy + :type value: Any + + :returns: The value with each ``NULL`` sentinel replaced by ``None``""" + + if is_null(value): + return None + + if isinstance(value, Mapping): + return {key: replace_null(item) for key, item in value.items()} + + if isinstance(value, Sequence) and not isinstance(value, (str, bytes)): + return [replace_null(item) for item in value] + + return value diff --git a/seam/paginator.py b/seam/paginator.py index fedb86d9..62aa3a9b 100644 --- a/seam/paginator.py +++ b/seam/paginator.py @@ -1,6 +1,7 @@ -from typing import Callable, Dict, Any, Tuple, Generator, List +from typing import Callable, Dict, Any, Optional, Tuple, Generator, List +from json import JSONDecodeError +from httpx import Response from .client import SeamHttpClient -from niquests import Response, JSONDecodeError from .pagination import Pagination @@ -17,7 +18,7 @@ def __init__( self, client: SeamHttpClient, request: Callable, - params: Dict[str, Any] = None, + params: Optional[Dict[str, Any]] = None, ): """ Initializes the Paginator. @@ -34,11 +35,11 @@ def __init__( def first_page(self) -> Tuple[List[Any], Pagination | None]: """Fetches the first page of results.""" - self.client.hooks["response"].append( + self.client.event_hooks["response"].append( lambda response: self._cache_pagination(response, self._FIRST_PAGE) ) data = self._request(**self._params) - self.client.hooks["response"].pop() + self.client.event_hooks["response"].pop() pagination = self._pagination_cache.get(self._FIRST_PAGE) @@ -56,11 +57,11 @@ def next_page( "page_cursor": next_page_cursor, } - self.client.hooks["response"].append( + self.client.event_hooks["response"].append( lambda response: self._cache_pagination(response, next_page_cursor) ) data = self._request(**params) - self.client.hooks["response"].pop() + self.client.event_hooks["response"].pop() pagination = self._pagination_cache.get(next_page_cursor) @@ -74,7 +75,7 @@ def flatten_to_list(self) -> List[Any]: if current_items: all_items.extend(current_items) - while pagination.has_next_page: + while pagination and pagination.has_next_page and pagination.next_page_cursor: current_items, pagination = self.next_page(pagination.next_page_cursor) if current_items: all_items.extend(current_items) @@ -95,6 +96,8 @@ def flatten(self) -> Generator[Any, None, None]: def _cache_pagination(self, response: Response, page_key: str) -> None: """Extracts pagination dict from response, creates Pagination object, and caches it.""" try: + # httpx response hooks fire before the response body is read. + response.read() response_json = response.json() pagination = response_json.get("pagination", {}) except JSONDecodeError: diff --git a/seam/resources/access_code.py b/seam/resources/access_code.py index 00fc66a5..7991193d 100644 --- a/seam/resources/access_code.py +++ b/seam/resources/access_code.py @@ -86,17 +86,17 @@ class DormakabaOracodeMetadata(ResourceMapping): :ivar user_level_name: Dormakaba Oracode user level name associated with this access code. """ - is_cancellable: bool - is_early_checkin_able: bool - is_extendable: bool - is_overridable: bool - site_name: str - stay_id: float - user_level_id: str - user_level_name: str + is_cancellable: Optional[bool] + is_early_checkin_able: Optional[bool] + is_extendable: Optional[bool] + is_overridable: Optional[bool] + site_name: Optional[str] + stay_id: Optional[float] + user_level_id: Optional[str] + user_level_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( is_cancellable=d.get("is_cancellable", None), is_early_checkin_able=d.get("is_early_checkin_able", None), @@ -146,31 +146,31 @@ class ModifiedFields(ResourceMapping): :ivar to: The new value of the field.""" field: str - from_: str - to: str + from_: Optional[str] + to: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( field=d.get("field", None), from_=d.get("from", None), to=d.get("to", None), ) - created_at: str + created_at: Optional[str] error_code: str - is_access_code_error: bool + is_access_code_error: Optional[bool] message: str - managed_access_code_id: str - unmanaged_access_code_id: str - change_type: str - modified_fields: List[ModifiedFields] - is_connected_account_error: bool - is_device_error: bool - is_bridge_error: bool + managed_access_code_id: Optional[str] + unmanaged_access_code_id: Optional[str] + change_type: Optional[str] + modified_fields: Optional[List[ModifiedFields]] + is_connected_account_error: Optional[bool] + is_device_error: Optional[bool] + is_bridge_error: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -216,13 +216,13 @@ class From(ResourceMapping): :ivar starts_at: Previous start time for the access code.""" - code: str - name: str - ends_at: str - starts_at: str + code: Optional[str] + name: Optional[str] + ends_at: Optional[str] + starts_at: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( code=d.get("code", None), name=d.get("name", None), @@ -242,13 +242,13 @@ class To(ResourceMapping): :ivar starts_at: New start time for the access code.""" - code: str - name: str - ends_at: str - starts_at: str + code: Optional[str] + name: Optional[str] + ends_at: Optional[str] + starts_at: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( code=d.get("code", None), name=d.get("name", None), @@ -259,12 +259,12 @@ def from_dict(cls, d: Dict[str, Any]): created_at: str message: str mutation_code: str - scheduled_at: str - from_: From - to: To + scheduled_at: Optional[str] + from_: Optional[From] + to: Optional[To] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -304,25 +304,25 @@ class ModifiedFields(ResourceMapping): :ivar to: The new value of the field.""" field: str - from_: str - to: str + from_: Optional[str] + to: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( field=d.get("field", None), from_=d.get("from", None), to=d.get("to", None), ) - created_at: str + created_at: Optional[str] message: str warning_code: str - change_type: str - modified_fields: List[ModifiedFields] + change_type: Optional[str] + modified_fields: Optional[List[ModifiedFields]] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -335,32 +335,32 @@ def from_dict(cls, d: Dict[str, Any]): ) access_code_id: str - code: str - common_code_key: str + code: Optional[str] + common_code_key: Optional[str] created_at: str device_id: str - dormakaba_oracode_metadata: DormakabaOracodeMetadata - ends_at: str + dormakaba_oracode_metadata: Optional[DormakabaOracodeMetadata] + ends_at: Optional[str] errors: List[Errors] - is_backup: bool + is_backup: Optional[bool] is_backup_access_code_available: bool is_external_modification_allowed: bool is_managed: bool is_offline_access_code: bool is_one_time_use: bool - is_scheduled_on_device: bool - is_waiting_for_code_assignment: bool - name: str + is_scheduled_on_device: Optional[bool] + is_waiting_for_code_assignment: Optional[bool] + name: Optional[str] pending_mutations: List[PendingMutations] - pulled_backup_access_code_id: str - starts_at: str + pulled_backup_access_code_id: Optional[str] + starts_at: Optional[str] status: str type: str warnings: List[Warnings] workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( access_code_id=d.get("access_code_id", None), code=d.get("code", None), diff --git a/seam/resources/access_grant.py b/seam/resources/access_grant.py index 77550335..1d442c5f 100644 --- a/seam/resources/access_grant.py +++ b/seam/resources/access_grant.py @@ -64,10 +64,10 @@ class Errors(ResourceMapping): created_at: str error_code: str message: str - missing_device_ids: List[str] + missing_device_ids: Optional[List[str]] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -101,12 +101,12 @@ class From(ResourceMapping): :ivar starts_at: Previous start time for access.""" - device_ids: List[str] - ends_at: str - starts_at: str + device_ids: Optional[List[str]] + ends_at: Optional[str] + starts_at: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_ids=d.get("device_ids", None), ends_at=d.get("ends_at", None), @@ -125,13 +125,13 @@ class To(ResourceMapping): :ivar starts_at: New start time for access.""" - common_code_key: str - device_ids: List[str] - ends_at: str - starts_at: str + common_code_key: Optional[str] + device_ids: Optional[List[str]] + ends_at: Optional[str] + starts_at: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( common_code_key=d.get("common_code_key", None), device_ids=d.get("device_ids", None), @@ -140,14 +140,14 @@ def from_dict(cls, d: Dict[str, Any]): ) created_at: str - from_: From + from_: Optional[From] message: str mutation_code: str - to: To - access_method_ids: List[str] + to: Optional[To] + access_method_ids: Optional[List[str]] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), from_=( @@ -178,15 +178,15 @@ class RequestedAccessMethods(ResourceMapping): :ivar mode: Access method mode. Supported values: ``code``, ``card``, ``mobile_key``, ``cloud_key``. """ - code: str + code: Optional[str] created_access_method_ids: List[str] created_at: str display_name: str - instant_key_max_use_count: int + instant_key_max_use_count: Optional[int] mode: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( code=d.get("code", None), created_access_method_ids=d.get("created_access_method_ids", None), @@ -234,7 +234,7 @@ class FailedDevices(ResourceMapping): message: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_id=d.get("device_id", None), error_code=d.get("error_code", None), @@ -244,15 +244,15 @@ def from_dict(cls, d: Dict[str, Any]): created_at: str message: str warning_code: str - failed_devices: List[FailedDevices] - access_method_ids: List[str] - device_id: str - new_code: str - original_code: str - reason: str + failed_devices: Optional[List[FailedDevices]] + access_method_ids: Optional[List[str]] + device_id: Optional[str] + new_code: Optional[str] + original_code: Optional[str] + reason: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -269,20 +269,20 @@ def from_dict(cls, d: Dict[str, Any]): ) access_grant_id: str - access_grant_key: str + access_grant_key: Optional[str] access_method_ids: List[str] - client_session_token: str + client_session_token: Optional[str] created_at: str - customization_profile_id: str + customization_profile_id: Optional[str] display_name: str - ends_at: str + ends_at: Optional[str] errors: List[Errors] - instant_key_url: str + instant_key_url: Optional[str] location_ids: List[str] - name: str + name: Optional[str] pending_mutations: List[PendingMutations] requested_access_methods: List[RequestedAccessMethods] - reservation_key: str + reservation_key: Optional[str] space_ids: List[str] starts_at: str user_identity_id: str @@ -290,7 +290,7 @@ def from_dict(cls, d: Dict[str, Any]): workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( access_grant_id=d.get("access_grant_id", None), access_grant_key=d.get("access_grant_key", None), diff --git a/seam/resources/access_method.py b/seam/resources/access_method.py index f8d11823..d7614ada 100644 --- a/seam/resources/access_method.py +++ b/seam/resources/access_method.py @@ -60,7 +60,7 @@ class Errors(ResourceMapping): message: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -91,12 +91,12 @@ class From(ResourceMapping): :ivar starts_at: Previous start time for access.""" - device_ids: List[str] - ends_at: str - starts_at: str + device_ids: Optional[List[str]] + ends_at: Optional[str] + starts_at: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_ids=d.get("device_ids", None), ends_at=d.get("ends_at", None), @@ -113,12 +113,12 @@ class To(ResourceMapping): :ivar starts_at: New start time for access.""" - device_ids: List[str] - ends_at: str - starts_at: str + device_ids: Optional[List[str]] + ends_at: Optional[str] + starts_at: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_ids=d.get("device_ids", None), ends_at=d.get("ends_at", None), @@ -126,13 +126,13 @@ def from_dict(cls, d: Dict[str, Any]): ) created_at: str - from_: From + from_: Optional[From] message: str mutation_code: str - to: To + to: Optional[To] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), from_=( @@ -161,10 +161,10 @@ class Warnings(ResourceMapping): created_at: str message: str warning_code: str - original_access_method_id: str + original_access_method_id: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -173,26 +173,26 @@ def from_dict(cls, d: Dict[str, Any]): ) access_method_id: str - client_session_token: str - code: str + client_session_token: Optional[str] + code: Optional[str] created_at: str - customization_profile_id: str + customization_profile_id: Optional[str] display_name: str errors: List[Errors] - instant_key_url: str - is_assignment_required: bool - is_encoding_required: bool + instant_key_url: Optional[str] + is_assignment_required: Optional[bool] + is_encoding_required: Optional[bool] is_issued: bool - is_ready_for_assignment: bool - is_ready_for_encoding: bool - issued_at: str + is_ready_for_assignment: Optional[bool] + is_ready_for_encoding: Optional[bool] + issued_at: Optional[str] mode: str pending_mutations: List[PendingMutations] warnings: List[Warnings] workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( access_method_id=d.get("access_method_id", None), client_session_token=d.get("client_session_token", None), diff --git a/seam/resources/acs_access_group.py b/seam/resources/acs_access_group.py index 24692f63..ca32527b 100644 --- a/seam/resources/acs_access_group.py +++ b/seam/resources/acs_access_group.py @@ -53,11 +53,11 @@ class AccessSchedule(ResourceMapping): :ivar starts_at: Date and time at which the user's access starts, in `ISO 8601 `_ format. """ - ends_at: str + ends_at: Optional[str] starts_at: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( ends_at=d.get("ends_at", None), starts_at=d.get("starts_at", None), @@ -79,7 +79,7 @@ class Errors(ResourceMapping): message: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -119,14 +119,14 @@ class From(ResourceMapping): :ivar acs_entrance_id: Old entrance ID.""" - name: str - ends_at: str - starts_at: str - acs_user_id: str - acs_entrance_id: str + name: Optional[str] + ends_at: Optional[str] + starts_at: Optional[str] + acs_user_id: Optional[str] + acs_entrance_id: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( name=d.get("name", None), ends_at=d.get("ends_at", None), @@ -149,14 +149,14 @@ class To(ResourceMapping): :ivar acs_entrance_id: New entrance ID.""" - name: str - ends_at: str - starts_at: str - acs_user_id: str - acs_entrance_id: str + name: Optional[str] + ends_at: Optional[str] + starts_at: Optional[str] + acs_user_id: Optional[str] + acs_entrance_id: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( name=d.get("name", None), ends_at=d.get("ends_at", None), @@ -168,13 +168,13 @@ def from_dict(cls, d: Dict[str, Any]): created_at: str message: str mutation_code: str - from_: From - to: To - acs_user_id: str - variant: str + from_: Optional[From] + to: Optional[To] + acs_user_id: Optional[str] + variant: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -205,7 +205,7 @@ class Warnings(ResourceMapping): warning_code: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -214,7 +214,7 @@ def from_dict(cls, d: Dict[str, Any]): access_group_type: str access_group_type_display_name: str - access_schedule: AccessSchedule + access_schedule: Optional[AccessSchedule] acs_access_group_id: str acs_system_id: str connected_account_id: str @@ -230,7 +230,7 @@ def from_dict(cls, d: Dict[str, Any]): workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( access_group_type=d.get("access_group_type", None), access_group_type_display_name=d.get( diff --git a/seam/resources/acs_credential.py b/seam/resources/acs_credential.py index 561bded7..b8f09b54 100644 --- a/seam/resources/acs_credential.py +++ b/seam/resources/acs_credential.py @@ -24,6 +24,8 @@ class AcsCredential: :ivar acs_user_id: ID of the `ACS user `_ to whom the `credential `_ belongs. + :ivar akiles_metadata: Akiles-specific metadata for the `credential `_. + :ivar assa_abloy_vostio_metadata: Vostio-specific metadata for the `credential `_. :ivar card_number: Number of the card associated with the `credential `_. @@ -71,6 +73,20 @@ class AcsCredential: :ivar workspace_id: ID of the workspace that contains the `credential `_. """ + @dataclass + class AkilesMetadata(ResourceMapping): + """Akiles-specific metadata for the `credential `_. + + :ivar member_pin_id: ID of the Akiles member PIN.""" + + member_pin_id: Optional[str] + + @classmethod + def from_dict(cls, d: Any): + return cls( + member_pin_id=d.get("member_pin_id", None), + ) + @dataclass class AssaAbloyVostioMetadata(ResourceMapping): """Vostio-specific metadata for the `credential `_. @@ -88,15 +104,15 @@ class AssaAbloyVostioMetadata(ResourceMapping): :ivar override_guest_acs_entrance_ids: IDs of the guest entrances to override in the Vostio access system. """ - auto_join: bool - door_names: List[str] - endpoint_id: str - key_id: str - key_issuing_request_id: str - override_guest_acs_entrance_ids: List[str] + auto_join: Optional[bool] + door_names: Optional[List[str]] + endpoint_id: Optional[str] + key_id: Optional[str] + key_issuing_request_id: Optional[str] + override_guest_acs_entrance_ids: Optional[List[str]] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( auto_join=d.get("auto_join", None), door_names=d.get("door_names", None), @@ -123,7 +139,7 @@ class Errors(ResourceMapping): message: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -151,17 +167,17 @@ class VisionlineMetadata(ResourceMapping): :ivar joiner_acs_credential_ids: IDs of the credentials to which you want to join. """ - auto_join: bool - card_function_type: str - card_id: str - common_acs_entrance_ids: List[str] - credential_id: str - guest_acs_entrance_ids: List[str] - is_valid: bool - joiner_acs_credential_ids: List[str] + auto_join: Optional[bool] + card_function_type: Optional[str] + card_id: Optional[str] + common_acs_entrance_ids: Optional[List[str]] + credential_id: Optional[str] + guest_acs_entrance_ids: Optional[List[str]] + is_valid: Optional[bool] + joiner_acs_credential_ids: Optional[List[str]] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( auto_join=d.get("auto_join", None), card_function_type=d.get("card_function_type", None), @@ -182,57 +198,71 @@ class Warnings(ResourceMapping): :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + + :ivar new_code: The PIN code that was assigned instead. + + :ivar original_code: The originally requested PIN code that could not be used. """ created_at: str message: str warning_code: str + new_code: Optional[str] + original_code: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), warning_code=d.get("warning_code", None), + new_code=d.get("new_code", None), + original_code=d.get("original_code", None), ) access_method: str acs_credential_id: str - acs_credential_pool_id: str + acs_credential_pool_id: Optional[str] acs_system_id: str - acs_user_id: str - assa_abloy_vostio_metadata: AssaAbloyVostioMetadata - card_number: str - code: str + acs_user_id: Optional[str] + akiles_metadata: Optional[AkilesMetadata] + assa_abloy_vostio_metadata: Optional[AssaAbloyVostioMetadata] + card_number: Optional[str] + code: Optional[str] connected_account_id: str created_at: str display_name: str - ends_at: str + ends_at: Optional[str] errors: List[Errors] - external_type: str - external_type_display_name: str - is_issued: bool - is_latest_desired_state_synced_with_provider: bool + external_type: Optional[str] + external_type_display_name: Optional[str] + is_issued: Optional[bool] + is_latest_desired_state_synced_with_provider: Optional[bool] is_managed: bool - is_multi_phone_sync_credential: bool - is_one_time_use: bool - issued_at: str - latest_desired_state_synced_with_provider_at: str - parent_acs_credential_id: str - starts_at: str - user_identity_id: str - visionline_metadata: VisionlineMetadata + is_multi_phone_sync_credential: Optional[bool] + is_one_time_use: Optional[bool] + issued_at: Optional[str] + latest_desired_state_synced_with_provider_at: Optional[str] + parent_acs_credential_id: Optional[str] + starts_at: Optional[str] + user_identity_id: Optional[str] + visionline_metadata: Optional[VisionlineMetadata] warnings: List[Warnings] workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( access_method=d.get("access_method", None), acs_credential_id=d.get("acs_credential_id", None), acs_credential_pool_id=d.get("acs_credential_pool_id", None), acs_system_id=d.get("acs_system_id", None), acs_user_id=d.get("acs_user_id", None), + akiles_metadata=( + cls.AkilesMetadata.from_dict(d.get("akiles_metadata")) + if d.get("akiles_metadata") is not None + else None + ), assa_abloy_vostio_metadata=( cls.AssaAbloyVostioMetadata.from_dict( d.get("assa_abloy_vostio_metadata") diff --git a/seam/resources/acs_encoder.py b/seam/resources/acs_encoder.py index 88bc5a75..c1e4f936 100644 --- a/seam/resources/acs_encoder.py +++ b/seam/resources/acs_encoder.py @@ -52,7 +52,7 @@ class Errors(ResourceMapping): message: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -68,7 +68,7 @@ def from_dict(cls, d: Dict[str, Any]): workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( acs_encoder_id=d.get("acs_encoder_id", None), acs_system_id=d.get("acs_system_id", None), diff --git a/seam/resources/acs_entrance.py b/seam/resources/acs_entrance.py index d3c40fee..491e6677 100644 --- a/seam/resources/acs_entrance.py +++ b/seam/resources/acs_entrance.py @@ -81,23 +81,23 @@ class Actions(ResourceMapping): :ivar name: Name of the gadget action.""" - id: str - name: str + id: Optional[str] + name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( id=d.get("id", None), name=d.get("name", None), ) - actions: List[Actions] - gadget_id: str - site_id: str - site_name: str + actions: Optional[List[Actions]] + gadget_id: Optional[str] + site_id: Optional[str] + site_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( actions=[cls.Actions.from_dict(i) for i in d.get("actions") or []], gadget_id=d.get("gadget_id", None), @@ -120,14 +120,14 @@ class AssaAbloyVostioMetadata(ResourceMapping): :ivar stand_open: Indicates whether keys are allowed to set the door in stand open mode in the Vostio access system. """ - door_name: str - door_number: float - door_type: str - pms_id: str - stand_open: bool + door_name: Optional[str] + door_number: Optional[float] + door_type: Optional[str] + pms_id: Optional[str] + stand_open: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( door_name=d.get("door_name", None), door_number=d.get("door_number", None), @@ -154,16 +154,16 @@ class AvigilonAltaMetadata(ResourceMapping): :ivar zone_name: Zone name for an Avigilon Alta system.""" - entry_name: str - entry_relays_total_count: float - org_name: str - site_id: float - site_name: str - zone_id: float - zone_name: str + entry_name: Optional[str] + entry_relays_total_count: Optional[float] + org_name: Optional[str] + site_id: Optional[float] + site_name: Optional[str] + zone_id: Optional[float] + zone_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( entry_name=d.get("entry_name", None), entry_relays_total_count=d.get("entry_relays_total_count", None), @@ -184,12 +184,12 @@ class BrivoMetadata(ResourceMapping): :ivar site_name: Name of the site that the access point belongs to.""" - access_point_id: str - site_id: float - site_name: str + access_point_id: Optional[str] + site_id: Optional[float] + site_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( access_point_id=d.get("access_point_id", None), site_id=d.get("site_id", None), @@ -203,10 +203,10 @@ class DormakabaAmbianceMetadata(ResourceMapping): :ivar access_point_name: Name of the access point in the dormakaba Ambiance access system. """ - access_point_name: str + access_point_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( access_point_name=d.get("access_point_name", None), ) @@ -218,10 +218,10 @@ class DormakabaCommunityMetadata(ResourceMapping): :ivar access_point_profile: Type of access point profile in the dormakaba Community access system. """ - access_point_profile: str + access_point_profile: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( access_point_profile=d.get("access_point_profile", None), ) @@ -242,7 +242,7 @@ class Errors(ResourceMapping): message: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -259,12 +259,12 @@ class HotekMetadata(ResourceMapping): :ivar room_number: Room number of the entrance.""" - common_area_name: str - common_area_number: str - room_number: str + common_area_name: Optional[str] + common_area_number: Optional[str] + room_number: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( common_area_name=d.get("common_area_name", None), common_area_number=d.get("common_area_number", None), @@ -283,13 +283,13 @@ class LatchMetadata(ResourceMapping): :ivar is_connected: Indicates whether the entrance is connected.""" - accessibility_type: str - door_name: str - door_type: str - is_connected: bool + accessibility_type: Optional[str] + door_name: Optional[str] + door_type: Optional[str] + is_connected: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( accessibility_type=d.get("accessibility_type", None), door_name=d.get("door_name", None), @@ -317,17 +317,17 @@ class SaltoKsMetadata(ResourceMapping): :ivar privacy_mode: Indicates whether privacy mode is enabled for the lock.""" - battery_level: str - door_name: str - intrusion_alarm: bool - left_open_alarm: bool - lock_type: str - locked_state: str - online: bool - privacy_mode: bool + battery_level: Optional[str] + door_name: Optional[str] + intrusion_alarm: Optional[bool] + left_open_alarm: Optional[bool] + lock_type: Optional[str] + locked_state: Optional[str] + online: Optional[bool] + privacy_mode: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( battery_level=d.get("battery_level", None), door_name=d.get("door_name", None), @@ -355,15 +355,15 @@ class SaltoSpaceMetadata(ResourceMapping): :ivar room_name: Name of the room in the Salto Space access system.""" - audit_on_keys: bool - door_description: str - door_id: str - door_name: str - room_description: str - room_name: str + audit_on_keys: Optional[bool] + door_description: Optional[str] + door_id: Optional[str] + door_name: Optional[str] + room_description: Optional[str] + room_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( audit_on_keys=d.get("audit_on_keys", None), door_description=d.get("door_description", None), @@ -392,11 +392,11 @@ class Profiles(ResourceMapping): :ivar visionline_door_profile_type: Door profile type in the Visionline access system. """ - visionline_door_profile_id: str - visionline_door_profile_type: str + visionline_door_profile_id: Optional[str] + visionline_door_profile_type: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( visionline_door_profile_id=d.get( "visionline_door_profile_id", None @@ -406,12 +406,12 @@ def from_dict(cls, d: Dict[str, Any]): ), ) - door_category: str - door_name: str - profiles: List[Profiles] + door_category: Optional[str] + door_name: Optional[str] + profiles: Optional[List[Profiles]] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( door_category=d.get("door_category", None), door_name=d.get("door_name", None), @@ -434,7 +434,7 @@ class Warnings(ResourceMapping): warning_code: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -443,32 +443,32 @@ def from_dict(cls, d: Dict[str, Any]): acs_entrance_id: str acs_system_id: str - akiles_metadata: AkilesMetadata - assa_abloy_vostio_metadata: AssaAbloyVostioMetadata - avigilon_alta_metadata: AvigilonAltaMetadata - brivo_metadata: BrivoMetadata - can_belong_to_reservation: bool - can_unlock_with_card: bool - can_unlock_with_cloud_key: bool - can_unlock_with_code: bool - can_unlock_with_mobile_key: bool + akiles_metadata: Optional[AkilesMetadata] + assa_abloy_vostio_metadata: Optional[AssaAbloyVostioMetadata] + avigilon_alta_metadata: Optional[AvigilonAltaMetadata] + brivo_metadata: Optional[BrivoMetadata] + can_belong_to_reservation: Optional[bool] + can_unlock_with_card: Optional[bool] + can_unlock_with_cloud_key: Optional[bool] + can_unlock_with_code: Optional[bool] + can_unlock_with_mobile_key: Optional[bool] connected_account_id: str created_at: str display_name: str - dormakaba_ambiance_metadata: DormakabaAmbianceMetadata - dormakaba_community_metadata: DormakabaCommunityMetadata + dormakaba_ambiance_metadata: Optional[DormakabaAmbianceMetadata] + dormakaba_community_metadata: Optional[DormakabaCommunityMetadata] errors: List[Errors] - hotek_metadata: HotekMetadata - is_locked: bool - latch_metadata: LatchMetadata - salto_ks_metadata: SaltoKsMetadata - salto_space_metadata: SaltoSpaceMetadata + hotek_metadata: Optional[HotekMetadata] + is_locked: Optional[bool] + latch_metadata: Optional[LatchMetadata] + salto_ks_metadata: Optional[SaltoKsMetadata] + salto_space_metadata: Optional[SaltoSpaceMetadata] space_ids: List[str] - visionline_metadata: VisionlineMetadata + visionline_metadata: Optional[VisionlineMetadata] warnings: List[Warnings] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( acs_entrance_id=d.get("acs_entrance_id", None), acs_system_id=d.get("acs_system_id", None), diff --git a/seam/resources/acs_system.py b/seam/resources/acs_system.py index 933eaecd..976a5070 100644 --- a/seam/resources/acs_system.py +++ b/seam/resources/acs_system.py @@ -69,10 +69,10 @@ class Errors(ResourceMapping): created_at: str error_code: str message: str - is_bridge_error: bool + is_bridge_error: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -87,10 +87,10 @@ class Location(ResourceMapping): :ivar time_zone: Time zone in which the `access control system `_ is located. """ - time_zone: str + time_zone: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( time_zone=d.get("time_zone", None), ) @@ -106,12 +106,12 @@ class VisionlineMetadata(ResourceMapping): :ivar system_id: Unique ID assigned by the ASSA ABLOY licensing team that identifies each hotel in your credential manager. """ - lan_address: str - mobile_access_uuid: str - system_id: str + lan_address: Optional[str] + mobile_access_uuid: Optional[str] + system_id: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( lan_address=d.get("lan_address", None), mobile_access_uuid=d.get("mobile_access_uuid", None), @@ -133,10 +133,10 @@ class Warnings(ResourceMapping): created_at: str message: str warning_code: str - misconfigured_acs_entrance_ids: List[str] + misconfigured_acs_entrance_ids: Optional[List[str]] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -146,29 +146,29 @@ def from_dict(cls, d: Dict[str, Any]): ), ) - acs_access_group_count: float + acs_access_group_count: Optional[float] acs_system_id: str - acs_user_count: float + acs_user_count: Optional[float] connected_account_id: str connected_account_ids: List[str] created_at: str - default_credential_manager_acs_system_id: str + default_credential_manager_acs_system_id: Optional[str] errors: List[Errors] - external_type: str - external_type_display_name: str + external_type: Optional[str] + external_type_display_name: Optional[str] image_alt_text: str image_url: str is_credential_manager: bool - location: Location + location: Optional[Location] name: str - system_type: str - system_type_display_name: str - visionline_metadata: VisionlineMetadata + system_type: Optional[str] + system_type_display_name: Optional[str] + visionline_metadata: Optional[VisionlineMetadata] warnings: List[Warnings] workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( acs_access_group_count=d.get("acs_access_group_count", None), acs_system_id=d.get("acs_system_id", None), diff --git a/seam/resources/acs_user.py b/seam/resources/acs_user.py index faef5196..3c794ab2 100644 --- a/seam/resources/acs_user.py +++ b/seam/resources/acs_user.py @@ -72,11 +72,11 @@ class AccessSchedule(ResourceMapping): :ivar starts_at: Date and time at which the user's access starts, in `ISO 8601 `_ format. """ - ends_at: str + ends_at: Optional[str] starts_at: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( ends_at=d.get("ends_at", None), starts_at=d.get("starts_at", None), @@ -98,7 +98,7 @@ class Errors(ResourceMapping): message: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -146,17 +146,17 @@ class From(ResourceMapping): :ivar acs_credential_id: Previous credential ID.""" - email_address: str - full_name: str - phone_number: str - ends_at: str - starts_at: str - is_suspended: bool - acs_access_group_id: str - acs_credential_id: str + email_address: Optional[str] + full_name: Optional[str] + phone_number: Optional[str] + ends_at: Optional[str] + starts_at: Optional[str] + is_suspended: Optional[bool] + acs_access_group_id: Optional[str] + acs_credential_id: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( email_address=d.get("email_address", None), full_name=d.get("full_name", None), @@ -188,17 +188,17 @@ class To(ResourceMapping): :ivar acs_credential_id: New credential ID.""" - email_address: str - full_name: str - phone_number: str - ends_at: str - starts_at: str - is_suspended: bool - acs_access_group_id: str - acs_credential_id: str + email_address: Optional[str] + full_name: Optional[str] + phone_number: Optional[str] + ends_at: Optional[str] + starts_at: Optional[str] + is_suspended: Optional[bool] + acs_access_group_id: Optional[str] + acs_credential_id: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( email_address=d.get("email_address", None), full_name=d.get("full_name", None), @@ -213,14 +213,14 @@ def from_dict(cls, d: Dict[str, Any]): created_at: str message: str mutation_code: str - scheduled_at: str - from_: From - to: To - acs_access_group_id: str - variant: str + scheduled_at: Optional[str] + from_: Optional[From] + to: Optional[To] + acs_access_group_id: Optional[str] + variant: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -243,10 +243,10 @@ class SaltoKsMetadata(ResourceMapping): :ivar is_subscribed: Indicates whether the user holds an active subscription slot on the Salto KS site. Only subscribed users can unlock doors and count against the site's user-subscription limit. A user may not be subscribed because their access schedule has not started or has ended, the site has reached its subscription limit, or they were manually unsubscribed. This is distinct from ``is_suspended``, which reflects whether the user has been explicitly blocked. """ - is_subscribed: bool + is_subscribed: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( is_subscribed=d.get("is_subscribed", None), ) @@ -259,11 +259,11 @@ class SaltoSpaceMetadata(ResourceMapping): :ivar user_id: User ID in the Salto Space access system.""" - audit_openings: bool - user_id: str + audit_openings: Optional[bool] + user_id: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( audit_openings=d.get("audit_openings", None), user_id=d.get("user_id", None), @@ -284,41 +284,41 @@ class Warnings(ResourceMapping): warning_code: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), warning_code=d.get("warning_code", None), ) - access_schedule: AccessSchedule + access_schedule: Optional[AccessSchedule] acs_system_id: str acs_user_id: str connected_account_id: str created_at: str display_name: str - email: str - email_address: str + email: Optional[str] + email_address: Optional[str] errors: List[Errors] - external_type: str - external_type_display_name: str - full_name: str - hid_acs_system_id: str + external_type: Optional[str] + external_type_display_name: Optional[str] + full_name: Optional[str] + hid_acs_system_id: Optional[str] is_managed: bool - is_suspended: bool - pending_mutations: List[PendingMutations] - phone_number: str - salto_ks_metadata: SaltoKsMetadata - salto_space_metadata: SaltoSpaceMetadata - user_identity_email_address: str - user_identity_full_name: str - user_identity_id: str - user_identity_phone_number: str + is_suspended: Optional[bool] + pending_mutations: Optional[List[PendingMutations]] + phone_number: Optional[str] + salto_ks_metadata: Optional[SaltoKsMetadata] + salto_space_metadata: Optional[SaltoSpaceMetadata] + user_identity_email_address: Optional[str] + user_identity_full_name: Optional[str] + user_identity_id: Optional[str] + user_identity_phone_number: Optional[str] warnings: List[Warnings] workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( access_schedule=( cls.AccessSchedule.from_dict(d.get("access_schedule")) diff --git a/seam/resources/action_attempt.py b/seam/resources/action_attempt.py index 21e4a97a..0c0ff07e 100644 --- a/seam/resources/action_attempt.py +++ b/seam/resources/action_attempt.py @@ -30,7 +30,7 @@ class Error(ResourceMapping): type: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( message=d.get("message", None), type=d.get("type", None), @@ -58,6 +58,8 @@ class Result(ResourceMapping): :ivar acs_user_id: ID of the `ACS user `_ to whom the `credential `_ belongs. + :ivar akiles_metadata: Akiles-specific metadata for the `credential `_. + :ivar assa_abloy_vostio_metadata: Vostio-specific metadata for the `credential `_. :ivar card_number: Number of the card associated with the `credential `_. @@ -121,7 +123,10 @@ class Result(ResourceMapping): :ivar mode: Access method mode. Supported values: ``code``, ``card``, ``mobile_key``, ``cloud_key``. :ivar pending_mutations: Pending mutations for the `access method `_. Indicates operations that are in progress. - """ + + :ivar access_code: + + :ivar noise_threshold:""" @dataclass class AcsCredentialOnEncoder(ResourceMapping): @@ -169,21 +174,21 @@ class VisionlineMetadata(ResourceMapping): :ivar pending_auto_update: Indicates whether the card associated with the `credential `_ is pending auto-update. """ - cancelled: bool - card_format: str - card_holder: str - card_id: str - common_acs_entrance_ids: List[str] - discarded: bool - expired: bool - guest_acs_entrance_ids: List[str] - number_of_issued_cards: float - overridden: bool - overwritten: bool - pending_auto_update: bool + cancelled: Optional[bool] + card_format: Optional[str] + card_holder: Optional[str] + card_id: Optional[str] + common_acs_entrance_ids: Optional[List[str]] + discarded: Optional[bool] + expired: Optional[bool] + guest_acs_entrance_ids: Optional[List[str]] + number_of_issued_cards: Optional[float] + overridden: Optional[bool] + overwritten: Optional[bool] + pending_auto_update: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( cancelled=d.get("cancelled", None), card_format=d.get("card_format", None), @@ -199,15 +204,15 @@ def from_dict(cls, d: Dict[str, Any]): pending_auto_update=d.get("pending_auto_update", None), ) - card_number: str - created_at: str - ends_at: str - is_issued: bool - starts_at: str - visionline_metadata: VisionlineMetadata + card_number: Optional[str] + created_at: Optional[str] + ends_at: Optional[str] + is_issued: Optional[bool] + starts_at: Optional[str] + visionline_metadata: Optional[VisionlineMetadata] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( card_number=d.get("card_number", None), created_at=d.get("created_at", None), @@ -235,6 +240,8 @@ class AcsCredentialOnSeam(ResourceMapping): :ivar acs_user_id: ID of the `ACS user `_ to whom the `credential `_ belongs. + :ivar akiles_metadata: Akiles-specific metadata for the `credential `_. + :ivar assa_abloy_vostio_metadata: Vostio-specific metadata for the `credential `_. :ivar card_number: Number of the card associated with the `credential `_. @@ -282,6 +289,20 @@ class AcsCredentialOnSeam(ResourceMapping): :ivar workspace_id: ID of the workspace that contains the `credential `_. """ + @dataclass + class AkilesMetadata(ResourceMapping): + """Akiles-specific metadata for the `credential `_. + + :ivar member_pin_id: ID of the Akiles member PIN.""" + + member_pin_id: Optional[str] + + @classmethod + def from_dict(cls, d: Any): + return cls( + member_pin_id=d.get("member_pin_id", None), + ) + @dataclass class AssaAbloyVostioMetadata(ResourceMapping): """Vostio-specific metadata for the `credential `_. @@ -299,15 +320,15 @@ class AssaAbloyVostioMetadata(ResourceMapping): :ivar override_guest_acs_entrance_ids: IDs of the guest entrances to override in the Vostio access system. """ - auto_join: bool - door_names: List[str] - endpoint_id: str - key_id: str - key_issuing_request_id: str - override_guest_acs_entrance_ids: List[str] + auto_join: Optional[bool] + door_names: Optional[List[str]] + endpoint_id: Optional[str] + key_id: Optional[str] + key_issuing_request_id: Optional[str] + override_guest_acs_entrance_ids: Optional[List[str]] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( auto_join=d.get("auto_join", None), door_names=d.get("door_names", None), @@ -334,7 +355,7 @@ class Errors(ResourceMapping): message: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -362,17 +383,17 @@ class VisionlineMetadata(ResourceMapping): :ivar joiner_acs_credential_ids: IDs of the credentials to which you want to join. """ - auto_join: bool - card_function_type: str - card_id: str - common_acs_entrance_ids: List[str] - credential_id: str - guest_acs_entrance_ids: List[str] - is_valid: bool - joiner_acs_credential_ids: List[str] + auto_join: Optional[bool] + card_function_type: Optional[str] + card_id: Optional[str] + common_acs_entrance_ids: Optional[List[str]] + credential_id: Optional[str] + guest_acs_entrance_ids: Optional[List[str]] + is_valid: Optional[bool] + joiner_acs_credential_ids: Optional[List[str]] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( auto_join=d.get("auto_join", None), card_function_type=d.get("card_function_type", None), @@ -395,57 +416,71 @@ class Warnings(ResourceMapping): :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. :ivar warning_code: Unique identifier of the type of warning. Enables quick recognition and categorization of the issue. + + :ivar new_code: The PIN code that was assigned instead. + + :ivar original_code: The originally requested PIN code that could not be used. """ created_at: str message: str warning_code: str + new_code: Optional[str] + original_code: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), warning_code=d.get("warning_code", None), + new_code=d.get("new_code", None), + original_code=d.get("original_code", None), ) access_method: str acs_credential_id: str - acs_credential_pool_id: str + acs_credential_pool_id: Optional[str] acs_system_id: str - acs_user_id: str - assa_abloy_vostio_metadata: AssaAbloyVostioMetadata - card_number: str - code: str + acs_user_id: Optional[str] + akiles_metadata: Optional[AkilesMetadata] + assa_abloy_vostio_metadata: Optional[AssaAbloyVostioMetadata] + card_number: Optional[str] + code: Optional[str] connected_account_id: str created_at: str display_name: str - ends_at: str + ends_at: Optional[str] errors: List[Errors] - external_type: str - external_type_display_name: str - is_issued: bool - is_latest_desired_state_synced_with_provider: bool + external_type: Optional[str] + external_type_display_name: Optional[str] + is_issued: Optional[bool] + is_latest_desired_state_synced_with_provider: Optional[bool] is_managed: bool - is_multi_phone_sync_credential: bool - is_one_time_use: bool - issued_at: str - latest_desired_state_synced_with_provider_at: str - parent_acs_credential_id: str - starts_at: str - user_identity_id: str - visionline_metadata: VisionlineMetadata + is_multi_phone_sync_credential: Optional[bool] + is_one_time_use: Optional[bool] + issued_at: Optional[str] + latest_desired_state_synced_with_provider_at: Optional[str] + parent_acs_credential_id: Optional[str] + starts_at: Optional[str] + user_identity_id: Optional[str] + visionline_metadata: Optional[VisionlineMetadata] warnings: List[Warnings] workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( access_method=d.get("access_method", None), acs_credential_id=d.get("acs_credential_id", None), acs_credential_pool_id=d.get("acs_credential_pool_id", None), acs_system_id=d.get("acs_system_id", None), acs_user_id=d.get("acs_user_id", None), + akiles_metadata=( + cls.AkilesMetadata.from_dict(d.get("akiles_metadata")) + if d.get("akiles_metadata") is not None + else None + ), assa_abloy_vostio_metadata=( cls.AssaAbloyVostioMetadata.from_dict( d.get("assa_abloy_vostio_metadata") @@ -503,25 +538,47 @@ class Warnings(ResourceMapping): :ivar message: Detailed description of the warning. Provides insights into the issue and potentially how to rectify it. + :ivar new_code: The PIN code that was assigned instead. + + :ivar original_code: The originally requested PIN code that could not be used. + :ivar original_access_method_id: ID of the original access method from which this backup access method was split, if applicable. """ warning_code: str - warning_message: str - created_at: str - message: str - original_access_method_id: str + warning_message: Optional[str] + created_at: Optional[str] + message: Optional[str] + new_code: Optional[str] + original_code: Optional[str] + original_access_method_id: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( warning_code=d.get("warning_code", None), warning_message=d.get("warning_message", None), created_at=d.get("created_at", None), message=d.get("message", None), + new_code=d.get("new_code", None), + original_code=d.get("original_code", None), original_access_method_id=d.get("original_access_method_id", None), ) + @dataclass + class AkilesMetadata(ResourceMapping): + """Akiles-specific metadata for the `credential `_. + + :ivar member_pin_id: ID of the Akiles member PIN.""" + + member_pin_id: Optional[str] + + @classmethod + def from_dict(cls, d: Any): + return cls( + member_pin_id=d.get("member_pin_id", None), + ) + @dataclass class AssaAbloyVostioMetadata(ResourceMapping): """Vostio-specific metadata for the `credential `_. @@ -539,15 +596,15 @@ class AssaAbloyVostioMetadata(ResourceMapping): :ivar override_guest_acs_entrance_ids: IDs of the guest entrances to override in the Vostio access system. """ - auto_join: bool - door_names: List[str] - endpoint_id: str - key_id: str - key_issuing_request_id: str - override_guest_acs_entrance_ids: List[str] + auto_join: Optional[bool] + door_names: Optional[List[str]] + endpoint_id: Optional[str] + key_id: Optional[str] + key_issuing_request_id: Optional[str] + override_guest_acs_entrance_ids: Optional[List[str]] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( auto_join=d.get("auto_join", None), door_names=d.get("door_names", None), @@ -575,7 +632,7 @@ class Errors(ResourceMapping): message: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -603,17 +660,17 @@ class VisionlineMetadata(ResourceMapping): :ivar joiner_acs_credential_ids: IDs of the credentials to which you want to join. """ - auto_join: bool - card_function_type: str - card_id: str - common_acs_entrance_ids: List[str] - credential_id: str - guest_acs_entrance_ids: List[str] - is_valid: bool - joiner_acs_credential_ids: List[str] + auto_join: Optional[bool] + card_function_type: Optional[str] + card_id: Optional[str] + common_acs_entrance_ids: Optional[List[str]] + credential_id: Optional[str] + guest_acs_entrance_ids: Optional[List[str]] + is_valid: Optional[bool] + joiner_acs_credential_ids: Optional[List[str]] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( auto_join=d.get("auto_join", None), card_function_type=d.get("card_function_type", None), @@ -647,11 +704,11 @@ class From(ResourceMapping): :ivar starts_at: Previous start time for access.""" - ends_at: str - starts_at: str + ends_at: Optional[str] + starts_at: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( ends_at=d.get("ends_at", None), starts_at=d.get("starts_at", None), @@ -665,24 +722,24 @@ class To(ResourceMapping): :ivar starts_at: New start time for access.""" - ends_at: str - starts_at: str + ends_at: Optional[str] + starts_at: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( ends_at=d.get("ends_at", None), starts_at=d.get("starts_at", None), ) created_at: str - from_: From + from_: Optional[From] message: str mutation_code: str - to: To + to: Optional[To] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), from_=( @@ -699,50 +756,53 @@ def from_dict(cls, d: Dict[str, Any]): ), ) - was_confirmed_by_device: bool - acs_credential_on_encoder: AcsCredentialOnEncoder - acs_credential_on_seam: AcsCredentialOnSeam - warnings: List[Warnings] - access_method: str - acs_credential_id: str - acs_credential_pool_id: str - acs_system_id: str - acs_user_id: str - assa_abloy_vostio_metadata: AssaAbloyVostioMetadata - card_number: str - code: str - connected_account_id: str - created_at: str - display_name: str - ends_at: str - errors: List[Errors] - external_type: str - external_type_display_name: str - is_issued: bool - is_latest_desired_state_synced_with_provider: bool - is_managed: bool - is_multi_phone_sync_credential: bool - is_one_time_use: bool - issued_at: str - latest_desired_state_synced_with_provider_at: str - parent_acs_credential_id: str - starts_at: str - user_identity_id: str - visionline_metadata: VisionlineMetadata - workspace_id: str - access_method_id: str - client_session_token: str - customization_profile_id: str - instant_key_url: str - is_assignment_required: bool - is_encoding_required: bool - is_ready_for_assignment: bool - is_ready_for_encoding: bool - mode: str - pending_mutations: List[PendingMutations] + was_confirmed_by_device: Optional[bool] + acs_credential_on_encoder: Optional[AcsCredentialOnEncoder] + acs_credential_on_seam: Optional[AcsCredentialOnSeam] + warnings: Optional[List[Warnings]] + access_method: Optional[str] + acs_credential_id: Optional[str] + acs_credential_pool_id: Optional[str] + acs_system_id: Optional[str] + acs_user_id: Optional[str] + akiles_metadata: Optional[AkilesMetadata] + assa_abloy_vostio_metadata: Optional[AssaAbloyVostioMetadata] + card_number: Optional[str] + code: Optional[str] + connected_account_id: Optional[str] + created_at: Optional[str] + display_name: Optional[str] + ends_at: Optional[str] + errors: Optional[List[Errors]] + external_type: Optional[str] + external_type_display_name: Optional[str] + is_issued: Optional[bool] + is_latest_desired_state_synced_with_provider: Optional[bool] + is_managed: Optional[bool] + is_multi_phone_sync_credential: Optional[bool] + is_one_time_use: Optional[bool] + issued_at: Optional[str] + latest_desired_state_synced_with_provider_at: Optional[str] + parent_acs_credential_id: Optional[str] + starts_at: Optional[str] + user_identity_id: Optional[str] + visionline_metadata: Optional[VisionlineMetadata] + workspace_id: Optional[str] + access_method_id: Optional[str] + client_session_token: Optional[str] + customization_profile_id: Optional[str] + instant_key_url: Optional[str] + is_assignment_required: Optional[bool] + is_encoding_required: Optional[bool] + is_ready_for_assignment: Optional[bool] + is_ready_for_encoding: Optional[bool] + mode: Optional[str] + pending_mutations: Optional[List[PendingMutations]] + access_code: Optional[Dict[str, Any]] + noise_threshold: Optional[Dict[str, Any]] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( was_confirmed_by_device=d.get("was_confirmed_by_device", None), acs_credential_on_encoder=( @@ -763,6 +823,11 @@ def from_dict(cls, d: Dict[str, Any]): acs_credential_pool_id=d.get("acs_credential_pool_id", None), acs_system_id=d.get("acs_system_id", None), acs_user_id=d.get("acs_user_id", None), + akiles_metadata=( + cls.AkilesMetadata.from_dict(d.get("akiles_metadata")) + if d.get("akiles_metadata") is not None + else None + ), assa_abloy_vostio_metadata=( cls.AssaAbloyVostioMetadata.from_dict( d.get("assa_abloy_vostio_metadata") @@ -814,16 +879,18 @@ def from_dict(cls, d: Dict[str, Any]): cls.PendingMutations.from_dict(i) for i in d.get("pending_mutations") or [] ], + access_code=DeepAttrDict(d.get("access_code", None)), + noise_threshold=DeepAttrDict(d.get("noise_threshold", None)), ) action_attempt_id: str action_type: str - error: Error - result: Result + error: Optional[Error] + result: Optional[Result] status: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( action_attempt_id=d.get("action_attempt_id", None), action_type=d.get("action_type", None), diff --git a/seam/resources/batch.py b/seam/resources/batch.py index 1313eb3e..02ae27b1 100644 --- a/seam/resources/batch.py +++ b/seam/resources/batch.py @@ -134,33 +134,33 @@ class Batch: :ivar workspaces: Represents a Seam `workspace `_. A workspace is a top-level entity that encompasses all other resources below it, such as devices, connected accounts, and Connect Webviews. Seam provides two types of workspaces. A `sandbox workspace `_ is a special type of workspace designed for testing code. Sandbox workspaces offer test device accounts and virtual devices that you can connect and control. This ability to work with virtual devices is quite handy because it removes the need to own physical devices from multiple brands. To connect real devices and systems to Seam, use a `production workspace `_. """ - access_codes: List[Dict[str, Any]] - access_grants: List[Dict[str, Any]] - access_methods: List[Dict[str, Any]] - acs_access_groups: List[Dict[str, Any]] - acs_credentials: List[Dict[str, Any]] - acs_encoders: List[Dict[str, Any]] - acs_entrances: List[Dict[str, Any]] - acs_systems: List[Dict[str, Any]] - acs_users: List[Dict[str, Any]] - action_attempts: List[Dict[str, Any]] - client_sessions: List[Dict[str, Any]] - connect_webviews: List[Dict[str, Any]] - connected_accounts: List[Dict[str, Any]] - devices: List[Dict[str, Any]] - events: List[Dict[str, Any]] - instant_keys: List[Dict[str, Any]] - noise_thresholds: List[Dict[str, Any]] - spaces: List[Dict[str, Any]] - thermostat_daily_programs: List[Dict[str, Any]] - thermostat_schedules: List[Dict[str, Any]] - unmanaged_access_codes: List[Dict[str, Any]] - unmanaged_devices: List[Dict[str, Any]] - user_identities: List[Dict[str, Any]] - workspaces: List[Dict[str, Any]] + access_codes: Optional[List[Dict[str, Any]]] + access_grants: Optional[List[Dict[str, Any]]] + access_methods: Optional[List[Dict[str, Any]]] + acs_access_groups: Optional[List[Dict[str, Any]]] + acs_credentials: Optional[List[Dict[str, Any]]] + acs_encoders: Optional[List[Dict[str, Any]]] + acs_entrances: Optional[List[Dict[str, Any]]] + acs_systems: Optional[List[Dict[str, Any]]] + acs_users: Optional[List[Dict[str, Any]]] + action_attempts: Optional[List[Dict[str, Any]]] + client_sessions: Optional[List[Dict[str, Any]]] + connect_webviews: Optional[List[Dict[str, Any]]] + connected_accounts: Optional[List[Dict[str, Any]]] + devices: Optional[List[Dict[str, Any]]] + events: Optional[List[Dict[str, Any]]] + instant_keys: Optional[List[Dict[str, Any]]] + noise_thresholds: Optional[List[Dict[str, Any]]] + spaces: Optional[List[Dict[str, Any]]] + thermostat_daily_programs: Optional[List[Dict[str, Any]]] + thermostat_schedules: Optional[List[Dict[str, Any]]] + unmanaged_access_codes: Optional[List[Dict[str, Any]]] + unmanaged_devices: Optional[List[Dict[str, Any]]] + user_identities: Optional[List[Dict[str, Any]]] + workspaces: Optional[List[Dict[str, Any]]] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( access_codes=d.get("access_codes", None), access_grants=d.get("access_grants", None), diff --git a/seam/resources/client_session.py b/seam/resources/client_session.py index fb289c16..c4ec26b1 100644 --- a/seam/resources/client_session.py +++ b/seam/resources/client_session.py @@ -44,17 +44,17 @@ class ClientSession: connect_webview_ids: List[str] connected_account_ids: List[str] created_at: str - customer_key: str + customer_key: Optional[str] device_count: float expires_at: str token: str - user_identifier_key: str - user_identity_id: str + user_identifier_key: Optional[str] + user_identity_id: Optional[str] user_identity_ids: List[str] workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( client_session_id=d.get("client_session_id", None), connect_webview_ids=d.get("connect_webview_ids", None), diff --git a/seam/resources/connect_webview.py b/seam/resources/connect_webview.py index fc948390..582894a2 100644 --- a/seam/resources/connect_webview.py +++ b/seam/resources/connect_webview.py @@ -59,25 +59,25 @@ class ConnectWebview: accepted_capabilities: List[str] accepted_providers: List[str] any_provider_allowed: bool - authorized_at: str + authorized_at: Optional[str] automatically_manage_new_devices: bool connect_webview_id: str - connected_account_id: str + connected_account_id: Optional[str] created_at: str custom_metadata: Dict[str, Any] - custom_redirect_failure_url: str - custom_redirect_url: str - customer_key: str + custom_redirect_failure_url: Optional[str] + custom_redirect_url: Optional[str] + customer_key: Optional[str] device_selection_mode: str login_successful: bool - selected_provider: str + selected_provider: Optional[str] status: str url: str wait_for_device_creation: bool workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( accepted_capabilities=d.get("accepted_capabilities", None), accepted_providers=d.get("accepted_providers", None), diff --git a/seam/resources/connected_account.py b/seam/resources/connected_account.py index 9cceb75d..c27e4e7f 100644 --- a/seam/resources/connected_account.py +++ b/seam/resources/connected_account.py @@ -81,13 +81,13 @@ class Sites(ResourceMapping): :ivar subscribed_site_user_count: Count of subscribed site users for a Salto site associated with the connected account that has an error. """ - site_id: str - site_name: str - site_user_subscription_limit: int - subscribed_site_user_count: int + site_id: Optional[str] + site_name: Optional[str] + site_user_subscription_limit: Optional[int] + subscribed_site_user_count: Optional[int] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( site_id=d.get("site_id", None), site_name=d.get("site_name", None), @@ -99,23 +99,23 @@ def from_dict(cls, d: Dict[str, Any]): ), ) - sites: List[Sites] + sites: Optional[List[Sites]] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( sites=[cls.Sites.from_dict(i) for i in d.get("sites") or []], ) created_at: str error_code: str - is_bridge_error: bool - is_connected_account_error: bool + is_bridge_error: Optional[bool] + is_connected_account_error: Optional[bool] message: str - salto_ks_metadata: SaltoKsMetadata + salto_ks_metadata: Optional[SaltoKsMetadata] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -144,14 +144,14 @@ class UserIdentifier(ResourceMapping): :ivar username: Username of the user identifier associated with the connected account. """ - api_url: str - email: str - exclusive: bool - phone: str - username: str + api_url: Optional[str] + email: Optional[str] + exclusive: Optional[bool] + phone: Optional[str] + username: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( api_url=d.get("api_url", None), email=d.get("email", None), @@ -193,13 +193,13 @@ class Sites(ResourceMapping): :ivar subscribed_site_user_count: Count of subscribed site users for a Salto site associated with the connected account that has a warning. """ - site_id: str - site_name: str - site_user_subscription_limit: int - subscribed_site_user_count: int + site_id: Optional[str] + site_name: Optional[str] + site_user_subscription_limit: Optional[int] + subscribed_site_user_count: Optional[int] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( site_id=d.get("site_id", None), site_name=d.get("site_name", None), @@ -211,10 +211,10 @@ def from_dict(cls, d: Dict[str, Any]): ), ) - sites: List[Sites] + sites: Optional[List[Sites]] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( sites=[cls.Sites.from_dict(i) for i in d.get("sites") or []], ) @@ -222,10 +222,10 @@ def from_dict(cls, d: Dict[str, Any]): created_at: str message: str warning_code: str - salto_ks_metadata: SaltoKsMetadata + salto_ks_metadata: Optional[SaltoKsMetadata] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -238,26 +238,26 @@ def from_dict(cls, d: Dict[str, Any]): ) accepted_capabilities: List[str] - account_type: str + account_type: Optional[str] account_type_display_name: str automatically_manage_new_devices: bool connected_account_id: str - created_at: str + created_at: Optional[str] custom_metadata: Dict[str, Any] - customer_key: str - default_checkin_time: str - default_checkout_time: str + customer_key: Optional[str] + default_checkin_time: Optional[str] + default_checkout_time: Optional[str] display_name: str errors: List[Errors] - ical_feed_origin: str - ical_url: str - image_url: str - time_zone: str - user_identifier: UserIdentifier + ical_feed_origin: Optional[str] + ical_url: Optional[str] + image_url: Optional[str] + time_zone: Optional[str] + user_identifier: Optional[UserIdentifier] warnings: List[Warnings] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( accepted_capabilities=d.get("accepted_capabilities", None), account_type=d.get("account_type", None), diff --git a/seam/resources/customer_portal.py b/seam/resources/customer_portal.py index 120cbc7a..d49625df 100644 --- a/seam/resources/customer_portal.py +++ b/seam/resources/customer_portal.py @@ -29,7 +29,7 @@ class CustomerPortal: workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), customer_key=d.get("customer_key", None), diff --git a/seam/resources/device.py b/seam/resources/device.py index 8d86fe69..827983a7 100644 --- a/seam/resources/device.py +++ b/seam/resources/device.py @@ -95,11 +95,11 @@ class DeviceManufacturer(ResourceMapping): """ display_name: str - image_url: str + image_url: Optional[str] manufacturer: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( display_name=d.get("display_name", None), image_url=d.get("image_url", None), @@ -121,11 +121,11 @@ class DeviceProvider(ResourceMapping): device_provider_name: str display_name: str - image_url: str + image_url: Optional[str] provider_category: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_provider_name=d.get("device_provider_name", None), display_name=d.get("display_name", None), @@ -152,13 +152,13 @@ class Errors(ResourceMapping): created_at: str error_code: str - is_connected_account_error: bool - is_device_error: bool + is_connected_account_error: Optional[bool] + is_device_error: Optional[bool] message: str - is_bridge_error: bool + is_bridge_error: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -174,19 +174,23 @@ class Location(ResourceMapping): :ivar location_name: Name of the device location. + :ivar room_name: Name of the room within the device location, when the provider reports one. + :ivar time_zone: Time zone of the device location. :ivar timezone: Deprecated: Use ``time_zone`` instead. Time zone of the device location. """ - location_name: str - time_zone: str - timezone: str + location_name: Optional[str] + room_name: Optional[str] + time_zone: Optional[str] + timezone: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( location_name=d.get("location_name", None), + room_name=d.get("room_name", None), time_zone=d.get("time_zone", None), timezone=d.get("timezone", None), ) @@ -287,7 +291,7 @@ class Properties(ResourceMapping): :ivar salto_ks_metadata: Metadata for a Salto KS device. - :ivar salto_metadata: Deprecated: Use ``salto_ks_metadata `` instead. Metada for a Salto device. + :ivar salto_metadata: Deprecated: Use ``salto_ks_metadata`` instead. Metada for a Salto device. :ivar schlage_metadata: Metadata for a Schlage device. @@ -311,6 +315,8 @@ class Properties(ResourceMapping): :ivar wyze_metadata: Metadata for a Wyze device. + :ivar yacan_metadata: Metadata for a Yacan device. + :ivar auto_lock_delay_seconds: The delay in seconds before the lock automatically locks after being unlocked. :ivar auto_lock_enabled: Indicates whether automatic locking is enabled. @@ -422,16 +428,16 @@ class Battery(ResourceMapping): level: float @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( level=d.get("level", None), ) - battery: Battery + battery: Optional[Battery] is_connected: bool @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( battery=( cls.Battery.from_dict(d.get("battery")) @@ -451,7 +457,7 @@ class Appearance(ResourceMapping): name: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( name=d.get("name", None), ) @@ -469,7 +475,7 @@ class Battery(ResourceMapping): status: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( level=d.get("level", None), status=d.get("status", None), @@ -494,16 +500,16 @@ class Model(ResourceMapping): :ivar online_access_codes_supported: Deprecated: use device.can_program_online_access_codes. """ - accessory_keypad_supported: bool - can_connect_accessory_keypad: bool + accessory_keypad_supported: Optional[bool] + can_connect_accessory_keypad: Optional[bool] display_name: str - has_built_in_keypad: bool + has_built_in_keypad: Optional[bool] manufacturer_display_name: str - offline_access_codes_supported: bool - online_access_codes_supported: bool + offline_access_codes_supported: Optional[bool] + online_access_codes_supported: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( accessory_keypad_supported=d.get( "accessory_keypad_supported", None @@ -539,21 +545,21 @@ class Endpoints(ResourceMapping): :ivar is_active: Indicated whether the endpoint is active.""" - endpoint_id: str - is_active: bool + endpoint_id: Optional[str] + is_active: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( endpoint_id=d.get("endpoint_id", None), is_active=d.get("is_active", None), ) - endpoints: List[Endpoints] - has_active_endpoint: bool + endpoints: Optional[List[Endpoints]] + has_active_endpoint: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( endpoints=[ cls.Endpoints.from_dict(i) for i in d.get("endpoints") or [] @@ -568,10 +574,10 @@ class SaltoSpaceCredentialServiceMetadata(ResourceMapping): :ivar has_active_phone: Indicates whether the credential service has an active associated phone. """ - has_active_phone: bool + has_active_phone: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( has_active_phone=d.get("has_active_phone", None), ) @@ -588,13 +594,13 @@ class AkilesMetadata(ResourceMapping): :ivar product_name: Product name for an Akiles device.""" - _member_group_id: str - gadget_id: str - gadget_name: str - product_name: str + _member_group_id: Optional[str] + gadget_id: Optional[str] + gadget_name: Optional[str] + product_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( _member_group_id=d.get("_member_group_id", None), gadget_id=d.get("gadget_id", None), @@ -622,17 +628,17 @@ class AqaraMetadata(ResourceMapping): :ivar time_zone: Time zone reported for an Aqara device (e.g. GMT-07:00).""" - device_name: str - did: str - firmware_version: str - model: str - model_type: float - parent_did: str - position_id: str - time_zone: str + device_name: Optional[str] + did: Optional[str] + firmware_version: Optional[str] + model: Optional[str] + model_type: Optional[float] + parent_did: Optional[str] + position_id: Optional[str] + time_zone: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_name=d.get("device_name", None), did=d.get("did", None), @@ -650,10 +656,10 @@ class AssaAbloyVostioMetadata(ResourceMapping): :ivar encoder_name: Encoder name for an ASSA ABLOY Vostio system.""" - encoder_name: str + encoder_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( encoder_name=d.get("encoder_name", None), ) @@ -676,16 +682,16 @@ class AugustMetadata(ResourceMapping): :ivar model: Model for an August device.""" - has_keypad: bool - house_id: str - house_name: str - keypad_battery_level: str - lock_id: str - lock_name: str - model: str + has_keypad: Optional[bool] + house_id: Optional[str] + house_name: Optional[str] + keypad_battery_level: Optional[str] + lock_id: Optional[str] + lock_name: Optional[str] + model: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( has_keypad=d.get("has_keypad", None), house_id=d.get("house_id", None), @@ -714,16 +720,16 @@ class AvigilonAltaMetadata(ResourceMapping): :ivar zone_name: Zone name for an Avigilon Alta system.""" - entry_name: str - entry_relays_total_count: float - org_name: str - site_id: float - site_name: str - zone_id: float - zone_name: str + entry_name: Optional[str] + entry_relays_total_count: Optional[float] + org_name: Optional[str] + site_id: Optional[float] + site_name: Optional[str] + zone_id: Optional[float] + zone_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( entry_name=d.get("entry_name", None), entry_relays_total_count=d.get("entry_relays_total_count", None), @@ -742,11 +748,11 @@ class BrivoMetadata(ResourceMapping): :ivar device_name: Device name for a Brivo device.""" - activation_enabled: bool - device_name: str + activation_enabled: Optional[bool] + device_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( activation_enabled=d.get("activation_enabled", None), device_name=d.get("device_name", None), @@ -762,12 +768,12 @@ class ControlbywebMetadata(ResourceMapping): :ivar relay_name: Relay name for a ControlByWeb device.""" - device_id: str - device_name: str - relay_name: str + device_id: Optional[str] + device_name: Optional[str] + relay_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_id=d.get("device_id", None), device_name=d.get("device_name", None), @@ -794,16 +800,6 @@ class DormakabaOracodeMetadata(ResourceMapping): :ivar site_name: Site name for a dormakaba Oracode device.""" - @dataclass - class DeviceId(ResourceMapping): - """Device ID for a dormakaba Oracode device.""" - - @classmethod - def from_dict(cls, d: Dict[str, Any]): - # This shape documents no properties, so there is nothing to read. - # pylint: disable=unused-argument - return cls() - @dataclass class PredefinedTimeSlots(ResourceMapping): """Predefined time slots for a dormakaba Oracode device. @@ -828,19 +824,19 @@ class PredefinedTimeSlots(ResourceMapping): :ivar prefix: Prefix for a time slot for a dormakaba Oracode device.""" - check_in_time: str - check_out_time: str - dormakaba_oracode_user_level_id: str - dormakaba_oracode_user_level_prefix: float - is_24_hour: bool - is_biweekly_mode: bool - is_master: bool - is_one_shot: bool - name: str - prefix: float + check_in_time: Optional[str] + check_out_time: Optional[str] + dormakaba_oracode_user_level_id: Optional[str] + dormakaba_oracode_user_level_prefix: Optional[float] + is_24_hour: Optional[bool] + is_biweekly_mode: Optional[bool] + is_master: Optional[bool] + is_one_shot: Optional[bool] + name: Optional[str] + prefix: Optional[float] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( check_in_time=d.get("check_in_time", None), check_out_time=d.get("check_out_time", None), @@ -858,23 +854,19 @@ def from_dict(cls, d: Dict[str, Any]): prefix=d.get("prefix", None), ) - device_id: DeviceId - door_id: float - door_is_wireless: bool - door_name: str - iana_timezone: str - predefined_time_slots: List[PredefinedTimeSlots] - site_id: float - site_name: str + device_id: Optional[str] + door_id: Optional[float] + door_is_wireless: Optional[bool] + door_name: Optional[str] + iana_timezone: Optional[str] + predefined_time_slots: Optional[List[PredefinedTimeSlots]] + site_id: Optional[float] + site_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( - device_id=( - cls.DeviceId.from_dict(d.get("device_id")) - if d.get("device_id") is not None - else None - ), + device_id=d.get("device_id", None), door_id=d.get("door_id", None), door_is_wireless=d.get("door_is_wireless", None), door_name=d.get("door_name", None), @@ -895,11 +887,11 @@ class EcobeeMetadata(ResourceMapping): :ivar ecobee_device_id: Device ID for an ecobee device.""" - device_name: str - ecobee_device_id: str + device_name: Optional[str] + ecobee_device_id: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_name=d.get("device_name", None), ecobee_device_id=d.get("ecobee_device_id", None), @@ -916,12 +908,12 @@ class FourSuitesMetadata(ResourceMapping): :ivar reclose_delay_in_seconds: Reclose delay, in seconds, for a 4SUITES device. """ - device_id: float - device_name: str - reclose_delay_in_seconds: float + device_id: Optional[float] + device_name: Optional[str] + reclose_delay_in_seconds: Optional[float] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_id=d.get("device_id", None), device_name=d.get("device_name", None), @@ -936,11 +928,11 @@ class GenieMetadata(ResourceMapping): :ivar door_name: Door name for a Genie device.""" - device_name: str - door_name: str + device_name: Optional[str] + door_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_name=d.get("device_name", None), door_name=d.get("door_name", None), @@ -955,11 +947,11 @@ class HoneywellResideoMetadata(ResourceMapping): :ivar honeywell_resideo_device_id: Device ID for a Honeywell Resideo device. """ - device_name: str - honeywell_resideo_device_id: str + device_name: Optional[str] + honeywell_resideo_device_id: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_name=d.get("device_name", None), honeywell_resideo_device_id=d.get( @@ -977,12 +969,12 @@ class IglooMetadata(ResourceMapping): :ivar model: Model for an igloo device.""" - bridge_id: str - device_id: str - model: str + bridge_id: Optional[str] + device_id: Optional[str] + model: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( bridge_id=d.get("bridge_id", None), device_id=d.get("device_id", None), @@ -1005,15 +997,15 @@ class IgloohomeMetadata(ResourceMapping): :ivar keypad_id: Keypad ID for an igloohome device.""" - bridge_id: str - bridge_name: str - device_id: str - device_name: str - is_accessory_keypad_linked_to_bridge: bool - keypad_id: str + bridge_id: Optional[str] + bridge_name: Optional[str] + device_id: Optional[str] + device_name: Optional[str] + is_accessory_keypad_linked_to_bridge: Optional[bool] + keypad_id: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( bridge_id=d.get("bridge_id", None), bridge_name=d.get("bridge_name", None), @@ -1071,30 +1063,30 @@ class KeynestMetadata(ResourceMapping): :ivar subscription_plan: Subscription plan for a KeyNest device.""" - address: str - current_or_last_store_id: float - current_status: str - current_user_company: str - current_user_email: str - current_user_name: str - current_user_phone_number: str - default_office_id: float - device_name: str - fob_id: float - handover_method: str - has_photo: bool - is_quadient_locker: bool - key_id: str - key_notes: str - keynest_app_user: str - last_movement: str - property_id: str - property_postcode: str - status_type: str - subscription_plan: str + address: Optional[str] + current_or_last_store_id: Optional[float] + current_status: Optional[str] + current_user_company: Optional[str] + current_user_email: Optional[str] + current_user_name: Optional[str] + current_user_phone_number: Optional[str] + default_office_id: Optional[float] + device_name: Optional[str] + fob_id: Optional[float] + handover_method: Optional[str] + has_photo: Optional[bool] + is_quadient_locker: Optional[bool] + key_id: Optional[str] + key_notes: Optional[str] + keynest_app_user: Optional[str] + last_movement: Optional[str] + property_id: Optional[str] + property_postcode: Optional[str] + status_type: Optional[str] + subscription_plan: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( address=d.get("address", None), current_or_last_store_id=d.get("current_or_last_store_id", None), @@ -1131,13 +1123,13 @@ class KisiMetadata(ResourceMapping): :ivar place_name: Place name for a Kisi device.""" - description: str - lock_id: float - lock_name: str - place_name: str + description: Optional[str] + lock_id: Optional[float] + lock_name: Optional[str] + place_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( description=d.get("description", None), lock_id=d.get("lock_id", None), @@ -1164,16 +1156,16 @@ class KorelockMetadata(ResourceMapping): :ivar wifi_signal_strength: WiFi signal strength (0-1) for a Korelock device. """ - device_id: str - device_name: str - firmware_version: str - location_id: str - model_code: str - serial_number: str - wifi_signal_strength: float + device_id: Optional[str] + device_name: Optional[str] + firmware_version: Optional[str] + location_id: Optional[str] + model_code: Optional[str] + serial_number: Optional[str] + wifi_signal_strength: Optional[float] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_id=d.get("device_id", None), device_name=d.get("device_name", None), @@ -1194,12 +1186,12 @@ class KwiksetMetadata(ResourceMapping): :ivar model_number: Model number for a Kwikset device.""" - device_id: str - device_name: str - model_number: str + device_id: Optional[str] + device_name: Optional[str] + model_number: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_id=d.get("device_id", None), device_name=d.get("device_name", None), @@ -1216,12 +1208,12 @@ class LocklyMetadata(ResourceMapping): :ivar model: Model for a Lockly device.""" - device_id: str - device_name: str - model: str + device_id: Optional[str] + device_name: Optional[str] + model: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_id=d.get("device_id", None), device_name=d.get("device_name", None), @@ -1261,11 +1253,11 @@ class AccelerometerZ(ResourceMapping): :ivar value: Value of latest accelerometer Z-axis reading for a Minut device. """ - time: str - value: float + time: Optional[str] + value: Optional[float] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( time=d.get("time", None), value=d.get("value", None), @@ -1279,11 +1271,11 @@ class Humidity(ResourceMapping): :ivar value: Value of latest humidity reading for a Minut device.""" - time: str - value: float + time: Optional[str] + value: Optional[float] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( time=d.get("time", None), value=d.get("value", None), @@ -1297,11 +1289,11 @@ class Pressure(ResourceMapping): :ivar value: Value of latest pressure reading for a Minut device.""" - time: str - value: float + time: Optional[str] + value: Optional[float] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( time=d.get("time", None), value=d.get("value", None), @@ -1315,11 +1307,11 @@ class Sound(ResourceMapping): :ivar value: Value of latest sound reading for a Minut device.""" - time: str - value: float + time: Optional[str] + value: Optional[float] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( time=d.get("time", None), value=d.get("value", None), @@ -1334,24 +1326,24 @@ class Temperature(ResourceMapping): :ivar value: Value of latest temperature reading for a Minut device. """ - time: str - value: float + time: Optional[str] + value: Optional[float] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( time=d.get("time", None), value=d.get("value", None), ) - accelerometer_z: AccelerometerZ - humidity: Humidity - pressure: Pressure - sound: Sound - temperature: Temperature + accelerometer_z: Optional[AccelerometerZ] + humidity: Optional[Humidity] + pressure: Optional[Pressure] + sound: Optional[Sound] + temperature: Optional[Temperature] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( accelerometer_z=( cls.AccelerometerZ.from_dict(d.get("accelerometer_z")) @@ -1380,12 +1372,12 @@ def from_dict(cls, d: Dict[str, Any]): ), ) - device_id: str - device_name: str - latest_sensor_values: LatestSensorValues + device_id: Optional[str] + device_name: Optional[str] + latest_sensor_values: Optional[LatestSensorValues] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_id=d.get("device_id", None), device_name=d.get("device_name", None), @@ -1406,20 +1398,29 @@ class NestMetadata(ResourceMapping): :ivar display_name: Display name for a Google Nest device. - :ivar nest_device_id: Device ID for a Google Nest device.""" + :ivar nest_device_id: Device ID for a Google Nest device. - device_custom_name: str - device_name: str - display_name: str - nest_device_id: str + :ivar nest_structure_id: ID of the Google Nest structure containing the device. + + :ivar structure_name: Name of the Google Nest structure containing the device. The device owner sets this value. + """ + + device_custom_name: Optional[str] + device_name: Optional[str] + display_name: Optional[str] + nest_device_id: Optional[str] + nest_structure_id: Optional[str] + structure_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_custom_name=d.get("device_custom_name", None), device_name=d.get("device_name", None), display_name=d.get("display_name", None), nest_device_id=d.get("nest_device_id", None), + nest_structure_id=d.get("nest_structure_id", None), + structure_name=d.get("structure_name", None), ) @dataclass @@ -1437,14 +1438,14 @@ class NoiseawareMetadata(ResourceMapping): :ivar noise_level_nrs: Noise level, expressed as a Noise Risk Score (NRS), for a NoiseAware device. """ - device_id: str - device_model: str - device_name: str - noise_level_decibel: float - noise_level_nrs: float + device_id: Optional[str] + device_model: Optional[str] + device_name: Optional[str] + noise_level_decibel: Optional[float] + noise_level_nrs: Optional[float] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_id=d.get("device_id", None), device_model=d.get("device_model", None), @@ -1468,14 +1469,14 @@ class NukiMetadata(ResourceMapping): :ivar keypad_paired: Indicates whether the keypad is paired for a Nuki device. """ - device_id: str - device_name: str - keypad_2_paired: bool - keypad_battery_critical: bool - keypad_paired: bool + device_id: Optional[str] + device_name: Optional[str] + keypad_2_paired: Optional[bool] + keypad_battery_critical: Optional[bool] + keypad_paired: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_id=d.get("device_id", None), device_name=d.get("device_name", None), @@ -1503,16 +1504,16 @@ class OmnitecMetadata(ResourceMapping): :ivar timezone_raw_offset_ms: Static UTC offset of the Omnitec lock in milliseconds. Does not account for DST. """ - has_gateway: bool - lock_alias: str - lock_id: float - lock_mac: str - lock_name: str - time_zone: str - timezone_raw_offset_ms: float + has_gateway: Optional[bool] + lock_alias: Optional[str] + lock_id: Optional[float] + lock_mac: Optional[str] + lock_name: Optional[str] + time_zone: Optional[str] + timezone_raw_offset_ms: Optional[float] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( has_gateway=d.get("has_gateway", None), lock_alias=d.get("lock_alias", None), @@ -1531,11 +1532,11 @@ class RingMetadata(ResourceMapping): :ivar device_name: Device name for a Ring device.""" - device_id: str - device_name: str + device_id: Optional[str] + device_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_id=d.get("device_id", None), device_name=d.get("device_name", None), @@ -1564,18 +1565,18 @@ class SaltoKsMetadata(ResourceMapping): :ivar site_name: Site name for the Salto KS site to which the device belongs. """ - battery_level: str - customer_reference: str - has_custom_pin_subscription: bool - lock_id: str - lock_type: str - locked_state: str - model: str - site_id: str - site_name: str + battery_level: Optional[str] + customer_reference: Optional[str] + has_custom_pin_subscription: Optional[bool] + lock_id: Optional[str] + lock_type: Optional[str] + locked_state: Optional[str] + model: Optional[str] + site_id: Optional[str] + site_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( battery_level=d.get("battery_level", None), customer_reference=d.get("customer_reference", None), @@ -1611,17 +1612,17 @@ class SaltoMetadata(ResourceMapping): :ivar site_name: Site name for the Salto KS site to which the device belongs. """ - battery_level: str - customer_reference: str - lock_id: str - lock_type: str - locked_state: str - model: str - site_id: str - site_name: str + battery_level: Optional[str] + customer_reference: Optional[str] + lock_id: Optional[str] + lock_type: Optional[str] + locked_state: Optional[str] + model: Optional[str] + site_id: Optional[str] + site_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( battery_level=d.get("battery_level", None), customer_reference=d.get("customer_reference", None), @@ -1643,12 +1644,12 @@ class SchlageMetadata(ResourceMapping): :ivar model: Model for a Schlage device.""" - device_id: str - device_name: str - model: str + device_id: Optional[str] + device_name: Optional[str] + model: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_id=d.get("device_id", None), device_name=d.get("device_name", None), @@ -1665,12 +1666,12 @@ class SeamBridgeMetadata(ResourceMapping): :ivar unlock_method: Unlock method for Seam Bridge.""" - device_num: float - name: str - unlock_method: str + device_num: Optional[float] + name: Optional[str] + unlock_method: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_num=d.get("device_num", None), name=d.get("name", None), @@ -1687,21 +1688,27 @@ class SensiMetadata(ResourceMapping): :ivar dual_setpoints_not_supported: Set to true when the device does not support the /dual-setpoints API endpoint. + :ivar enforced_setpoint_range_celsius: Enforced setpoint range in Celsius for a Sensi device, derived from an OutOfRange API error. + :ivar product_type: Product type for a Sensi device.""" - device_id: str - device_name: str - dual_setpoints_not_supported: bool - product_type: str + device_id: Optional[str] + device_name: Optional[str] + dual_setpoints_not_supported: Optional[bool] + enforced_setpoint_range_celsius: Optional[List[float]] + product_type: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_id=d.get("device_id", None), device_name=d.get("device_name", None), dual_setpoints_not_supported=d.get( "dual_setpoints_not_supported", None ), + enforced_setpoint_range_celsius=d.get( + "enforced_setpoint_range_celsius", None + ), product_type=d.get("product_type", None), ) @@ -1717,13 +1724,13 @@ class SmartthingsMetadata(ResourceMapping): :ivar model: Model for a SmartThings device.""" - device_id: str - device_name: str - location_id: str - model: str + device_id: Optional[str] + device_name: Optional[str] + location_id: Optional[str] + model: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_id=d.get("device_id", None), device_name=d.get("device_name", None), @@ -1739,11 +1746,11 @@ class TadoMetadata(ResourceMapping): :ivar serial_no: Serial number for a tado° device.""" - device_type: str - serial_no: str + device_type: Optional[str] + serial_no: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_type=d.get("device_type", None), serial_no=d.get("serial_no", None), @@ -1767,16 +1774,16 @@ class TedeeMetadata(ResourceMapping): :ivar serial_number: Serial number for a Tedee device.""" - bridge_id: float - bridge_name: str - device_id: float - device_model: str - device_name: str - keypad_id: float - serial_number: str + bridge_id: Optional[float] + bridge_name: Optional[str] + device_id: Optional[float] + device_model: Optional[str] + device_name: Optional[str] + keypad_id: Optional[float] + serial_number: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( bridge_id=d.get("bridge_id", None), bridge_name=d.get("bridge_name", None), @@ -1823,16 +1830,16 @@ class Features(ResourceMapping): :ivar wifi: Indicates whether a TTLock device supports Wi-Fi.""" - auto_lock_time_config: bool - incomplete_keyboard_passcode: bool - lock_command: bool - passcode: bool - passcode_management: bool - unlock_via_gateway: bool - wifi: bool + auto_lock_time_config: Optional[bool] + incomplete_keyboard_passcode: Optional[bool] + lock_command: Optional[bool] + passcode: Optional[bool] + passcode_management: Optional[bool] + unlock_via_gateway: Optional[bool] + wifi: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( auto_lock_time_config=d.get("auto_lock_time_config", None), incomplete_keyboard_passcode=d.get( @@ -1854,26 +1861,26 @@ class WirelessKeypads(ResourceMapping): :ivar wireless_keypad_name: Name for a wireless keypad for a TTLock device. """ - wireless_keypad_id: float - wireless_keypad_name: str + wireless_keypad_id: Optional[float] + wireless_keypad_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( wireless_keypad_id=d.get("wireless_keypad_id", None), wireless_keypad_name=d.get("wireless_keypad_name", None), ) - feature_value: str - features: Features - has_gateway: bool - lock_alias: str - lock_id: float - timezone_raw_offset_ms: float - wireless_keypads: List[WirelessKeypads] + feature_value: Optional[str] + features: Optional[Features] + has_gateway: Optional[bool] + lock_alias: Optional[str] + lock_id: Optional[float] + timezone_raw_offset_ms: Optional[float] + wireless_keypads: Optional[List[WirelessKeypads]] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( feature_value=d.get("feature_value", None), features=( @@ -1899,11 +1906,11 @@ class TwoNMetadata(ResourceMapping): :ivar device_name: Device name for a 2N device.""" - device_id: float - device_name: str + device_id: Optional[float] + device_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_id=d.get("device_id", None), device_name=d.get("device_name", None), @@ -1921,13 +1928,13 @@ class UltraloqMetadata(ResourceMapping): :ivar time_zone: IANA timezone for the Ultraloq device.""" - device_id: str - device_name: str - device_type: str - time_zone: str + device_id: Optional[str] + device_name: Optional[str] + device_type: Optional[str] + time_zone: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_id=d.get("device_id", None), device_name=d.get("device_name", None), @@ -1941,10 +1948,10 @@ class VisionlineMetadata(ResourceMapping): :ivar encoder_id: Encoder ID for an ASSA ABLOY Visionline system.""" - encoder_id: str + encoder_id: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( encoder_id=d.get("encoder_id", None), ) @@ -1969,17 +1976,17 @@ class WyzeMetadata(ResourceMapping): :ivar product_type: Product type for a Wyze device.""" - device_id: str - device_info_model: str - device_name: str - keypad_uuid: str - locker_status_hardlock: float - product_model: str - product_name: str - product_type: str + device_id: Optional[str] + device_info_model: Optional[str] + device_name: Optional[str] + keypad_uuid: Optional[str] + locker_status_hardlock: Optional[float] + product_model: Optional[str] + product_name: Optional[str] + product_type: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_id=d.get("device_id", None), device_info_model=d.get("device_info_model", None), @@ -1991,6 +1998,32 @@ def from_dict(cls, d: Dict[str, Any]): product_type=d.get("product_type", None), ) + @dataclass + class YacanMetadata(ResourceMapping): + """Metadata for a Yacan device. + + :ivar device_id: Device ID for a Yacan device. + + :ivar device_name: Device name for a Yacan device. + + :ivar device_type: Device type for a Yacan device. + + :ivar serial_number: Serial number for a Yacan device.""" + + device_id: Optional[str] + device_name: Optional[str] + device_type: Optional[str] + serial_number: Optional[str] + + @classmethod + def from_dict(cls, d: Any): + return cls( + device_id=d.get("device_id", None), + device_name=d.get("device_name", None), + device_type=d.get("device_type", None), + serial_number=d.get("serial_number", None), + ) + @dataclass class CodeConstraints(ResourceMapping): """Constraints on access codes for the device. Seam represents each constraint as an object with a ``constraint_type`` property. Depending on the constraint type, there may also be additional properties. Note that some constraints are manufacturer- or device-specific. @@ -2002,11 +2035,11 @@ class CodeConstraints(ResourceMapping): :ivar min_length: Minimum name length constraint for access codes.""" constraint_type: str - max_length: float - min_length: float + max_length: Optional[float] + min_length: Optional[float] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( constraint_type=d.get("constraint_type", None), max_length=d.get("max_length", None), @@ -2022,7 +2055,7 @@ class KeypadBattery(ResourceMapping): level: float @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( level=d.get("level", None), ) @@ -2064,7 +2097,7 @@ class TimePairs(ResourceMapping): start_time: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( display_name=d.get("display_name", None), end_time=d.get("end_time", None), @@ -2072,16 +2105,16 @@ def from_dict(cls, d: Dict[str, Any]): ) display_name: str - end_date_recurrence_rule: str - matching_start_end_time: bool - max_duration: str - min_duration: str - start_date_recurrence_rule: str - time_pairs: List[TimePairs] - time_zone: str + end_date_recurrence_rule: Optional[str] + matching_start_end_time: Optional[bool] + max_duration: Optional[str] + min_duration: Optional[str] + start_date_recurrence_rule: Optional[str] + time_pairs: Optional[List[TimePairs]] + time_zone: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( display_name=d.get("display_name", None), end_date_recurrence_rule=d.get("end_date_recurrence_rule", None), @@ -2134,7 +2167,7 @@ class TimePairs(ResourceMapping): start_time: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( display_name=d.get("display_name", None), end_time=d.get("end_time", None), @@ -2142,16 +2175,16 @@ def from_dict(cls, d: Dict[str, Any]): ) display_name: str - end_date_recurrence_rule: str - matching_start_end_time: bool - max_duration: str - min_duration: str - start_date_recurrence_rule: str - time_pairs: List[TimePairs] - time_zone: str + end_date_recurrence_rule: Optional[str] + matching_start_end_time: Optional[bool] + max_duration: Optional[str] + min_duration: Optional[str] + start_date_recurrence_rule: Optional[str] + time_pairs: Optional[List[TimePairs]] + time_zone: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( display_name=d.get("display_name", None), end_date_recurrence_rule=d.get("end_date_recurrence_rule", None), @@ -2210,7 +2243,7 @@ class Errors(ResourceMapping): message: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -2222,15 +2255,15 @@ def from_dict(cls, d: Dict[str, Any]): device_id: str ends_at: str errors: List[Errors] - is_override_allowed: bool - max_override_period_minutes: int - name: str + is_override_allowed: Optional[bool] + max_override_period_minutes: Optional[int] + name: Optional[str] starts_at: str thermostat_schedule_id: str workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( climate_preset_key=d.get("climate_preset_key", None), created_at=d.get("created_at", None), @@ -2293,12 +2326,12 @@ class EcobeeMetadata(ResourceMapping): :ivar owner: Indicates whether the climate preset is owned by the user or the system. """ - climate_ref: str - is_optimized: bool - owner: str + climate_ref: Optional[str] + is_optimized: Optional[bool] + owner: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( climate_ref=d.get("climate_ref", None), is_optimized=d.get("is_optimized", None), @@ -2309,20 +2342,20 @@ def from_dict(cls, d: Dict[str, Any]): can_edit: bool can_use_with_thermostat_daily_programs: bool climate_preset_key: str - climate_preset_mode: str - cooling_set_point_celsius: float - cooling_set_point_fahrenheit: float + climate_preset_mode: Optional[str] + cooling_set_point_celsius: Optional[float] + cooling_set_point_fahrenheit: Optional[float] display_name: str - ecobee_metadata: EcobeeMetadata - fan_mode_setting: str - heating_set_point_celsius: float - heating_set_point_fahrenheit: float - hvac_mode_setting: str + ecobee_metadata: Optional[EcobeeMetadata] + fan_mode_setting: Optional[str] + heating_set_point_celsius: Optional[float] + heating_set_point_fahrenheit: Optional[float] + hvac_mode_setting: Optional[str] manual_override_allowed: bool - name: str + name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( can_delete=d.get("can_delete", None), can_edit=d.get("can_edit", None), @@ -2397,36 +2430,36 @@ class EcobeeMetadata(ResourceMapping): :ivar owner: Indicates whether the climate preset is owned by the user or the system. """ - climate_ref: str - is_optimized: bool - owner: str + climate_ref: Optional[str] + is_optimized: Optional[bool] + owner: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( climate_ref=d.get("climate_ref", None), is_optimized=d.get("is_optimized", None), owner=d.get("owner", None), ) - can_delete: bool - can_edit: bool - can_use_with_thermostat_daily_programs: bool - climate_preset_key: str - climate_preset_mode: str - cooling_set_point_celsius: float - cooling_set_point_fahrenheit: float - display_name: str - ecobee_metadata: EcobeeMetadata - fan_mode_setting: str - heating_set_point_celsius: float - heating_set_point_fahrenheit: float - hvac_mode_setting: str - manual_override_allowed: bool - name: str + can_delete: Optional[bool] + can_edit: Optional[bool] + can_use_with_thermostat_daily_programs: Optional[bool] + climate_preset_key: Optional[str] + climate_preset_mode: Optional[str] + cooling_set_point_celsius: Optional[float] + cooling_set_point_fahrenheit: Optional[float] + display_name: Optional[str] + ecobee_metadata: Optional[EcobeeMetadata] + fan_mode_setting: Optional[str] + heating_set_point_celsius: Optional[float] + heating_set_point_fahrenheit: Optional[float] + hvac_mode_setting: Optional[str] + manual_override_allowed: Optional[bool] + name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( can_delete=d.get("can_delete", None), can_edit=d.get("can_edit", None), @@ -2501,36 +2534,36 @@ class EcobeeMetadata(ResourceMapping): :ivar owner: Indicates whether the climate preset is owned by the user or the system. """ - climate_ref: str - is_optimized: bool - owner: str + climate_ref: Optional[str] + is_optimized: Optional[bool] + owner: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( climate_ref=d.get("climate_ref", None), is_optimized=d.get("is_optimized", None), owner=d.get("owner", None), ) - can_delete: bool - can_edit: bool - can_use_with_thermostat_daily_programs: bool - climate_preset_key: str - climate_preset_mode: str - cooling_set_point_celsius: float - cooling_set_point_fahrenheit: float - display_name: str - ecobee_metadata: EcobeeMetadata - fan_mode_setting: str - heating_set_point_celsius: float - heating_set_point_fahrenheit: float - hvac_mode_setting: str - manual_override_allowed: bool - name: str + can_delete: Optional[bool] + can_edit: Optional[bool] + can_use_with_thermostat_daily_programs: Optional[bool] + climate_preset_key: Optional[str] + climate_preset_mode: Optional[str] + cooling_set_point_celsius: Optional[float] + cooling_set_point_fahrenheit: Optional[float] + display_name: Optional[str] + ecobee_metadata: Optional[EcobeeMetadata] + fan_mode_setting: Optional[str] + heating_set_point_celsius: Optional[float] + heating_set_point_fahrenheit: Optional[float] + hvac_mode_setting: Optional[str] + manual_override_allowed: Optional[bool] + name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( can_delete=d.get("can_delete", None), can_edit=d.get("can_edit", None), @@ -2572,13 +2605,13 @@ class TemperatureThreshold(ResourceMapping): :ivar upper_limit_fahrenheit: Upper limit in °F within the current `temperature threshold `_ set for the thermostat. """ - lower_limit_celsius: float - lower_limit_fahrenheit: float - upper_limit_celsius: float - upper_limit_fahrenheit: float + lower_limit_celsius: Optional[float] + lower_limit_fahrenheit: Optional[float] + upper_limit_celsius: Optional[float] + upper_limit_fahrenheit: Optional[float] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( lower_limit_celsius=d.get("lower_limit_celsius", None), lower_limit_fahrenheit=d.get("lower_limit_fahrenheit", None), @@ -2616,7 +2649,7 @@ class Periods(ResourceMapping): starts_at_time: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( climate_preset_key=d.get("climate_preset_key", None), starts_at_time=d.get("starts_at_time", None), @@ -2624,13 +2657,13 @@ def from_dict(cls, d: Dict[str, Any]): created_at: str device_id: str - name: str + name: Optional[str] periods: List[Periods] thermostat_daily_program_id: str workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), device_id=d.get("device_id", None), @@ -2664,16 +2697,16 @@ class ThermostatWeeklyProgram(ResourceMapping): """ created_at: str - friday_program_id: str - monday_program_id: str - saturday_program_id: str - sunday_program_id: str - thursday_program_id: str - tuesday_program_id: str - wednesday_program_id: str + friday_program_id: Optional[str] + monday_program_id: Optional[str] + saturday_program_id: Optional[str] + sunday_program_id: Optional[str] + thursday_program_id: Optional[str] + tuesday_program_id: Optional[str] + wednesday_program_id: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), friday_program_id=d.get("friday_program_id", None), @@ -2685,113 +2718,118 @@ def from_dict(cls, d: Dict[str, Any]): wednesday_program_id=d.get("wednesday_program_id", None), ) - accessory_keypad: AccessoryKeypad - appearance: Appearance - battery: Battery - battery_level: float - currently_triggering_noise_threshold_ids: List[str] - has_direct_power: bool - image_alt_text: str - image_url: str - manufacturer: str - model: Model + accessory_keypad: Optional[AccessoryKeypad] + appearance: Optional[Appearance] + battery: Optional[Battery] + battery_level: Optional[float] + currently_triggering_noise_threshold_ids: Optional[List[str]] + has_direct_power: Optional[bool] + image_alt_text: Optional[str] + image_url: Optional[str] + manufacturer: Optional[str] + model: Optional[Model] name: str - noise_level_decibels: float - offline_access_codes_enabled: bool + noise_level_decibels: Optional[float] + offline_access_codes_enabled: Optional[bool] online: bool - online_access_codes_enabled: bool - serial_number: str - supports_accessory_keypad: bool - supports_offline_access_codes: bool - assa_abloy_credential_service_metadata: AssaAbloyCredentialServiceMetadata - salto_space_credential_service_metadata: SaltoSpaceCredentialServiceMetadata - akiles_metadata: AkilesMetadata - aqara_metadata: AqaraMetadata - assa_abloy_vostio_metadata: AssaAbloyVostioMetadata - august_metadata: AugustMetadata - avigilon_alta_metadata: AvigilonAltaMetadata - brivo_metadata: BrivoMetadata - controlbyweb_metadata: ControlbywebMetadata - dormakaba_oracode_metadata: DormakabaOracodeMetadata - ecobee_metadata: EcobeeMetadata - four_suites_metadata: FourSuitesMetadata - genie_metadata: GenieMetadata - honeywell_resideo_metadata: HoneywellResideoMetadata - igloo_metadata: IglooMetadata - igloohome_metadata: IgloohomeMetadata - keynest_metadata: KeynestMetadata - kisi_metadata: KisiMetadata - korelock_metadata: KorelockMetadata - kwikset_metadata: KwiksetMetadata - lockly_metadata: LocklyMetadata - minut_metadata: MinutMetadata - nest_metadata: NestMetadata - noiseaware_metadata: NoiseawareMetadata - nuki_metadata: NukiMetadata - omnitec_metadata: OmnitecMetadata - ring_metadata: RingMetadata - salto_ks_metadata: SaltoKsMetadata - salto_metadata: SaltoMetadata - schlage_metadata: SchlageMetadata - seam_bridge_metadata: SeamBridgeMetadata - sensi_metadata: SensiMetadata - smartthings_metadata: SmartthingsMetadata - tado_metadata: TadoMetadata - tedee_metadata: TedeeMetadata - ttlock_metadata: TtlockMetadata - two_n_metadata: TwoNMetadata - ultraloq_metadata: UltraloqMetadata - visionline_metadata: VisionlineMetadata - wyze_metadata: WyzeMetadata - auto_lock_delay_seconds: float - auto_lock_enabled: bool - backup_access_code_pool_enabled: bool - code_constraints: List[CodeConstraints] - door_open: bool - has_native_entry_events: bool - keypad_battery: KeypadBattery - locked: bool - max_active_codes_supported: float - offline_time_frame_options: List[OfflineTimeFrameOptions] - online_time_frame_options: List[OnlineTimeFrameOptions] - supported_code_lengths: List[float] - supports_backup_access_code_pool: bool - active_thermostat_schedule: ActiveThermostatSchedule - active_thermostat_schedule_id: str - available_climate_preset_modes: List[str] - available_climate_presets: List[AvailableClimatePresets] - available_fan_mode_settings: List[str] - available_hvac_mode_settings: List[str] - current_climate_setting: CurrentClimateSetting - default_climate_setting: DefaultClimateSetting - fallback_climate_preset_key: str - fan_mode_setting: str - is_cooling: bool - is_fan_running: bool - is_heating: bool - is_temporary_manual_override_active: bool - max_cooling_set_point_celsius: float - max_cooling_set_point_fahrenheit: float - max_heating_set_point_celsius: float - max_heating_set_point_fahrenheit: float - max_thermostat_daily_program_periods_per_day: float - max_unique_climate_presets_per_thermostat_weekly_program: float - min_cooling_set_point_celsius: float - min_cooling_set_point_fahrenheit: float - min_heating_cooling_delta_celsius: float - min_heating_cooling_delta_fahrenheit: float - min_heating_set_point_celsius: float - min_heating_set_point_fahrenheit: float - relative_humidity: float - temperature_celsius: float - temperature_fahrenheit: float - temperature_threshold: TemperatureThreshold - thermostat_daily_program_period_precision_minutes: float - thermostat_daily_programs: List[ThermostatDailyPrograms] - thermostat_weekly_program: ThermostatWeeklyProgram + online_access_codes_enabled: Optional[bool] + serial_number: Optional[str] + supports_accessory_keypad: Optional[bool] + supports_offline_access_codes: Optional[bool] + assa_abloy_credential_service_metadata: Optional[ + AssaAbloyCredentialServiceMetadata + ] + salto_space_credential_service_metadata: Optional[ + SaltoSpaceCredentialServiceMetadata + ] + akiles_metadata: Optional[AkilesMetadata] + aqara_metadata: Optional[AqaraMetadata] + assa_abloy_vostio_metadata: Optional[AssaAbloyVostioMetadata] + august_metadata: Optional[AugustMetadata] + avigilon_alta_metadata: Optional[AvigilonAltaMetadata] + brivo_metadata: Optional[BrivoMetadata] + controlbyweb_metadata: Optional[ControlbywebMetadata] + dormakaba_oracode_metadata: Optional[DormakabaOracodeMetadata] + ecobee_metadata: Optional[EcobeeMetadata] + four_suites_metadata: Optional[FourSuitesMetadata] + genie_metadata: Optional[GenieMetadata] + honeywell_resideo_metadata: Optional[HoneywellResideoMetadata] + igloo_metadata: Optional[IglooMetadata] + igloohome_metadata: Optional[IgloohomeMetadata] + keynest_metadata: Optional[KeynestMetadata] + kisi_metadata: Optional[KisiMetadata] + korelock_metadata: Optional[KorelockMetadata] + kwikset_metadata: Optional[KwiksetMetadata] + lockly_metadata: Optional[LocklyMetadata] + minut_metadata: Optional[MinutMetadata] + nest_metadata: Optional[NestMetadata] + noiseaware_metadata: Optional[NoiseawareMetadata] + nuki_metadata: Optional[NukiMetadata] + omnitec_metadata: Optional[OmnitecMetadata] + ring_metadata: Optional[RingMetadata] + salto_ks_metadata: Optional[SaltoKsMetadata] + salto_metadata: Optional[SaltoMetadata] + schlage_metadata: Optional[SchlageMetadata] + seam_bridge_metadata: Optional[SeamBridgeMetadata] + sensi_metadata: Optional[SensiMetadata] + smartthings_metadata: Optional[SmartthingsMetadata] + tado_metadata: Optional[TadoMetadata] + tedee_metadata: Optional[TedeeMetadata] + ttlock_metadata: Optional[TtlockMetadata] + two_n_metadata: Optional[TwoNMetadata] + ultraloq_metadata: Optional[UltraloqMetadata] + visionline_metadata: Optional[VisionlineMetadata] + wyze_metadata: Optional[WyzeMetadata] + yacan_metadata: Optional[YacanMetadata] + auto_lock_delay_seconds: Optional[float] + auto_lock_enabled: Optional[bool] + backup_access_code_pool_enabled: Optional[bool] + code_constraints: Optional[List[CodeConstraints]] + door_open: Optional[bool] + has_native_entry_events: Optional[bool] + keypad_battery: Optional[KeypadBattery] + locked: Optional[bool] + max_active_codes_supported: Optional[float] + offline_time_frame_options: Optional[List[OfflineTimeFrameOptions]] + online_time_frame_options: Optional[List[OnlineTimeFrameOptions]] + supported_code_lengths: Optional[List[float]] + supports_backup_access_code_pool: Optional[bool] + active_thermostat_schedule: Optional[ActiveThermostatSchedule] + active_thermostat_schedule_id: Optional[str] + available_climate_preset_modes: Optional[List[str]] + available_climate_presets: Optional[List[AvailableClimatePresets]] + available_fan_mode_settings: Optional[List[str]] + available_hvac_mode_settings: Optional[List[str]] + current_climate_setting: Optional[CurrentClimateSetting] + default_climate_setting: Optional[DefaultClimateSetting] + fallback_climate_preset_key: Optional[str] + fan_mode_setting: Optional[str] + is_cooling: Optional[bool] + is_fan_running: Optional[bool] + is_heating: Optional[bool] + is_temporary_manual_override_active: Optional[bool] + max_cooling_set_point_celsius: Optional[float] + max_cooling_set_point_fahrenheit: Optional[float] + max_heating_set_point_celsius: Optional[float] + max_heating_set_point_fahrenheit: Optional[float] + max_thermostat_daily_program_periods_per_day: Optional[float] + max_unique_climate_presets_per_thermostat_weekly_program: Optional[float] + min_cooling_set_point_celsius: Optional[float] + min_cooling_set_point_fahrenheit: Optional[float] + min_heating_cooling_delta_celsius: Optional[float] + min_heating_cooling_delta_fahrenheit: Optional[float] + min_heating_set_point_celsius: Optional[float] + min_heating_set_point_fahrenheit: Optional[float] + relative_humidity: Optional[float] + temperature_celsius: Optional[float] + temperature_fahrenheit: Optional[float] + temperature_threshold: Optional[TemperatureThreshold] + thermostat_daily_program_period_precision_minutes: Optional[float] + thermostat_daily_programs: Optional[List[ThermostatDailyPrograms]] + thermostat_weekly_program: Optional[ThermostatWeeklyProgram] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( accessory_keypad=( cls.AccessoryKeypad.from_dict(d.get("accessory_keypad")) @@ -3043,6 +3081,11 @@ def from_dict(cls, d: Dict[str, Any]): if d.get("wyze_metadata") is not None else None ), + yacan_metadata=( + cls.YacanMetadata.from_dict(d.get("yacan_metadata")) + if d.get("yacan_metadata") is not None + else None + ), auto_lock_delay_seconds=d.get("auto_lock_delay_seconds", None), auto_lock_enabled=d.get("auto_lock_enabled", None), backup_access_code_pool_enabled=d.get( @@ -3194,11 +3237,11 @@ class Warnings(ResourceMapping): created_at: str message: str warning_code: str - active_access_code_count: int - max_active_access_code_count: int + active_access_code_count: Optional[int] + max_active_access_code_count: Optional[int] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -3209,46 +3252,46 @@ def from_dict(cls, d: Dict[str, Any]): ), ) - can_configure_auto_lock: bool - can_hvac_cool: bool - can_hvac_heat: bool - can_hvac_heat_cool: bool - can_program_offline_access_codes: bool - can_program_online_access_codes: bool - can_program_thermostat_programs_as_different_each_day: bool - can_program_thermostat_programs_as_same_each_day: bool - can_program_thermostat_programs_as_weekday_weekend: bool - can_remotely_lock: bool - can_remotely_unlock: bool - can_run_thermostat_programs: bool - can_simulate_connection: bool - can_simulate_disconnection: bool - can_simulate_hub_connection: bool - can_simulate_hub_disconnection: bool - can_simulate_paid_subscription: bool - can_simulate_removal: bool - can_turn_off_hvac: bool - can_unlock_with_code: bool + can_configure_auto_lock: Optional[bool] + can_hvac_cool: Optional[bool] + can_hvac_heat: Optional[bool] + can_hvac_heat_cool: Optional[bool] + can_program_offline_access_codes: Optional[bool] + can_program_online_access_codes: Optional[bool] + can_program_thermostat_programs_as_different_each_day: Optional[bool] + can_program_thermostat_programs_as_same_each_day: Optional[bool] + can_program_thermostat_programs_as_weekday_weekend: Optional[bool] + can_remotely_lock: Optional[bool] + can_remotely_unlock: Optional[bool] + can_run_thermostat_programs: Optional[bool] + can_simulate_connection: Optional[bool] + can_simulate_disconnection: Optional[bool] + can_simulate_hub_connection: Optional[bool] + can_simulate_hub_disconnection: Optional[bool] + can_simulate_paid_subscription: Optional[bool] + can_simulate_removal: Optional[bool] + can_turn_off_hvac: Optional[bool] + can_unlock_with_code: Optional[bool] capabilities_supported: List[str] connected_account_id: str created_at: str custom_metadata: Dict[str, Any] device_id: str - device_manufacturer: DeviceManufacturer - device_provider: DeviceProvider + device_manufacturer: Optional[DeviceManufacturer] + device_provider: Optional[DeviceProvider] device_type: str display_name: str errors: List[Errors] is_managed: bool - location: Location - nickname: str - properties: Properties + location: Optional[Location] + nickname: Optional[str] + properties: Optional[Properties] space_ids: List[str] warnings: List[Warnings] workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( can_configure_auto_lock=d.get("can_configure_auto_lock", None), can_hvac_cool=d.get("can_hvac_cool", None), diff --git a/seam/resources/device_provider.py b/seam/resources/device_provider.py index 2b498134..f58fb9d9 100644 --- a/seam/resources/device_provider.py +++ b/seam/resources/device_provider.py @@ -57,33 +57,33 @@ class DeviceProvider: :ivar provider_categories: List of provider categories to which the device provider belongs, such as ``stable``, ``consumer_smartlocks``, ``thermostats``, and so on. """ - can_configure_auto_lock: bool - can_hvac_cool: bool - can_hvac_heat: bool - can_hvac_heat_cool: bool - can_program_offline_access_codes: bool - can_program_online_access_codes: bool - can_program_thermostat_programs_as_different_each_day: bool - can_program_thermostat_programs_as_same_each_day: bool - can_program_thermostat_programs_as_weekday_weekend: bool - can_remotely_lock: bool - can_remotely_unlock: bool - can_run_thermostat_programs: bool - can_simulate_connection: bool - can_simulate_disconnection: bool - can_simulate_hub_connection: bool - can_simulate_hub_disconnection: bool - can_simulate_paid_subscription: bool - can_simulate_removal: bool - can_turn_off_hvac: bool - can_unlock_with_code: bool + can_configure_auto_lock: Optional[bool] + can_hvac_cool: Optional[bool] + can_hvac_heat: Optional[bool] + can_hvac_heat_cool: Optional[bool] + can_program_offline_access_codes: Optional[bool] + can_program_online_access_codes: Optional[bool] + can_program_thermostat_programs_as_different_each_day: Optional[bool] + can_program_thermostat_programs_as_same_each_day: Optional[bool] + can_program_thermostat_programs_as_weekday_weekend: Optional[bool] + can_remotely_lock: Optional[bool] + can_remotely_unlock: Optional[bool] + can_run_thermostat_programs: Optional[bool] + can_simulate_connection: Optional[bool] + can_simulate_disconnection: Optional[bool] + can_simulate_hub_connection: Optional[bool] + can_simulate_hub_disconnection: Optional[bool] + can_simulate_paid_subscription: Optional[bool] + can_simulate_removal: Optional[bool] + can_turn_off_hvac: Optional[bool] + can_unlock_with_code: Optional[bool] device_provider_name: str display_name: str image_url: str provider_categories: List[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( can_configure_auto_lock=d.get("can_configure_auto_lock", None), can_hvac_cool=d.get("can_hvac_cool", None), diff --git a/seam/resources/instant_key.py b/seam/resources/instant_key.py index 39a98211..775e8a11 100644 --- a/seam/resources/instant_key.py +++ b/seam/resources/instant_key.py @@ -38,12 +38,12 @@ class Customization(ResourceMapping): :ivar secondary_color: Secondary color used in the Instant Key UI.""" - logo_url: str - primary_color: str - secondary_color: str + logo_url: Optional[str] + primary_color: Optional[str] + secondary_color: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( logo_url=d.get("logo_url", None), primary_color=d.get("primary_color", None), @@ -52,8 +52,8 @@ def from_dict(cls, d: Dict[str, Any]): client_session_id: str created_at: str - customization: Customization - customization_profile_id: str + customization: Optional[Customization] + customization_profile_id: Optional[str] expires_at: str instant_key_id: str instant_key_url: str @@ -61,7 +61,7 @@ def from_dict(cls, d: Dict[str, Any]): workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( client_session_id=d.get("client_session_id", None), created_at=d.get("created_at", None), diff --git a/seam/resources/noise_threshold.py b/seam/resources/noise_threshold.py index e0eee750..9918d3c3 100644 --- a/seam/resources/noise_threshold.py +++ b/seam/resources/noise_threshold.py @@ -28,11 +28,11 @@ class NoiseThreshold: name: str noise_threshold_decibels: float noise_threshold_id: str - noise_threshold_nrs: float + noise_threshold_nrs: Optional[float] starts_daily_at: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_id=d.get("device_id", None), ends_daily_at=d.get("ends_daily_at", None), diff --git a/seam/resources/pagination.py b/seam/resources/pagination.py index a2180092..b1faa8c7 100644 --- a/seam/resources/pagination.py +++ b/seam/resources/pagination.py @@ -15,11 +15,11 @@ class Pagination: :ivar next_page_url: URL to get the next page of results.""" has_next_page: bool - next_page_cursor: str - next_page_url: str + next_page_cursor: Optional[str] + next_page_url: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( has_next_page=d.get("has_next_page", None), next_page_cursor=d.get("next_page_cursor", None), diff --git a/seam/resources/phone.py b/seam/resources/phone.py index c7ed93c2..74b1963b 100644 --- a/seam/resources/phone.py +++ b/seam/resources/phone.py @@ -43,7 +43,7 @@ class Errors(ResourceMapping): message: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -76,21 +76,21 @@ class Endpoints(ResourceMapping): :ivar is_active: Indicated whether the endpoint is active.""" - endpoint_id: str - is_active: bool + endpoint_id: Optional[str] + is_active: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( endpoint_id=d.get("endpoint_id", None), is_active=d.get("is_active", None), ) - endpoints: List[Endpoints] - has_active_endpoint: bool + endpoints: Optional[List[Endpoints]] + has_active_endpoint: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( endpoints=[ cls.Endpoints.from_dict(i) for i in d.get("endpoints") or [] @@ -105,19 +105,23 @@ class SaltoSpaceCredentialServiceMetadata(ResourceMapping): :ivar has_active_phone: Indicates whether the credential service has an active associated phone. """ - has_active_phone: bool + has_active_phone: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( has_active_phone=d.get("has_active_phone", None), ) - assa_abloy_credential_service_metadata: AssaAbloyCredentialServiceMetadata - salto_space_credential_service_metadata: SaltoSpaceCredentialServiceMetadata + assa_abloy_credential_service_metadata: Optional[ + AssaAbloyCredentialServiceMetadata + ] + salto_space_credential_service_metadata: Optional[ + SaltoSpaceCredentialServiceMetadata + ] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( assa_abloy_credential_service_metadata=( cls.AssaAbloyCredentialServiceMetadata.from_dict( @@ -150,7 +154,7 @@ class Warnings(ResourceMapping): warning_code: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -163,13 +167,13 @@ def from_dict(cls, d: Dict[str, Any]): device_type: str display_name: str errors: List[Errors] - nickname: str - properties: Properties + nickname: Optional[str] + properties: Optional[Properties] warnings: List[Warnings] workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), custom_metadata=DeepAttrDict(d.get("custom_metadata", None)), diff --git a/seam/resources/seam_event.py b/seam/resources/seam_event.py index 28a57f65..323115b6 100644 --- a/seam/resources/seam_event.py +++ b/seam/resources/seam_event.py @@ -80,7 +80,7 @@ class SeamEvent: :ivar is_backup_code: Indicates whether the code is a backup code (only present when mode is 'code' and a backup code was used). - :ivar acs_system_id: + :ivar acs_system_id: ID of the access system. :ivar acs_system_errors: Errors associated with the access control system. @@ -88,7 +88,7 @@ class SeamEvent: :ivar acs_credential_id: ID of the affected credential. - :ivar acs_user_id: + :ivar acs_user_id: ID of the affected access system user. :ivar acs_encoder_id: ID of the affected encoder. @@ -100,8 +100,6 @@ class SeamEvent: :ivar customer_key: - :ivar connected_account_type: undocumented: Unreleased. - :ivar action_attempt_id: :ivar action_type: Type of the action. @@ -136,8 +134,6 @@ class SeamEvent: :ivar method: - :ivar user_identity_id: - :ivar reason: Why access was denied, when the provider reports a determinable cause. Omitted when unknown. :ivar climate_preset_key: Key of the climate preset that was activated. @@ -200,12 +196,12 @@ class ChangedProperties(ResourceMapping): :ivar to: New value of the property, or null if cleared.""" - from_: str + from_: Optional[str] property: str - to: str + to: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( from_=d.get("from", None), property=d.get("property", None), @@ -224,13 +220,13 @@ class From(ResourceMapping): :ivar starts_at: Previous start time.""" - name: str - code: str - ends_at: str - starts_at: str + name: Optional[str] + code: Optional[str] + ends_at: Optional[str] + starts_at: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( name=d.get("name", None), code=d.get("code", None), @@ -250,13 +246,13 @@ class To(ResourceMapping): :ivar starts_at: New start time.""" - name: str - code: str - ends_at: str - starts_at: str + name: Optional[str] + code: Optional[str] + ends_at: Optional[str] + starts_at: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( name=d.get("name", None), code=d.get("code", None), @@ -275,12 +271,12 @@ class RequestedMutations(ResourceMapping): :ivar to: New property values after the requested change. Keys depend on the mutation type. Absent for non-property mutations like ``deleting``. """ - from_: Dict[str, Any] + from_: Optional[Dict[str, Any]] mutation_code: str - to: Dict[str, Any] + to: Optional[Dict[str, Any]] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( from_=DeepAttrDict(d.get("from", None)), mutation_code=d.get("mutation_code", None), @@ -303,7 +299,7 @@ class AccessCodeErrors(ResourceMapping): message: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -326,7 +322,7 @@ class AccessCodeWarnings(ResourceMapping): warning_code: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -349,7 +345,7 @@ class ConnectedAccountErrors(ResourceMapping): message: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -372,7 +368,7 @@ class ConnectedAccountWarnings(ResourceMapping): warning_code: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -395,7 +391,7 @@ class DeviceErrors(ResourceMapping): message: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -418,7 +414,7 @@ class DeviceWarnings(ResourceMapping): warning_code: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -441,7 +437,7 @@ class AcsSystemErrors(ResourceMapping): message: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -464,7 +460,7 @@ class AcsSystemWarnings(ResourceMapping): warning_code: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -484,106 +480,104 @@ class Reason(ResourceMapping): reason_code: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( message=d.get("message", None), reason_code=d.get("reason_code", None), ) - access_code_id: str - connected_account_custom_metadata: Dict[str, Any] - connected_account_id: str + access_code_id: Optional[str] + connected_account_custom_metadata: Optional[Dict[str, Any]] + connected_account_id: Optional[str] created_at: str - device_custom_metadata: Dict[str, Any] - device_id: str - event_description: str + device_custom_metadata: Optional[Dict[str, Any]] + device_id: Optional[str] + event_description: Optional[str] event_id: str event_type: str occurred_at: str workspace_id: str - change_reason: str - changed_properties: List[ChangedProperties] - description: str - from_: From - to: To - requested_mutations: List[RequestedMutations] - code: str - access_code_errors: List[AccessCodeErrors] - access_code_warnings: List[AccessCodeWarnings] - connected_account_errors: List[ConnectedAccountErrors] - connected_account_warnings: List[ConnectedAccountWarnings] - device_errors: List[DeviceErrors] - device_warnings: List[DeviceWarnings] - backup_access_code_id: str - access_grant_id: str - acs_entrance_id: str - access_grant_key: str - ends_at: str - starts_at: str - error_message: str - missing_device_ids: List[str] - access_grant_ids: List[str] - access_grant_keys: List[str] - access_method_id: str - is_backup_code: bool - acs_system_id: str - acs_system_errors: List[AcsSystemErrors] - acs_system_warnings: List[AcsSystemWarnings] - acs_credential_id: str - acs_user_id: str - acs_encoder_id: str - acs_access_group_id: str - client_session_id: str - connect_webview_id: str - customer_key: str - connected_account_type: str - action_attempt_id: str - action_type: str - status: str - error_code: str - battery_level: float - battery_status: str - device_name: str - minut_metadata: Dict[str, Any] - noise_level_decibels: float - noise_level_nrs: float - noise_threshold_id: str - noise_threshold_name: str - noiseaware_metadata: Dict[str, Any] - access_code_is_managed: bool - is_via_bluetooth: bool - is_via_nfc: bool - method: str - user_identity_id: str - reason: Reason - climate_preset_key: str - is_fallback_climate_preset: bool - thermostat_schedule_id: str - cooling_set_point_celsius: float - cooling_set_point_fahrenheit: float - fan_mode_setting: str - heating_set_point_celsius: float - heating_set_point_fahrenheit: float - hvac_mode_setting: str - lower_limit_celsius: float - lower_limit_fahrenheit: float - temperature_celsius: float - temperature_fahrenheit: float - upper_limit_celsius: float - upper_limit_fahrenheit: float - desired_temperature_celsius: float - desired_temperature_fahrenheit: float - activation_reason: str - image_url: str - motion_sub_type: str - video_url: str - acs_entrance_ids: List[str] - device_ids: List[str] - space_id: str - space_key: str + change_reason: Optional[str] + changed_properties: Optional[List[ChangedProperties]] + description: Optional[str] + from_: Optional[From] + to: Optional[To] + requested_mutations: Optional[List[RequestedMutations]] + code: Optional[str] + access_code_errors: Optional[List[AccessCodeErrors]] + access_code_warnings: Optional[List[AccessCodeWarnings]] + connected_account_errors: Optional[List[ConnectedAccountErrors]] + connected_account_warnings: Optional[List[ConnectedAccountWarnings]] + device_errors: Optional[List[DeviceErrors]] + device_warnings: Optional[List[DeviceWarnings]] + backup_access_code_id: Optional[str] + access_grant_id: Optional[str] + acs_entrance_id: Optional[str] + access_grant_key: Optional[str] + ends_at: Optional[str] + starts_at: Optional[str] + error_message: Optional[str] + missing_device_ids: Optional[List[str]] + access_grant_ids: Optional[List[str]] + access_grant_keys: Optional[List[str]] + access_method_id: Optional[str] + is_backup_code: Optional[bool] + acs_system_id: Optional[str] + acs_system_errors: Optional[List[AcsSystemErrors]] + acs_system_warnings: Optional[List[AcsSystemWarnings]] + acs_credential_id: Optional[str] + acs_user_id: Optional[str] + acs_encoder_id: Optional[str] + acs_access_group_id: Optional[str] + client_session_id: Optional[str] + connect_webview_id: Optional[str] + customer_key: Optional[str] + action_attempt_id: Optional[str] + action_type: Optional[str] + status: Optional[str] + error_code: Optional[str] + battery_level: Optional[float] + battery_status: Optional[str] + device_name: Optional[str] + minut_metadata: Optional[Dict[str, Any]] + noise_level_decibels: Optional[float] + noise_level_nrs: Optional[float] + noise_threshold_id: Optional[str] + noise_threshold_name: Optional[str] + noiseaware_metadata: Optional[Dict[str, Any]] + access_code_is_managed: Optional[bool] + is_via_bluetooth: Optional[bool] + is_via_nfc: Optional[bool] + method: Optional[str] + reason: Optional[Reason] + climate_preset_key: Optional[str] + is_fallback_climate_preset: Optional[bool] + thermostat_schedule_id: Optional[str] + cooling_set_point_celsius: Optional[float] + cooling_set_point_fahrenheit: Optional[float] + fan_mode_setting: Optional[str] + heating_set_point_celsius: Optional[float] + heating_set_point_fahrenheit: Optional[float] + hvac_mode_setting: Optional[str] + lower_limit_celsius: Optional[float] + lower_limit_fahrenheit: Optional[float] + temperature_celsius: Optional[float] + temperature_fahrenheit: Optional[float] + upper_limit_celsius: Optional[float] + upper_limit_fahrenheit: Optional[float] + desired_temperature_celsius: Optional[float] + desired_temperature_fahrenheit: Optional[float] + activation_reason: Optional[str] + image_url: Optional[str] + motion_sub_type: Optional[str] + video_url: Optional[str] + acs_entrance_ids: Optional[List[str]] + device_ids: Optional[List[str]] + space_id: Optional[str] + space_key: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( access_code_id=d.get("access_code_id", None), connected_account_custom_metadata=DeepAttrDict( @@ -663,7 +657,6 @@ def from_dict(cls, d: Dict[str, Any]): client_session_id=d.get("client_session_id", None), connect_webview_id=d.get("connect_webview_id", None), customer_key=d.get("customer_key", None), - connected_account_type=d.get("connected_account_type", None), action_attempt_id=d.get("action_attempt_id", None), action_type=d.get("action_type", None), status=d.get("status", None), @@ -681,7 +674,6 @@ def from_dict(cls, d: Dict[str, Any]): is_via_bluetooth=d.get("is_via_bluetooth", None), is_via_nfc=d.get("is_via_nfc", None), method=d.get("method", None), - user_identity_id=d.get("user_identity_id", None), reason=( cls.Reason.from_dict(d.get("reason")) if d.get("reason") is not None diff --git a/seam/resources/space.py b/seam/resources/space.py index 2712f62b..65e68978 100644 --- a/seam/resources/space.py +++ b/seam/resources/space.py @@ -42,13 +42,13 @@ class CustomerData(ResourceMapping): :ivar time_zone: IANA time zone for the space, e.g. America/Los_Angeles.""" - address: str - default_checkin_time: str - default_checkout_time: str - time_zone: str + address: Optional[str] + default_checkin_time: Optional[str] + default_checkout_time: Optional[str] + time_zone: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( address=d.get("address", None), default_checkin_time=d.get("default_checkin_time", None), @@ -68,7 +68,7 @@ class Geolocation(ResourceMapping): longitude: float @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( latitude=d.get("latitude", None), longitude=d.get("longitude", None), @@ -76,18 +76,18 @@ def from_dict(cls, d: Dict[str, Any]): acs_entrance_count: float created_at: str - customer_data: CustomerData - customer_key: str + customer_data: Optional[CustomerData] + customer_key: Optional[str] device_count: float display_name: str - geolocation: Geolocation + geolocation: Optional[Geolocation] name: str space_id: str - space_key: str + space_key: Optional[str] workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( acs_entrance_count=d.get("acs_entrance_count", None), created_at=d.get("created_at", None), diff --git a/seam/resources/thermostat_daily_program.py b/seam/resources/thermostat_daily_program.py index 3876b0d6..0c1c262d 100644 --- a/seam/resources/thermostat_daily_program.py +++ b/seam/resources/thermostat_daily_program.py @@ -34,7 +34,7 @@ class Periods(ResourceMapping): starts_at_time: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( climate_preset_key=d.get("climate_preset_key", None), starts_at_time=d.get("starts_at_time", None), @@ -42,13 +42,13 @@ def from_dict(cls, d: Dict[str, Any]): created_at: str device_id: str - name: str + name: Optional[str] periods: List[Periods] thermostat_daily_program_id: str workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), device_id=d.get("device_id", None), diff --git a/seam/resources/thermostat_schedule.py b/seam/resources/thermostat_schedule.py index 4bb71bc9..2c022801 100644 --- a/seam/resources/thermostat_schedule.py +++ b/seam/resources/thermostat_schedule.py @@ -46,7 +46,7 @@ class Errors(ResourceMapping): message: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -58,15 +58,15 @@ def from_dict(cls, d: Dict[str, Any]): device_id: str ends_at: str errors: List[Errors] - is_override_allowed: bool - max_override_period_minutes: int - name: str + is_override_allowed: Optional[bool] + max_override_period_minutes: Optional[int] + name: Optional[str] starts_at: str thermostat_schedule_id: str workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( climate_preset_key=d.get("climate_preset_key", None), created_at=d.get("created_at", None), diff --git a/seam/resources/unmanaged_access_code.py b/seam/resources/unmanaged_access_code.py index faecc79e..8c1d00ed 100644 --- a/seam/resources/unmanaged_access_code.py +++ b/seam/resources/unmanaged_access_code.py @@ -72,17 +72,17 @@ class DormakabaOracodeMetadata(ResourceMapping): :ivar user_level_name: Dormakaba Oracode user level name associated with this access code. """ - is_cancellable: bool - is_early_checkin_able: bool - is_extendable: bool - is_overridable: bool - site_name: str - stay_id: float - user_level_id: str - user_level_name: str + is_cancellable: Optional[bool] + is_early_checkin_able: Optional[bool] + is_extendable: Optional[bool] + is_overridable: Optional[bool] + site_name: Optional[str] + stay_id: Optional[float] + user_level_id: Optional[str] + user_level_name: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( is_cancellable=d.get("is_cancellable", None), is_early_checkin_able=d.get("is_early_checkin_able", None), @@ -132,31 +132,31 @@ class ModifiedFields(ResourceMapping): :ivar to: The new value of the field.""" field: str - from_: str - to: str + from_: Optional[str] + to: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( field=d.get("field", None), from_=d.get("from", None), to=d.get("to", None), ) - created_at: str + created_at: Optional[str] error_code: str - is_access_code_error: bool + is_access_code_error: Optional[bool] message: str - managed_access_code_id: str - unmanaged_access_code_id: str - change_type: str - modified_fields: List[ModifiedFields] - is_connected_account_error: bool - is_device_error: bool - is_bridge_error: bool + managed_access_code_id: Optional[str] + unmanaged_access_code_id: Optional[str] + change_type: Optional[str] + modified_fields: Optional[List[ModifiedFields]] + is_connected_account_error: Optional[bool] + is_device_error: Optional[bool] + is_bridge_error: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -200,25 +200,25 @@ class ModifiedFields(ResourceMapping): :ivar to: The new value of the field.""" field: str - from_: str - to: str + from_: Optional[str] + to: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( field=d.get("field", None), from_=d.get("from", None), to=d.get("to", None), ) - created_at: str + created_at: Optional[str] message: str warning_code: str - change_type: str - modified_fields: List[ModifiedFields] + change_type: Optional[str] + modified_fields: Optional[List[ModifiedFields]] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -231,24 +231,24 @@ def from_dict(cls, d: Dict[str, Any]): ) access_code_id: str - cannot_be_managed: bool - cannot_delete_unmanaged_access_code: bool - code: str + cannot_be_managed: Optional[bool] + cannot_delete_unmanaged_access_code: Optional[bool] + code: Optional[str] created_at: str device_id: str - dormakaba_oracode_metadata: DormakabaOracodeMetadata - ends_at: str + dormakaba_oracode_metadata: Optional[DormakabaOracodeMetadata] + ends_at: Optional[str] errors: List[Errors] is_managed: bool - name: str - starts_at: str + name: Optional[str] + starts_at: Optional[str] status: str type: str warnings: List[Warnings] workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( access_code_id=d.get("access_code_id", None), cannot_be_managed=d.get("cannot_be_managed", None), diff --git a/seam/resources/unmanaged_access_grant.py b/seam/resources/unmanaged_access_grant.py index 9b2c3f30..71e7a68e 100644 --- a/seam/resources/unmanaged_access_grant.py +++ b/seam/resources/unmanaged_access_grant.py @@ -56,10 +56,10 @@ class Errors(ResourceMapping): created_at: str error_code: str message: str - missing_device_ids: List[str] + missing_device_ids: Optional[List[str]] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -93,12 +93,12 @@ class From(ResourceMapping): :ivar starts_at: Previous start time for access.""" - device_ids: List[str] - ends_at: str - starts_at: str + device_ids: Optional[List[str]] + ends_at: Optional[str] + starts_at: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_ids=d.get("device_ids", None), ends_at=d.get("ends_at", None), @@ -117,13 +117,13 @@ class To(ResourceMapping): :ivar starts_at: New start time for access.""" - common_code_key: str - device_ids: List[str] - ends_at: str - starts_at: str + common_code_key: Optional[str] + device_ids: Optional[List[str]] + ends_at: Optional[str] + starts_at: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( common_code_key=d.get("common_code_key", None), device_ids=d.get("device_ids", None), @@ -132,14 +132,14 @@ def from_dict(cls, d: Dict[str, Any]): ) created_at: str - from_: From + from_: Optional[From] message: str mutation_code: str - to: To - access_method_ids: List[str] + to: Optional[To] + access_method_ids: Optional[List[str]] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), from_=( @@ -170,15 +170,15 @@ class RequestedAccessMethods(ResourceMapping): :ivar mode: Access method mode. Supported values: ``code``, ``card``, ``mobile_key``, ``cloud_key``. """ - code: str + code: Optional[str] created_access_method_ids: List[str] created_at: str display_name: str - instant_key_max_use_count: int + instant_key_max_use_count: Optional[int] mode: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( code=d.get("code", None), created_access_method_ids=d.get("created_access_method_ids", None), @@ -226,7 +226,7 @@ class FailedDevices(ResourceMapping): message: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_id=d.get("device_id", None), error_code=d.get("error_code", None), @@ -236,15 +236,15 @@ def from_dict(cls, d: Dict[str, Any]): created_at: str message: str warning_code: str - failed_devices: List[FailedDevices] - access_method_ids: List[str] - device_id: str - new_code: str - original_code: str - reason: str + failed_devices: Optional[List[FailedDevices]] + access_method_ids: Optional[List[str]] + device_id: Optional[str] + new_code: Optional[str] + original_code: Optional[str] + reason: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -264,21 +264,21 @@ def from_dict(cls, d: Dict[str, Any]): access_method_ids: List[str] created_at: str display_name: str - ends_at: str + ends_at: Optional[str] errors: List[Errors] location_ids: List[str] - name: str + name: Optional[str] pending_mutations: List[PendingMutations] requested_access_methods: List[RequestedAccessMethods] - reservation_key: str + reservation_key: Optional[str] space_ids: List[str] starts_at: str - user_identity_id: str + user_identity_id: Optional[str] warnings: List[Warnings] workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( access_grant_id=d.get("access_grant_id", None), access_method_ids=d.get("access_method_ids", None), diff --git a/seam/resources/unmanaged_access_method.py b/seam/resources/unmanaged_access_method.py index edd18201..c30e9a4e 100644 --- a/seam/resources/unmanaged_access_method.py +++ b/seam/resources/unmanaged_access_method.py @@ -54,7 +54,7 @@ class Errors(ResourceMapping): message: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -85,12 +85,12 @@ class From(ResourceMapping): :ivar starts_at: Previous start time for access.""" - device_ids: List[str] - ends_at: str - starts_at: str + device_ids: Optional[List[str]] + ends_at: Optional[str] + starts_at: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_ids=d.get("device_ids", None), ends_at=d.get("ends_at", None), @@ -107,12 +107,12 @@ class To(ResourceMapping): :ivar starts_at: New start time for access.""" - device_ids: List[str] - ends_at: str - starts_at: str + device_ids: Optional[List[str]] + ends_at: Optional[str] + starts_at: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( device_ids=d.get("device_ids", None), ends_at=d.get("ends_at", None), @@ -120,13 +120,13 @@ def from_dict(cls, d: Dict[str, Any]): ) created_at: str - from_: From + from_: Optional[From] message: str mutation_code: str - to: To + to: Optional[To] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), from_=( @@ -155,10 +155,10 @@ class Warnings(ResourceMapping): created_at: str message: str warning_code: str - original_access_method_id: str + original_access_method_id: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -167,23 +167,23 @@ def from_dict(cls, d: Dict[str, Any]): ) access_method_id: str - code: str + code: Optional[str] created_at: str display_name: str errors: List[Errors] - is_assignment_required: bool - is_encoding_required: bool + is_assignment_required: Optional[bool] + is_encoding_required: Optional[bool] is_issued: bool - is_ready_for_assignment: bool - is_ready_for_encoding: bool - issued_at: str + is_ready_for_assignment: Optional[bool] + is_ready_for_encoding: Optional[bool] + issued_at: Optional[str] mode: str pending_mutations: List[PendingMutations] warnings: List[Warnings] workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( access_method_id=d.get("access_method_id", None), code=d.get("code", None), diff --git a/seam/resources/unmanaged_device.py b/seam/resources/unmanaged_device.py index 34d4eff7..15ba2dcc 100644 --- a/seam/resources/unmanaged_device.py +++ b/seam/resources/unmanaged_device.py @@ -92,13 +92,13 @@ class Errors(ResourceMapping): created_at: str error_code: str - is_connected_account_error: bool - is_device_error: bool + is_connected_account_error: Optional[bool] + is_device_error: Optional[bool] message: str - is_bridge_error: bool + is_bridge_error: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), error_code=d.get("error_code", None), @@ -114,19 +114,23 @@ class Location(ResourceMapping): :ivar location_name: Name of the device location. + :ivar room_name: Name of the room within the device location, when the provider reports one. + :ivar time_zone: Time zone of the device location. :ivar timezone: Deprecated: Use ``time_zone`` instead. Time zone of the device location. """ - location_name: str - time_zone: str - timezone: str + location_name: Optional[str] + room_name: Optional[str] + time_zone: Optional[str] + timezone: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( location_name=d.get("location_name", None), + room_name=d.get("room_name", None), time_zone=d.get("time_zone", None), timezone=d.get("timezone", None), ) @@ -176,16 +180,16 @@ class Battery(ResourceMapping): level: float @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( level=d.get("level", None), ) - battery: Battery + battery: Optional[Battery] is_connected: bool @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( battery=( cls.Battery.from_dict(d.get("battery")) @@ -208,7 +212,7 @@ class Battery(ResourceMapping): status: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( level=d.get("level", None), status=d.get("status", None), @@ -233,16 +237,16 @@ class Model(ResourceMapping): :ivar online_access_codes_supported: Deprecated: use device.can_program_online_access_codes. """ - accessory_keypad_supported: bool - can_connect_accessory_keypad: bool + accessory_keypad_supported: Optional[bool] + can_connect_accessory_keypad: Optional[bool] display_name: str - has_built_in_keypad: bool + has_built_in_keypad: Optional[bool] manufacturer_display_name: str - offline_access_codes_supported: bool - online_access_codes_supported: bool + offline_access_codes_supported: Optional[bool] + online_access_codes_supported: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( accessory_keypad_supported=d.get( "accessory_keypad_supported", None @@ -261,20 +265,20 @@ def from_dict(cls, d: Dict[str, Any]): ), ) - accessory_keypad: AccessoryKeypad - battery: Battery - battery_level: float - image_alt_text: str - image_url: str - manufacturer: str - model: Model + accessory_keypad: Optional[AccessoryKeypad] + battery: Optional[Battery] + battery_level: Optional[float] + image_alt_text: Optional[str] + image_url: Optional[str] + manufacturer: Optional[str] + model: Optional[Model] name: str - offline_access_codes_enabled: bool + offline_access_codes_enabled: Optional[bool] online: bool - online_access_codes_enabled: bool + online_access_codes_enabled: Optional[bool] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( accessory_keypad=( cls.AccessoryKeypad.from_dict(d.get("accessory_keypad")) @@ -321,11 +325,11 @@ class Warnings(ResourceMapping): created_at: str message: str warning_code: str - active_access_code_count: int - max_active_access_code_count: int + active_access_code_count: Optional[int] + max_active_access_code_count: Optional[int] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -336,26 +340,26 @@ def from_dict(cls, d: Dict[str, Any]): ), ) - can_configure_auto_lock: bool - can_hvac_cool: bool - can_hvac_heat: bool - can_hvac_heat_cool: bool - can_program_offline_access_codes: bool - can_program_online_access_codes: bool - can_program_thermostat_programs_as_different_each_day: bool - can_program_thermostat_programs_as_same_each_day: bool - can_program_thermostat_programs_as_weekday_weekend: bool - can_remotely_lock: bool - can_remotely_unlock: bool - can_run_thermostat_programs: bool - can_simulate_connection: bool - can_simulate_disconnection: bool - can_simulate_hub_connection: bool - can_simulate_hub_disconnection: bool - can_simulate_paid_subscription: bool - can_simulate_removal: bool - can_turn_off_hvac: bool - can_unlock_with_code: bool + can_configure_auto_lock: Optional[bool] + can_hvac_cool: Optional[bool] + can_hvac_heat: Optional[bool] + can_hvac_heat_cool: Optional[bool] + can_program_offline_access_codes: Optional[bool] + can_program_online_access_codes: Optional[bool] + can_program_thermostat_programs_as_different_each_day: Optional[bool] + can_program_thermostat_programs_as_same_each_day: Optional[bool] + can_program_thermostat_programs_as_weekday_weekend: Optional[bool] + can_remotely_lock: Optional[bool] + can_remotely_unlock: Optional[bool] + can_run_thermostat_programs: Optional[bool] + can_simulate_connection: Optional[bool] + can_simulate_disconnection: Optional[bool] + can_simulate_hub_connection: Optional[bool] + can_simulate_hub_disconnection: Optional[bool] + can_simulate_paid_subscription: Optional[bool] + can_simulate_removal: Optional[bool] + can_turn_off_hvac: Optional[bool] + can_unlock_with_code: Optional[bool] capabilities_supported: List[str] connected_account_id: str created_at: str @@ -364,13 +368,13 @@ def from_dict(cls, d: Dict[str, Any]): device_type: str errors: List[Errors] is_managed: bool - location: Location - properties: Properties + location: Optional[Location] + properties: Optional[Properties] warnings: List[Warnings] workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( can_configure_auto_lock=d.get("can_configure_auto_lock", None), can_hvac_cool=d.get("can_hvac_cool", None), diff --git a/seam/resources/unmanaged_user_identity.py b/seam/resources/unmanaged_user_identity.py index 307cc2ea..6905f5af 100644 --- a/seam/resources/unmanaged_user_identity.py +++ b/seam/resources/unmanaged_user_identity.py @@ -50,7 +50,7 @@ class Errors(ResourceMapping): message: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( acs_system_id=d.get("acs_system_id", None), acs_user_id=d.get("acs_user_id", None), @@ -75,7 +75,7 @@ class Warnings(ResourceMapping): warning_code: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -85,16 +85,16 @@ def from_dict(cls, d: Dict[str, Any]): acs_user_ids: List[str] created_at: str display_name: str - email_address: str + email_address: Optional[str] errors: List[Errors] - full_name: str - phone_number: str + full_name: Optional[str] + phone_number: Optional[str] user_identity_id: str warnings: List[Warnings] workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( acs_user_ids=d.get("acs_user_ids", None), created_at=d.get("created_at", None), diff --git a/seam/resources/user_identity.py b/seam/resources/user_identity.py index d1693265..141a82a3 100644 --- a/seam/resources/user_identity.py +++ b/seam/resources/user_identity.py @@ -52,7 +52,7 @@ class Errors(ResourceMapping): message: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( acs_system_id=d.get("acs_system_id", None), acs_user_id=d.get("acs_user_id", None), @@ -77,7 +77,7 @@ class Warnings(ResourceMapping): warning_code: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( created_at=d.get("created_at", None), message=d.get("message", None), @@ -87,17 +87,17 @@ def from_dict(cls, d: Dict[str, Any]): acs_user_ids: List[str] created_at: str display_name: str - email_address: str + email_address: Optional[str] errors: List[Errors] - full_name: str - phone_number: str + full_name: Optional[str] + phone_number: Optional[str] user_identity_id: str - user_identity_key: str + user_identity_key: Optional[str] warnings: List[Warnings] workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( acs_user_ids=d.get("acs_user_ids", None), created_at=d.get("created_at", None), diff --git a/seam/resources/webhook.py b/seam/resources/webhook.py index fba1c282..137e031b 100644 --- a/seam/resources/webhook.py +++ b/seam/resources/webhook.py @@ -16,13 +16,13 @@ class Webhook: :ivar webhook_id: ID of the webhook.""" - event_types: List[str] - secret: str + event_types: Optional[List[str]] + secret: Optional[str] url: str webhook_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( event_types=d.get("event_types", None), secret=d.get("secret", None), diff --git a/seam/resources/workspace.py b/seam/resources/workspace.py index 2c89663c..bbc0e01e 100644 --- a/seam/resources/workspace.py +++ b/seam/resources/workspace.py @@ -43,14 +43,14 @@ class ConnectWebviewCustomization(ResourceMapping): :ivar success_message: Success message for `Connect Webviews `_ in the workspace. See also `Customize the Look and Feel of Your Connect Webviews `_. """ - inviter_logo_url: str - logo_shape: str - primary_button_color: str - primary_button_text_color: str - success_message: str + inviter_logo_url: Optional[str] + logo_shape: Optional[str] + primary_button_color: Optional[str] + primary_button_text_color: Optional[str] + success_message: Optional[str] @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( inviter_logo_url=d.get("inviter_logo_url", None), logo_shape=d.get("logo_shape", None), @@ -60,18 +60,18 @@ def from_dict(cls, d: Dict[str, Any]): ) company_name: str - connect_partner_name: str - connect_webview_customization: ConnectWebviewCustomization + connect_partner_name: Optional[str] + connect_webview_customization: Optional[ConnectWebviewCustomization] is_publishable_key_auth_enabled: bool is_sandbox: bool is_suspended: bool name: str - organization_id: str - publishable_key: str + organization_id: Optional[str] + publishable_key: Optional[str] workspace_id: str @classmethod - def from_dict(cls, d: Dict[str, Any]): + def from_dict(cls, d: Any): return cls( company_name=d.get("company_name", None), connect_partner_name=d.get("connect_partner_name", None), diff --git a/seam/route.py b/seam/route.py new file mode 100644 index 00000000..59f71fb8 --- /dev/null +++ b/seam/route.py @@ -0,0 +1,17 @@ +from typing import Any, Callable, TypeVar, cast + +F = TypeVar("F", bound=Callable) + + +def route_metadata(*, path: str, has_required_parameters: bool, has_pagination: bool): + """Attach generated route metadata to a request callable.""" + + def decorate(request: F) -> F: + # Functions do not declare these attributes, so set them through Any. + route = cast(Any, request) + route.__seam_path__ = path + route.__seam_has_required_parameters__ = has_required_parameters + route.__seam_has_pagination__ = has_pagination + return request + + return decorate diff --git a/seam/routes/access_codes.py b/seam/routes/access_codes.py index a194118e..3f5053f5 100644 --- a/seam/routes/access_codes.py +++ b/seam/routes/access_codes.py @@ -1,6 +1,8 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +from ..null import Null from ..resources import AccessCode from .access_codes_simulate import AbstractAccessCodesSimulate, AccessCodesSimulate from .access_codes_unmanaged import AbstractAccessCodesUnmanaged, AccessCodesUnmanaged @@ -37,7 +39,7 @@ def create( preferred_code_length: Optional[float] = None, starts_at: Optional[str] = None, use_backup_access_code_pool: Optional[bool] = None, - use_offline_access_code: Optional[bool] = None + use_offline_access_code: Optional[bool] = None, ) -> AccessCode: """Creates a new `access code `_. For granting access, we recommend `Access Grants `_ instead: they work across both standalone smart locks and access control systems and manage the underlying codes for you. Use this low-level endpoint only when you need direct control over a code on a single device, such as setting a custom PIN value. @@ -79,7 +81,9 @@ def create( :param use_offline_access_code: Deprecated: Use ``is_offline_access_code`` instead. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -97,7 +101,7 @@ def create_multiple( prefer_native_scheduling: Optional[bool] = None, preferred_code_length: Optional[float] = None, starts_at: Optional[str] = None, - use_backup_access_code_pool: Optional[bool] = None + use_backup_access_code_pool: Optional[bool] = None, ) -> List[AccessCode]: """Creates new `access codes `_ that share a common code across multiple devices. @@ -141,7 +145,9 @@ def create_multiple( :param use_backup_access_code_pool: Indicates whether to use a `backup access code pool `_ provided by Seam. If ``true``, you can use ```/access_codes/pull_backup_access_code`` `_. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -151,7 +157,8 @@ def delete(self, *, access_code_id: str, device_id: Optional[str] = None) -> Non :param access_code_id: ID of the access code that you want to delete. :param device_id: ID of the device for which you want to delete the access code. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -160,7 +167,9 @@ def generate_code(self, *, device_id: str) -> AccessCode: :param device_id: ID of the device for which you want to generate a code. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -169,7 +178,7 @@ def get( *, access_code_id: Optional[str] = None, code: Optional[str] = None, - device_id: Optional[str] = None + device_id: Optional[str] = None, ) -> AccessCode: """Returns a specified `access code `_. @@ -181,7 +190,9 @@ def get( :param device_id: ID of the device containing the access code that you want to get. You must specify either ``access_code_id`` or both ``device_id`` and ``code``. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -195,9 +206,9 @@ def list( customer_key: Optional[str] = None, device_id: Optional[str] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, - user_identifier_key: Optional[str] = None + user_identifier_key: Optional[str] = None, ) -> List[AccessCode]: """Returns a list of all `access codes `_. @@ -223,7 +234,9 @@ def list( :param user_identifier_key: Your user ID for the user by which to filter access codes. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -240,7 +253,9 @@ def pull_backup_access_code(self, *, access_code_id: str) -> AccessCode: :param access_code_id: ID of the access code for which you want to pull a backup access code. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -250,7 +265,7 @@ def report_device_constraints( device_id: str, max_code_length: Optional[int] = None, min_code_length: Optional[int] = None, - supported_code_lengths: Optional[List[float]] = None + supported_code_lengths: Optional[List[float]] = None, ) -> None: """Enables you to report access code-related constraints for a device. Currently, supports reporting supported code length constraints for SmartThings devices. @@ -263,7 +278,8 @@ def report_device_constraints( :param min_code_length: Minimum supported code length as an integer between 4 and 20, inclusive. You can specify either ``min_code_length``/``max_code_length`` or ``supported_code_lengths``. :param supported_code_lengths: Array of supported code lengths as integers between 4 and 20, inclusive. You can specify either ``supported_code_lengths`` or ``min_code_length``/``max_code_length``. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -278,16 +294,9 @@ def update( ends_at: Optional[str] = None, is_external_modification_allowed: Optional[bool] = None, is_managed: Optional[bool] = None, - is_offline_access_code: Optional[bool] = None, - is_one_time_use: Optional[bool] = None, - max_time_rounding: Optional[str] = None, name: Optional[str] = None, - prefer_native_scheduling: Optional[bool] = None, - preferred_code_length: Optional[float] = None, starts_at: Optional[str] = None, type: Optional[str] = None, - use_backup_access_code_pool: Optional[bool] = None, - use_offline_access_code: Optional[bool] = None ) -> None: """Updates a specified active or upcoming `access code `_. @@ -309,12 +318,6 @@ def update( :param is_managed: Indicates whether the access code is managed through Seam. Note that to convert an unmanaged access code into a managed access code, use ``/access_codes/unmanaged/convert_to_managed``. - :param is_offline_access_code: Indicates whether the access code is an `offline access code `_. - - :param is_one_time_use: Indicates whether the `offline access code `_ is a single-use access code. - - :param max_time_rounding: Maximum rounding adjustment. To create a daily-bound `offline access code `_ for devices that support this feature, set this parameter to ``1d``. - :param name: Name of the new access code. Enables administrators and users to identify the access code easily, especially when there are numerous access codes. Note that the name provided on Seam is used to identify the code on Seam and is not necessarily the name that will appear in the lock provider's app or on the device. This is because lock providers may have constraints on names, such as length, uniqueness, or characters that can be used. In addition, some lock providers may break down names into components such as ``first_name`` and ``last_name``. @@ -323,18 +326,11 @@ def update( To help your users identify codes set by Seam, Seam provides the name exactly as it appears on the lock provider's app or on the device as a separate property called ``appearance``. This is an object with a ``name`` property and, optionally, ``first_name`` and ``last_name`` properties (for providers that break down a name into components). - :param prefer_native_scheduling: Indicates whether `native scheduling `_ should be used for time-bound codes when supported by the provider. Default: ``true``. - - :param preferred_code_length: Preferred code length. Only applicable if you do not specify a ``code``. If the affected device does not support the preferred code length, Seam reverts to using the shortest supported code length. - :param starts_at: Date and time at which the validity of the new access code starts, in `ISO 8601 `_ format. :param type: Type to which you want to convert the access code. To convert a time-bound access code to an ongoing access code, set ``type`` to ``ongoing``. See also `Changing a time-bound access code to permanent access `_. - :param use_backup_access_code_pool: Indicates whether to use a `backup access code pool `_ provided by Seam. If ``true``, you can use ```/access_codes/pull_backup_access_code`` `_. - - :param use_offline_access_code: Deprecated: Use ``is_offline_access_code`` instead. - """ + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -344,7 +340,7 @@ def update_multiple( common_code_key: str, ends_at: Optional[str] = None, name: Optional[str] = None, - starts_at: Optional[str] = None + starts_at: Optional[str] = None, ) -> None: """Updates `access codes `_ that share a common code across multiple devices. @@ -365,7 +361,8 @@ def update_multiple( To help your users identify codes set by Seam, Seam provides the name exactly as it appears on the lock provider's app or on the device as a separate property called ``appearance``. This is an object with a ``name`` property and, optionally, ``first_name`` and ``last_name`` properties (for providers that break down a name into components). :param starts_at: Date and time at which the validity of the new access code starts, in `ISO 8601 `_ format. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -384,6 +381,9 @@ def simulate(self) -> AccessCodesSimulate: def unmanaged(self) -> AccessCodesUnmanaged: return self._unmanaged + @route_metadata( + path="/access_codes/create", has_required_parameters=True, has_pagination=False + ) def create( self, *, @@ -402,7 +402,7 @@ def create( preferred_code_length: Optional[float] = None, starts_at: Optional[str] = None, use_backup_access_code_pool: Optional[bool] = None, - use_offline_access_code: Optional[bool] = None + use_offline_access_code: Optional[bool] = None, ) -> AccessCode: """Creates a new `access code `_. For granting access, we recommend `Access Grants `_ instead: they work across both standalone smart locks and access control systems and manage the underlying codes for you. Use this low-level endpoint only when you need direct control over a code on a single device, such as setting a custom PIN value. @@ -444,8 +444,10 @@ def create( :param use_offline_access_code: Deprecated: Use ``is_offline_access_code`` instead. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id @@ -482,10 +484,20 @@ def create( if use_offline_access_code is not None: json_payload["use_offline_access_code"] = use_offline_access_code + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_codes/create" + ) + res = self.client.post("/access_codes/create", json=json_payload) return AccessCode.from_dict(res["access_code"]) + @route_metadata( + path="/access_codes/create_multiple", + has_required_parameters=True, + has_pagination=False, + ) def create_multiple( self, *, @@ -500,7 +512,7 @@ def create_multiple( prefer_native_scheduling: Optional[bool] = None, preferred_code_length: Optional[float] = None, starts_at: Optional[str] = None, - use_backup_access_code_pool: Optional[bool] = None + use_backup_access_code_pool: Optional[bool] = None, ) -> List[AccessCode]: """Creates new `access codes `_ that share a common code across multiple devices. @@ -544,8 +556,10 @@ def create_multiple( :param use_backup_access_code_pool: Indicates whether to use a `backup access code pool `_ provided by Seam. If ``true``, you can use ```/access_codes/pull_backup_access_code`` `_. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_ids is not None: json_payload["device_ids"] = device_ids @@ -576,49 +590,78 @@ def create_multiple( if use_backup_access_code_pool is not None: json_payload["use_backup_access_code_pool"] = use_backup_access_code_pool - res = self.client.post("/access_codes/create_multiple", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_codes/create_multiple" + ) + + res = self.client.put("/access_codes/create_multiple", json=json_payload) return [AccessCode.from_dict(item) for item in res["access_codes"]] + @route_metadata( + path="/access_codes/delete", has_required_parameters=True, has_pagination=False + ) def delete(self, *, access_code_id: str, device_id: Optional[str] = None) -> None: """Deletes an `access code `_. :param access_code_id: ID of the access code that you want to delete. :param device_id: ID of the device for which you want to delete the access code. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if access_code_id is not None: - json_payload["access_code_id"] = access_code_id + params["access_code_id"] = access_code_id if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id + + if not params: + raise ValueError( + "At least one parameter is required for /access_codes/delete" + ) - self.client.post("/access_codes/delete", json=json_payload) + self.client.delete("/access_codes/delete", params=params) return None + @route_metadata( + path="/access_codes/generate_code", + has_required_parameters=True, + has_pagination=False, + ) def generate_code(self, *, device_id: str) -> AccessCode: """Generates a code for an `access code `_, given a device ID. :param device_id: ID of the device for which you want to generate a code. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id - res = self.client.post("/access_codes/generate_code", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /access_codes/generate_code" + ) + + res = self.client.get("/access_codes/generate_code", params=params) return AccessCode.from_dict(res["generated_code"]) + @route_metadata( + path="/access_codes/get", has_required_parameters=True, has_pagination=False + ) def get( self, *, access_code_id: Optional[str] = None, code: Optional[str] = None, - device_id: Optional[str] = None + device_id: Optional[str] = None, ) -> AccessCode: """Returns a specified `access code `_. @@ -630,20 +673,28 @@ def get( :param device_id: ID of the device containing the access code that you want to get. You must specify either ``access_code_id`` or both ``device_id`` and ``code``. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if access_code_id is not None: - json_payload["access_code_id"] = access_code_id + params["access_code_id"] = access_code_id if code is not None: - json_payload["code"] = code + params["code"] = code if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id + + if not params: + raise ValueError("At least one parameter is required for /access_codes/get") - res = self.client.post("/access_codes/get", json=json_payload) + res = self.client.get("/access_codes/get", params=params) return AccessCode.from_dict(res["access_code"]) + @route_metadata( + path="/access_codes/list", has_required_parameters=True, has_pagination=True + ) def list( self, *, @@ -654,9 +705,9 @@ def list( customer_key: Optional[str] = None, device_id: Optional[str] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, - user_identifier_key: Optional[str] = None + user_identifier_key: Optional[str] = None, ) -> List[AccessCode]: """Returns a list of all `access codes `_. @@ -682,8 +733,10 @@ def list( :param user_identifier_key: Your user ID for the user by which to filter access codes. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if access_code_ids is not None: json_payload["access_code_ids"] = access_code_ids @@ -706,10 +759,20 @@ def list( if user_identifier_key is not None: json_payload["user_identifier_key"] = user_identifier_key + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_codes/list" + ) + res = self.client.post("/access_codes/list", json=json_payload) return [AccessCode.from_dict(item) for item in res["access_codes"]] + @route_metadata( + path="/access_codes/pull_backup_access_code", + has_required_parameters=True, + has_pagination=False, + ) def pull_backup_access_code(self, *, access_code_id: str) -> AccessCode: """Retrieves a backup access code for an `access code `_. See also `Managing Backup Access Codes `_. @@ -723,25 +786,37 @@ def pull_backup_access_code(self, *, access_code_id: str) -> AccessCode: :param access_code_id: ID of the access code for which you want to pull a backup access code. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if access_code_id is not None: json_payload["access_code_id"] = access_code_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_codes/pull_backup_access_code" + ) + res = self.client.post( "/access_codes/pull_backup_access_code", json=json_payload ) return AccessCode.from_dict(res["access_code"]) + @route_metadata( + path="/access_codes/report_device_constraints", + has_required_parameters=True, + has_pagination=False, + ) def report_device_constraints( self, *, device_id: str, max_code_length: Optional[int] = None, min_code_length: Optional[int] = None, - supported_code_lengths: Optional[List[float]] = None + supported_code_lengths: Optional[List[float]] = None, ) -> None: """Enables you to report access code-related constraints for a device. Currently, supports reporting supported code length constraints for SmartThings devices. @@ -754,8 +829,9 @@ def report_device_constraints( :param min_code_length: Minimum supported code length as an integer between 4 and 20, inclusive. You can specify either ``min_code_length``/``max_code_length`` or ``supported_code_lengths``. :param supported_code_lengths: Array of supported code lengths as integers between 4 and 20, inclusive. You can specify either ``supported_code_lengths`` or ``min_code_length``/``max_code_length``. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id @@ -766,10 +842,18 @@ def report_device_constraints( if supported_code_lengths is not None: json_payload["supported_code_lengths"] = supported_code_lengths + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_codes/report_device_constraints" + ) + self.client.post("/access_codes/report_device_constraints", json=json_payload) return None + @route_metadata( + path="/access_codes/update", has_required_parameters=True, has_pagination=False + ) def update( self, *, @@ -781,16 +865,9 @@ def update( ends_at: Optional[str] = None, is_external_modification_allowed: Optional[bool] = None, is_managed: Optional[bool] = None, - is_offline_access_code: Optional[bool] = None, - is_one_time_use: Optional[bool] = None, - max_time_rounding: Optional[str] = None, name: Optional[str] = None, - prefer_native_scheduling: Optional[bool] = None, - preferred_code_length: Optional[float] = None, starts_at: Optional[str] = None, type: Optional[str] = None, - use_backup_access_code_pool: Optional[bool] = None, - use_offline_access_code: Optional[bool] = None ) -> None: """Updates a specified active or upcoming `access code `_. @@ -812,12 +889,6 @@ def update( :param is_managed: Indicates whether the access code is managed through Seam. Note that to convert an unmanaged access code into a managed access code, use ``/access_codes/unmanaged/convert_to_managed``. - :param is_offline_access_code: Indicates whether the access code is an `offline access code `_. - - :param is_one_time_use: Indicates whether the `offline access code `_ is a single-use access code. - - :param max_time_rounding: Maximum rounding adjustment. To create a daily-bound `offline access code `_ for devices that support this feature, set this parameter to ``1d``. - :param name: Name of the new access code. Enables administrators and users to identify the access code easily, especially when there are numerous access codes. Note that the name provided on Seam is used to identify the code on Seam and is not necessarily the name that will appear in the lock provider's app or on the device. This is because lock providers may have constraints on names, such as length, uniqueness, or characters that can be used. In addition, some lock providers may break down names into components such as ``first_name`` and ``last_name``. @@ -826,19 +897,12 @@ def update( To help your users identify codes set by Seam, Seam provides the name exactly as it appears on the lock provider's app or on the device as a separate property called ``appearance``. This is an object with a ``name`` property and, optionally, ``first_name`` and ``last_name`` properties (for providers that break down a name into components). - :param prefer_native_scheduling: Indicates whether `native scheduling `_ should be used for time-bound codes when supported by the provider. Default: ``true``. - - :param preferred_code_length: Preferred code length. Only applicable if you do not specify a ``code``. If the affected device does not support the preferred code length, Seam reverts to using the shortest supported code length. - :param starts_at: Date and time at which the validity of the new access code starts, in `ISO 8601 `_ format. :param type: Type to which you want to convert the access code. To convert a time-bound access code to an ongoing access code, set ``type`` to ``ongoing``. See also `Changing a time-bound access code to permanent access `_. - :param use_backup_access_code_pool: Indicates whether to use a `backup access code pool `_ provided by Seam. If ``true``, you can use ```/access_codes/pull_backup_access_code`` `_. - - :param use_offline_access_code: Deprecated: Use ``is_offline_access_code`` instead. - """ - json_payload = {} + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if access_code_id is not None: json_payload["access_code_id"] = access_code_id @@ -858,38 +922,34 @@ def update( ) if is_managed is not None: json_payload["is_managed"] = is_managed - if is_offline_access_code is not None: - json_payload["is_offline_access_code"] = is_offline_access_code - if is_one_time_use is not None: - json_payload["is_one_time_use"] = is_one_time_use - if max_time_rounding is not None: - json_payload["max_time_rounding"] = max_time_rounding if name is not None: json_payload["name"] = name - if prefer_native_scheduling is not None: - json_payload["prefer_native_scheduling"] = prefer_native_scheduling - if preferred_code_length is not None: - json_payload["preferred_code_length"] = preferred_code_length if starts_at is not None: json_payload["starts_at"] = starts_at if type is not None: json_payload["type"] = type - if use_backup_access_code_pool is not None: - json_payload["use_backup_access_code_pool"] = use_backup_access_code_pool - if use_offline_access_code is not None: - json_payload["use_offline_access_code"] = use_offline_access_code - self.client.post("/access_codes/update", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_codes/update" + ) + + self.client.put("/access_codes/update", json=json_payload) return None + @route_metadata( + path="/access_codes/update_multiple", + has_required_parameters=True, + has_pagination=False, + ) def update_multiple( self, *, common_code_key: str, ends_at: Optional[str] = None, name: Optional[str] = None, - starts_at: Optional[str] = None + starts_at: Optional[str] = None, ) -> None: """Updates `access codes `_ that share a common code across multiple devices. @@ -910,8 +970,9 @@ def update_multiple( To help your users identify codes set by Seam, Seam provides the name exactly as it appears on the lock provider's app or on the device as a separate property called ``appearance``. This is an object with a ``name`` property and, optionally, ``first_name`` and ``last_name`` properties (for providers that break down a name into components). :param starts_at: Date and time at which the validity of the new access code starts, in `ISO 8601 `_ format. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if common_code_key is not None: json_payload["common_code_key"] = common_code_key @@ -922,6 +983,11 @@ def update_multiple( if starts_at is not None: json_payload["starts_at"] = starts_at - self.client.post("/access_codes/update_multiple", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_codes/update_multiple" + ) + + self.client.patch("/access_codes/update_multiple", json=json_payload) return None diff --git a/seam/routes/access_codes_simulate.py b/seam/routes/access_codes_simulate.py index 048c13fb..4c4c756c 100644 --- a/seam/routes/access_codes_simulate.py +++ b/seam/routes/access_codes_simulate.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata from ..resources import UnmanagedAccessCode @@ -18,7 +19,9 @@ def create_unmanaged_access_code( :param name: Name of the simulated unmanaged access code. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -27,6 +30,11 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/access_codes/simulate/create_unmanaged_access_code", + has_required_parameters=True, + has_pagination=False, + ) def create_unmanaged_access_code( self, *, code: str, device_id: str, name: str ) -> UnmanagedAccessCode: @@ -38,8 +46,10 @@ def create_unmanaged_access_code( :param name: Name of the simulated unmanaged access code. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if code is not None: json_payload["code"] = code @@ -48,6 +58,11 @@ def create_unmanaged_access_code( if name is not None: json_payload["name"] = name + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_codes/simulate/create_unmanaged_access_code" + ) + res = self.client.post( "/access_codes/simulate/create_unmanaged_access_code", json=json_payload ) diff --git a/seam/routes/access_codes_unmanaged.py b/seam/routes/access_codes_unmanaged.py index ac90e4ee..8a6e67d4 100644 --- a/seam/routes/access_codes_unmanaged.py +++ b/seam/routes/access_codes_unmanaged.py @@ -1,6 +1,8 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +from ..null import Null from ..resources import UnmanagedAccessCode @@ -13,7 +15,7 @@ def convert_to_managed( access_code_id: str, allow_external_modification: Optional[bool] = None, force: Optional[bool] = None, - is_external_modification_allowed: Optional[bool] = None + is_external_modification_allowed: Optional[bool] = None, ) -> None: """Converts an `unmanaged access code `_ to an `access code managed through Seam `_. @@ -28,7 +30,8 @@ def convert_to_managed( :param force: Indicates whether to force the access code conversion. To switch management of an access code from one Seam workspace to another, set ``force`` to ``true``. :param is_external_modification_allowed: Indicates whether `external modification `_ of the access code is allowed. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -36,7 +39,8 @@ def delete(self, *, access_code_id: str) -> None: """Deletes an `unmanaged access code `_. :param access_code_id: ID of the unmanaged access code that you want to delete. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -45,7 +49,7 @@ def get( *, access_code_id: Optional[str] = None, code: Optional[str] = None, - device_id: Optional[str] = None + device_id: Optional[str] = None, ) -> UnmanagedAccessCode: """Returns a specified `unmanaged access code `_. @@ -57,7 +61,9 @@ def get( :param device_id: ID of the device containing the unmanaged access code that you want to get. You must specify either ``access_code_id`` or both ``device_id`` and ``code``. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -66,9 +72,9 @@ def list( *, device_id: str, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, - user_identifier_key: Optional[str] = None + user_identifier_key: Optional[str] = None, ) -> List[UnmanagedAccessCode]: """Returns a list of all `unmanaged access codes `_. @@ -82,7 +88,9 @@ def list( :param user_identifier_key: Your user ID for the user by which to filter unmanaged access codes. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -93,7 +101,7 @@ def update( is_managed: bool, allow_external_modification: Optional[bool] = None, force: Optional[bool] = None, - is_external_modification_allowed: Optional[bool] = None + is_external_modification_allowed: Optional[bool] = None, ) -> None: """Updates a specified `unmanaged access code `_. @@ -106,7 +114,8 @@ def update( :param force: Indicates whether to force the unmanaged access code update. :param is_external_modification_allowed: Indicates whether `external modification `_ of the code is allowed. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -115,13 +124,18 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/access_codes/unmanaged/convert_to_managed", + has_required_parameters=True, + has_pagination=False, + ) def convert_to_managed( self, *, access_code_id: str, allow_external_modification: Optional[bool] = None, force: Optional[bool] = None, - is_external_modification_allowed: Optional[bool] = None + is_external_modification_allowed: Optional[bool] = None, ) -> None: """Converts an `unmanaged access code `_ to an `access code managed through Seam `_. @@ -136,8 +150,9 @@ def convert_to_managed( :param force: Indicates whether to force the access code conversion. To switch management of an access code from one Seam workspace to another, set ``force`` to ``true``. :param is_external_modification_allowed: Indicates whether `external modification `_ of the access code is allowed. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if access_code_id is not None: json_payload["access_code_id"] = access_code_id @@ -150,32 +165,53 @@ def convert_to_managed( is_external_modification_allowed ) - self.client.post( + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_codes/unmanaged/convert_to_managed" + ) + + self.client.patch( "/access_codes/unmanaged/convert_to_managed", json=json_payload ) return None + @route_metadata( + path="/access_codes/unmanaged/delete", + has_required_parameters=True, + has_pagination=False, + ) def delete(self, *, access_code_id: str) -> None: """Deletes an `unmanaged access code `_. :param access_code_id: ID of the unmanaged access code that you want to delete. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if access_code_id is not None: - json_payload["access_code_id"] = access_code_id + params["access_code_id"] = access_code_id + + if not params: + raise ValueError( + "At least one parameter is required for /access_codes/unmanaged/delete" + ) - self.client.post("/access_codes/unmanaged/delete", json=json_payload) + self.client.delete("/access_codes/unmanaged/delete", params=params) return None + @route_metadata( + path="/access_codes/unmanaged/get", + has_required_parameters=True, + has_pagination=False, + ) def get( self, *, access_code_id: Optional[str] = None, code: Optional[str] = None, - device_id: Optional[str] = None + device_id: Optional[str] = None, ) -> UnmanagedAccessCode: """Returns a specified `unmanaged access code `_. @@ -187,28 +223,40 @@ def get( :param device_id: ID of the device containing the unmanaged access code that you want to get. You must specify either ``access_code_id`` or both ``device_id`` and ``code``. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if access_code_id is not None: - json_payload["access_code_id"] = access_code_id + params["access_code_id"] = access_code_id if code is not None: - json_payload["code"] = code + params["code"] = code if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id - res = self.client.post("/access_codes/unmanaged/get", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /access_codes/unmanaged/get" + ) + + res = self.client.get("/access_codes/unmanaged/get", params=params) return UnmanagedAccessCode.from_dict(res["access_code"]) + @route_metadata( + path="/access_codes/unmanaged/list", + has_required_parameters=True, + has_pagination=True, + ) def list( self, *, device_id: str, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, - user_identifier_key: Optional[str] = None + user_identifier_key: Optional[str] = None, ) -> List[UnmanagedAccessCode]: """Returns a list of all `unmanaged access codes `_. @@ -222,24 +270,36 @@ def list( :param user_identifier_key: Your user ID for the user by which to filter unmanaged access codes. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if search is not None: - json_payload["search"] = search + params["search"] = search if user_identifier_key is not None: - json_payload["user_identifier_key"] = user_identifier_key + params["user_identifier_key"] = user_identifier_key - res = self.client.post("/access_codes/unmanaged/list", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /access_codes/unmanaged/list" + ) + + res = self.client.get("/access_codes/unmanaged/list", params=params) return [UnmanagedAccessCode.from_dict(item) for item in res["access_codes"]] + @route_metadata( + path="/access_codes/unmanaged/update", + has_required_parameters=True, + has_pagination=False, + ) def update( self, *, @@ -247,7 +307,7 @@ def update( is_managed: bool, allow_external_modification: Optional[bool] = None, force: Optional[bool] = None, - is_external_modification_allowed: Optional[bool] = None + is_external_modification_allowed: Optional[bool] = None, ) -> None: """Updates a specified `unmanaged access code `_. @@ -260,8 +320,9 @@ def update( :param force: Indicates whether to force the unmanaged access code update. :param is_external_modification_allowed: Indicates whether `external modification `_ of the code is allowed. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if access_code_id is not None: json_payload["access_code_id"] = access_code_id @@ -276,6 +337,11 @@ def update( is_external_modification_allowed ) - self.client.post("/access_codes/unmanaged/update", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_codes/unmanaged/update" + ) + + self.client.patch("/access_codes/unmanaged/update", json=json_payload) return None diff --git a/seam/routes/access_grants.py b/seam/routes/access_grants.py index f6835ebd..fbc7685a 100644 --- a/seam/routes/access_grants.py +++ b/seam/routes/access_grants.py @@ -1,6 +1,8 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +from ..null import Null from ..resources import AccessGrant, Batch from .access_grants_unmanaged import ( AbstractAccessGrantsUnmanaged, @@ -26,14 +28,14 @@ def create( acs_entrance_ids: Optional[List[str]] = None, customization_profile_id: Optional[str] = None, device_ids: Optional[List[str]] = None, - ends_at: Optional[str] = None, + ends_at: Optional[Union[str, Null]] = None, location: Optional[Dict[str, Any]] = None, location_ids: Optional[List[str]] = None, - name: Optional[str] = None, + name: Optional[Union[str, Null]] = None, reservation_key: Optional[str] = None, space_ids: Optional[List[str]] = None, space_keys: Optional[List[str]] = None, - starts_at: Optional[str] = None + starts_at: Optional[str] = None, ) -> AccessGrant: """Creates a new `Access Grant `_. Access Grants are the default and recommended way to grant a user access to any physical space, irrespective of the locking hardware. They work with both standalone smart locks (using ``device_ids``) and access control systems (using ``acs_entrance_ids`` or ``space_ids``), and can issue PIN codes, key cards, and mobile keys through a single request. @@ -67,14 +69,18 @@ def create( :param starts_at: Date and time at which the validity of the new grant starts, in `ISO 8601 `_ format. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod def delete(self, *, access_grant_id: str) -> None: """Delete an Access Grant. - :param access_grant_id: ID of Access Grant to delete.""" + :param access_grant_id: ID of Access Grant to delete. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -82,7 +88,7 @@ def get( self, *, access_grant_id: Optional[str] = None, - access_grant_key: Optional[str] = None + access_grant_key: Optional[str] = None, ) -> AccessGrant: """Get an Access Grant. @@ -90,7 +96,9 @@ def get( :param access_grant_key: Unique key of Access Grant to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -100,7 +108,7 @@ def get_related( access_grant_ids: Optional[List[str]] = None, access_grant_keys: Optional[List[str]] = None, exclude: Optional[List[str]] = None, - include: Optional[List[str]] = None + include: Optional[List[str]] = None, ) -> Batch: """Gets all related resources for one or more Access Grants. @@ -112,7 +120,9 @@ def get_related( :param include: - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -121,17 +131,17 @@ def list( *, access_code_id: Optional[str] = None, access_grant_ids: Optional[List[str]] = None, - access_grant_key: Optional[str] = None, + access_grant_key: Optional[Union[str, Null]] = None, acs_entrance_id: Optional[str] = None, acs_system_id: Optional[str] = None, customer_key: Optional[str] = None, device_id: Optional[str] = None, limit: Optional[float] = None, location_id: Optional[str] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, reservation_key: Optional[str] = None, space_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> List[AccessGrant]: """Gets an Access Grant. @@ -174,7 +184,9 @@ def request_access_methods( :param requested_access_methods: Array of requested access methods to add to the access grant. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -183,9 +195,9 @@ def update( *, access_grant_id: Optional[str] = None, access_grant_key: Optional[str] = None, - ends_at: Optional[str] = None, - name: Optional[str] = None, - starts_at: Optional[str] = None + ends_at: Optional[Union[str, Null]] = None, + name: Optional[Union[str, Null]] = None, + starts_at: Optional[str] = None, ) -> None: """Updates an existing Access Grant's time window. @@ -198,7 +210,8 @@ def update( :param name: Display name for the access grant. :param starts_at: Date and time at which the validity of the grant starts, in `ISO 8601 `_ format. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -212,6 +225,9 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): def unmanaged(self) -> AccessGrantsUnmanaged: return self._unmanaged + @route_metadata( + path="/access_grants/create", has_required_parameters=True, has_pagination=False + ) def create( self, *, @@ -222,14 +238,14 @@ def create( acs_entrance_ids: Optional[List[str]] = None, customization_profile_id: Optional[str] = None, device_ids: Optional[List[str]] = None, - ends_at: Optional[str] = None, + ends_at: Optional[Union[str, Null]] = None, location: Optional[Dict[str, Any]] = None, location_ids: Optional[List[str]] = None, - name: Optional[str] = None, + name: Optional[Union[str, Null]] = None, reservation_key: Optional[str] = None, space_ids: Optional[List[str]] = None, space_keys: Optional[List[str]] = None, - starts_at: Optional[str] = None + starts_at: Optional[str] = None, ) -> AccessGrant: """Creates a new `Access Grant `_. Access Grants are the default and recommended way to grant a user access to any physical space, irrespective of the locking hardware. They work with both standalone smart locks (using ``device_ids``) and access control systems (using ``acs_entrance_ids`` or ``space_ids``), and can issue PIN codes, key cards, and mobile keys through a single request. @@ -263,8 +279,10 @@ def create( :param starts_at: Date and time at which the validity of the new grant starts, in `ISO 8601 `_ format. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if requested_access_methods is not None: json_payload["requested_access_methods"] = requested_access_methods @@ -297,28 +315,46 @@ def create( if starts_at is not None: json_payload["starts_at"] = starts_at + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_grants/create" + ) + res = self.client.post("/access_grants/create", json=json_payload) return AccessGrant.from_dict(res["access_grant"]) + @route_metadata( + path="/access_grants/delete", has_required_parameters=True, has_pagination=False + ) def delete(self, *, access_grant_id: str) -> None: """Delete an Access Grant. - :param access_grant_id: ID of Access Grant to delete.""" - json_payload = {} + :param access_grant_id: ID of Access Grant to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if access_grant_id is not None: - json_payload["access_grant_id"] = access_grant_id + params["access_grant_id"] = access_grant_id - self.client.post("/access_grants/delete", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /access_grants/delete" + ) + + self.client.delete("/access_grants/delete", params=params) return None + @route_metadata( + path="/access_grants/get", has_required_parameters=True, has_pagination=False + ) def get( self, *, access_grant_id: Optional[str] = None, - access_grant_key: Optional[str] = None + access_grant_key: Optional[str] = None, ) -> AccessGrant: """Get an Access Grant. @@ -326,25 +362,37 @@ def get( :param access_grant_key: Unique key of Access Grant to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if access_grant_id is not None: - json_payload["access_grant_id"] = access_grant_id + params["access_grant_id"] = access_grant_id if access_grant_key is not None: - json_payload["access_grant_key"] = access_grant_key + params["access_grant_key"] = access_grant_key + + if not params: + raise ValueError( + "At least one parameter is required for /access_grants/get" + ) - res = self.client.post("/access_grants/get", json=json_payload) + res = self.client.get("/access_grants/get", params=params) return AccessGrant.from_dict(res["access_grant"]) + @route_metadata( + path="/access_grants/get_related", + has_required_parameters=True, + has_pagination=False, + ) def get_related( self, *, access_grant_ids: Optional[List[str]] = None, access_grant_keys: Optional[List[str]] = None, exclude: Optional[List[str]] = None, - include: Optional[List[str]] = None + include: Optional[List[str]] = None, ) -> Batch: """Gets all related resources for one or more Access Grants. @@ -356,8 +404,10 @@ def get_related( :param include: - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if access_grant_ids is not None: json_payload["access_grant_ids"] = access_grant_ids @@ -368,26 +418,34 @@ def get_related( if include is not None: json_payload["include"] = include + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_grants/get_related" + ) + res = self.client.post("/access_grants/get_related", json=json_payload) return Batch.from_dict(res["batch"]) + @route_metadata( + path="/access_grants/list", has_required_parameters=False, has_pagination=True + ) def list( self, *, access_code_id: Optional[str] = None, access_grant_ids: Optional[List[str]] = None, - access_grant_key: Optional[str] = None, + access_grant_key: Optional[Union[str, Null]] = None, acs_entrance_id: Optional[str] = None, acs_system_id: Optional[str] = None, customer_key: Optional[str] = None, device_id: Optional[str] = None, limit: Optional[float] = None, location_id: Optional[str] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, reservation_key: Optional[str] = None, space_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> List[AccessGrant]: """Gets an Access Grant. @@ -418,7 +476,7 @@ def list( :param user_identity_id: ID of user identity by which you want to filter the list of Access Grants. :returns: OK""" - json_payload = {} + json_payload: Dict[str, Any] = {} if access_code_id is not None: json_payload["access_code_id"] = access_code_id @@ -451,6 +509,11 @@ def list( return [AccessGrant.from_dict(item) for item in res["access_grants"]] + @route_metadata( + path="/access_grants/request_access_methods", + has_required_parameters=True, + has_pagination=False, + ) def request_access_methods( self, *, access_grant_id: str, requested_access_methods: List[Dict[str, Any]] ) -> AccessGrant: @@ -460,28 +523,38 @@ def request_access_methods( :param requested_access_methods: Array of requested access methods to add to the access grant. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if access_grant_id is not None: json_payload["access_grant_id"] = access_grant_id if requested_access_methods is not None: json_payload["requested_access_methods"] = requested_access_methods + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_grants/request_access_methods" + ) + res = self.client.post( "/access_grants/request_access_methods", json=json_payload ) return AccessGrant.from_dict(res["access_grant"]) + @route_metadata( + path="/access_grants/update", has_required_parameters=True, has_pagination=False + ) def update( self, *, access_grant_id: Optional[str] = None, access_grant_key: Optional[str] = None, - ends_at: Optional[str] = None, - name: Optional[str] = None, - starts_at: Optional[str] = None + ends_at: Optional[Union[str, Null]] = None, + name: Optional[Union[str, Null]] = None, + starts_at: Optional[str] = None, ) -> None: """Updates an existing Access Grant's time window. @@ -494,8 +567,9 @@ def update( :param name: Display name for the access grant. :param starts_at: Date and time at which the validity of the grant starts, in `ISO 8601 `_ format. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if access_grant_id is not None: json_payload["access_grant_id"] = access_grant_id @@ -508,6 +582,11 @@ def update( if starts_at is not None: json_payload["starts_at"] = starts_at - self.client.post("/access_grants/update", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_grants/update" + ) + + self.client.patch("/access_grants/update", json=json_payload) return None diff --git a/seam/routes/access_grants_unmanaged.py b/seam/routes/access_grants_unmanaged.py index 43786955..843709e8 100644 --- a/seam/routes/access_grants_unmanaged.py +++ b/seam/routes/access_grants_unmanaged.py @@ -1,6 +1,8 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +from ..null import Null from ..resources import UnmanagedAccessGrant @@ -12,7 +14,9 @@ def get(self, *, access_grant_id: str) -> UnmanagedAccessGrant: :param access_grant_id: ID of unmanaged Access Grant to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -22,9 +26,9 @@ def list( acs_entrance_id: Optional[str] = None, acs_system_id: Optional[str] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, reservation_key: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> List[UnmanagedAccessGrant]: """Gets unmanaged Access Grants (where is_managed = false). @@ -49,7 +53,7 @@ def update( *, access_grant_id: str, is_managed: bool, - access_grant_key: Optional[str] = None + access_grant_key: Optional[str] = None, ) -> None: """Updates an unmanaged Access Grant to make it managed. @@ -62,7 +66,8 @@ def update( :param is_managed: Must be set to true to convert the unmanaged access grant to managed. :param access_grant_key: Unique key for the access grant. If not provided, the existing key will be preserved. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -71,30 +76,47 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/access_grants/unmanaged/get", + has_required_parameters=True, + has_pagination=False, + ) def get(self, *, access_grant_id: str) -> UnmanagedAccessGrant: """Get an unmanaged Access Grant (where is_managed = false). :param access_grant_id: ID of unmanaged Access Grant to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if access_grant_id is not None: - json_payload["access_grant_id"] = access_grant_id + params["access_grant_id"] = access_grant_id + + if not params: + raise ValueError( + "At least one parameter is required for /access_grants/unmanaged/get" + ) - res = self.client.post("/access_grants/unmanaged/get", json=json_payload) + res = self.client.get("/access_grants/unmanaged/get", params=params) return UnmanagedAccessGrant.from_dict(res["access_grant"]) + @route_metadata( + path="/access_grants/unmanaged/list", + has_required_parameters=False, + has_pagination=True, + ) def list( self, *, acs_entrance_id: Optional[str] = None, acs_system_id: Optional[str] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, reservation_key: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> List[UnmanagedAccessGrant]: """Gets unmanaged Access Grants (where is_managed = false). @@ -111,31 +133,36 @@ def list( :param user_identity_id: ID of user identity by which you want to filter the list of unmanaged Access Grants. :returns: OK""" - json_payload = {} + params: Dict[str, Any] = {} if acs_entrance_id is not None: - json_payload["acs_entrance_id"] = acs_entrance_id + params["acs_entrance_id"] = acs_entrance_id if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if reservation_key is not None: - json_payload["reservation_key"] = reservation_key + params["reservation_key"] = reservation_key if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id - res = self.client.post("/access_grants/unmanaged/list", json=json_payload) + res = self.client.get("/access_grants/unmanaged/list", params=params) return [UnmanagedAccessGrant.from_dict(item) for item in res["access_grants"]] + @route_metadata( + path="/access_grants/unmanaged/update", + has_required_parameters=True, + has_pagination=False, + ) def update( self, *, access_grant_id: str, is_managed: bool, - access_grant_key: Optional[str] = None + access_grant_key: Optional[str] = None, ) -> None: """Updates an unmanaged Access Grant to make it managed. @@ -148,8 +175,9 @@ def update( :param is_managed: Must be set to true to convert the unmanaged access grant to managed. :param access_grant_key: Unique key for the access grant. If not provided, the existing key will be preserved. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if access_grant_id is not None: json_payload["access_grant_id"] = access_grant_id @@ -158,6 +186,11 @@ def update( if access_grant_key is not None: json_payload["access_grant_key"] = access_grant_key - self.client.post("/access_grants/unmanaged/update", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_grants/unmanaged/update" + ) + + self.client.patch("/access_grants/unmanaged/update", json=json_payload) return None diff --git a/seam/routes/access_methods.py b/seam/routes/access_methods.py index ffafd2d5..915c4773 100644 --- a/seam/routes/access_methods.py +++ b/seam/routes/access_methods.py @@ -1,6 +1,8 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +from ..null import Null from ..resources import ActionAttempt, AccessMethod, Batch from .access_methods_unmanaged import ( AbstractAccessMethodsUnmanaged, @@ -22,7 +24,7 @@ def assign_card( *, access_method_id: str, card_number: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Assigns a pre-registered card credential, identified by ``card_number``, to a card-mode access method. Use this endpoint for access systems that use pre-registered cards, where a physical card must be associated with an access method before it can be used for access. Assigning a card credential also triggers issuance of the access method. @@ -32,7 +34,9 @@ def assign_card( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -41,7 +45,7 @@ def delete( *, access_method_id: Optional[str] = None, access_grant_id: Optional[str] = None, - reservation_key: Optional[str] = None + reservation_key: Optional[str] = None, ) -> None: """Deletes an access method. @@ -50,7 +54,8 @@ def delete( :param access_grant_id: ID of access grant whose access methods should be deleted. :param reservation_key: Reservation key of the access grant whose access methods should be deleted. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -59,7 +64,7 @@ def encode( *, access_method_id: str, acs_encoder_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Encodes an existing access method onto a plastic card placed on the specified `encoder `_. @@ -69,7 +74,9 @@ def encode( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -78,7 +85,9 @@ def get(self, *, access_method_id: str) -> AccessMethod: :param access_method_id: ID of access method to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -87,7 +96,7 @@ def get_related( *, access_method_ids: List[str], exclude: Optional[List[str]] = None, - include: Optional[List[str]] = None + include: Optional[List[str]] = None, ) -> Batch: """Gets all related resources for one or more Access Methods. @@ -97,7 +106,9 @@ def get_related( :param include: - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -110,8 +121,8 @@ def list( acs_entrance_id: Optional[str] = None, device_id: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = None, - space_id: Optional[str] = None + page_cursor: Optional[Union[str, Null]] = None, + space_id: Optional[str] = None, ) -> List[AccessMethod]: """Lists all access methods, usually filtered by Access Grant. @@ -131,7 +142,9 @@ def list( :param space_id: ID of the space by which to filter the returned access methods. Must be combined with ``access_grant_id``, ``access_grant_key``, or ``acs_entrance_id``. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -140,7 +153,7 @@ def unlock_door( *, access_method_id: str, acs_entrance_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Remotely unlocks a specified `entrance `_ using the cloud key credential associated with an access method. Returns an action attempt that tracks the progress of the unlock operation. @@ -150,7 +163,9 @@ def unlock_door( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -164,12 +179,17 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): def unmanaged(self) -> AccessMethodsUnmanaged: return self._unmanaged + @route_metadata( + path="/access_methods/assign_card", + has_required_parameters=True, + has_pagination=False, + ) def assign_card( self, *, access_method_id: str, card_number: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Assigns a pre-registered card credential, identified by ``card_number``, to a card-mode access method. Use this endpoint for access systems that use pre-registered cards, where a physical card must be associated with an access method before it can be used for access. Assigning a card credential also triggers issuance of the access method. @@ -179,14 +199,21 @@ def assign_card( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if access_method_id is not None: json_payload["access_method_id"] = access_method_id if card_number is not None: json_payload["card_number"] = card_number + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_methods/assign_card" + ) + res = self.client.post("/access_methods/assign_card", json=json_payload) wait_for_action_attempt = ( @@ -201,12 +228,17 @@ def assign_card( wait_for_action_attempt=wait_for_action_attempt, ) + @route_metadata( + path="/access_methods/delete", + has_required_parameters=True, + has_pagination=False, + ) def delete( self, *, access_method_id: Optional[str] = None, access_grant_id: Optional[str] = None, - reservation_key: Optional[str] = None + reservation_key: Optional[str] = None, ) -> None: """Deletes an access method. @@ -215,26 +247,37 @@ def delete( :param access_grant_id: ID of access grant whose access methods should be deleted. :param reservation_key: Reservation key of the access grant whose access methods should be deleted. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if access_method_id is not None: - json_payload["access_method_id"] = access_method_id + params["access_method_id"] = access_method_id if access_grant_id is not None: - json_payload["access_grant_id"] = access_grant_id + params["access_grant_id"] = access_grant_id if reservation_key is not None: - json_payload["reservation_key"] = reservation_key + params["reservation_key"] = reservation_key + + if not params: + raise ValueError( + "At least one parameter is required for /access_methods/delete" + ) - self.client.post("/access_methods/delete", json=json_payload) + self.client.delete("/access_methods/delete", params=params) return None + @route_metadata( + path="/access_methods/encode", + has_required_parameters=True, + has_pagination=False, + ) def encode( self, *, access_method_id: str, acs_encoder_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Encodes an existing access method onto a plastic card placed on the specified `encoder `_. @@ -244,14 +287,21 @@ def encode( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if access_method_id is not None: json_payload["access_method_id"] = access_method_id if acs_encoder_id is not None: json_payload["acs_encoder_id"] = acs_encoder_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_methods/encode" + ) + res = self.client.post("/access_methods/encode", json=json_payload) wait_for_action_attempt = ( @@ -266,27 +316,42 @@ def encode( wait_for_action_attempt=wait_for_action_attempt, ) + @route_metadata( + path="/access_methods/get", has_required_parameters=True, has_pagination=False + ) def get(self, *, access_method_id: str) -> AccessMethod: """Gets an access method. :param access_method_id: ID of access method to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if access_method_id is not None: - json_payload["access_method_id"] = access_method_id + params["access_method_id"] = access_method_id + + if not params: + raise ValueError( + "At least one parameter is required for /access_methods/get" + ) - res = self.client.post("/access_methods/get", json=json_payload) + res = self.client.get("/access_methods/get", params=params) return AccessMethod.from_dict(res["access_method"]) + @route_metadata( + path="/access_methods/get_related", + has_required_parameters=True, + has_pagination=False, + ) def get_related( self, *, access_method_ids: List[str], exclude: Optional[List[str]] = None, - include: Optional[List[str]] = None + include: Optional[List[str]] = None, ) -> Batch: """Gets all related resources for one or more Access Methods. @@ -296,8 +361,10 @@ def get_related( :param include: - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if access_method_ids is not None: json_payload["access_method_ids"] = access_method_ids @@ -306,10 +373,18 @@ def get_related( if include is not None: json_payload["include"] = include + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_methods/get_related" + ) + res = self.client.post("/access_methods/get_related", json=json_payload) return Batch.from_dict(res["batch"]) + @route_metadata( + path="/access_methods/list", has_required_parameters=True, has_pagination=True + ) def list( self, *, @@ -319,8 +394,8 @@ def list( acs_entrance_id: Optional[str] = None, device_id: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = None, - space_id: Optional[str] = None + page_cursor: Optional[Union[str, Null]] = None, + space_id: Optional[str] = None, ) -> List[AccessMethod]: """Lists all access methods, usually filtered by Access Grant. @@ -340,36 +415,48 @@ def list( :param space_id: ID of the space by which to filter the returned access methods. Must be combined with ``access_grant_id``, ``access_grant_key``, or ``acs_entrance_id``. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if access_code_id is not None: - json_payload["access_code_id"] = access_code_id + params["access_code_id"] = access_code_id if access_grant_id is not None: - json_payload["access_grant_id"] = access_grant_id + params["access_grant_id"] = access_grant_id if access_grant_key is not None: - json_payload["access_grant_key"] = access_grant_key + params["access_grant_key"] = access_grant_key if acs_entrance_id is not None: - json_payload["acs_entrance_id"] = acs_entrance_id + params["acs_entrance_id"] = acs_entrance_id if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if space_id is not None: - json_payload["space_id"] = space_id + params["space_id"] = space_id - res = self.client.post("/access_methods/list", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /access_methods/list" + ) + + res = self.client.get("/access_methods/list", params=params) return [AccessMethod.from_dict(item) for item in res["access_methods"]] + @route_metadata( + path="/access_methods/unlock_door", + has_required_parameters=True, + has_pagination=False, + ) def unlock_door( self, *, access_method_id: str, acs_entrance_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Remotely unlocks a specified `entrance `_ using the cloud key credential associated with an access method. Returns an action attempt that tracks the progress of the unlock operation. @@ -379,14 +466,21 @@ def unlock_door( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if access_method_id is not None: json_payload["access_method_id"] = access_method_id if acs_entrance_id is not None: json_payload["acs_entrance_id"] = acs_entrance_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /access_methods/unlock_door" + ) + res = self.client.post("/access_methods/unlock_door", json=json_payload) wait_for_action_attempt = ( diff --git a/seam/routes/access_methods_unmanaged.py b/seam/routes/access_methods_unmanaged.py index 44ad9c5f..fd7cd14a 100644 --- a/seam/routes/access_methods_unmanaged.py +++ b/seam/routes/access_methods_unmanaged.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata from ..resources import UnmanagedAccessMethod @@ -12,7 +13,9 @@ def get(self, *, access_method_id: str) -> UnmanagedAccessMethod: :param access_method_id: ID of unmanaged access method to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -22,7 +25,7 @@ def list( access_grant_id: str, acs_entrance_id: Optional[str] = None, device_id: Optional[str] = None, - space_id: Optional[str] = None + space_id: Optional[str] = None, ) -> List[UnmanagedAccessMethod]: """Lists all unmanaged access methods (where is_managed = false), usually filtered by Access Grant. @@ -34,7 +37,9 @@ def list( :param space_id: ID of the space for which you want to retrieve all unmanaged access methods. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -43,28 +48,45 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/access_methods/unmanaged/get", + has_required_parameters=True, + has_pagination=False, + ) def get(self, *, access_method_id: str) -> UnmanagedAccessMethod: """Gets an unmanaged access method (where is_managed = false). :param access_method_id: ID of unmanaged access method to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if access_method_id is not None: - json_payload["access_method_id"] = access_method_id + params["access_method_id"] = access_method_id - res = self.client.post("/access_methods/unmanaged/get", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /access_methods/unmanaged/get" + ) + + res = self.client.get("/access_methods/unmanaged/get", params=params) return UnmanagedAccessMethod.from_dict(res["access_method"]) + @route_metadata( + path="/access_methods/unmanaged/list", + has_required_parameters=True, + has_pagination=False, + ) def list( self, *, access_grant_id: str, acs_entrance_id: Optional[str] = None, device_id: Optional[str] = None, - space_id: Optional[str] = None + space_id: Optional[str] = None, ) -> List[UnmanagedAccessMethod]: """Lists all unmanaged access methods (where is_managed = false), usually filtered by Access Grant. @@ -76,18 +98,25 @@ def list( :param space_id: ID of the space for which you want to retrieve all unmanaged access methods. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if access_grant_id is not None: - json_payload["access_grant_id"] = access_grant_id + params["access_grant_id"] = access_grant_id if acs_entrance_id is not None: - json_payload["acs_entrance_id"] = acs_entrance_id + params["acs_entrance_id"] = acs_entrance_id if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if space_id is not None: - json_payload["space_id"] = space_id + params["space_id"] = space_id + + if not params: + raise ValueError( + "At least one parameter is required for /access_methods/unmanaged/list" + ) - res = self.client.post("/access_methods/unmanaged/list", json=json_payload) + res = self.client.get("/access_methods/unmanaged/list", params=params) return [UnmanagedAccessMethod.from_dict(item) for item in res["access_methods"]] diff --git a/seam/routes/acs.py b/seam/routes/acs.py index 8207cf12..125f3c31 100644 --- a/seam/routes/acs.py +++ b/seam/routes/acs.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata from .acs_access_groups import AbstractAcsAccessGroups, AcsAccessGroups from .acs_credentials import AbstractAcsCredentials, AcsCredentials from .acs_encoders import AbstractAcsEncoders, AcsEncoders diff --git a/seam/routes/acs_access_groups.py b/seam/routes/acs_access_groups.py index 11ce5959..6b84487e 100644 --- a/seam/routes/acs_access_groups.py +++ b/seam/routes/acs_access_groups.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata from ..resources import AcsAccessGroup, AcsEntrance, AcsUser @@ -12,7 +13,7 @@ def add_user( *, acs_access_group_id: str, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """Adds a specified `access system user `_ to a specified `access group `_. @@ -21,14 +22,17 @@ def add_user( :param acs_user_id: ID of the access system user that you want to add to an access group. You can only provide one of acs_user_id or user_identity_id. :param user_identity_id: ID of the desired user identity that you want to add to an access group. You can only provide one of acs_user_id or user_identity_id. If the ACS system contains an ACS user with the same ``email_address`` or ``phone_number`` as the user identity that you specify, they are linked, and the access group membership belongs to the ACS user. If the ACS system does not have a corresponding ACS user, one is created. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod def delete(self, *, acs_access_group_id: str) -> None: """Deletes a specified `access group `_. - :param acs_access_group_id: ID of the access group that you want to delete.""" + :param acs_access_group_id: ID of the access group that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -37,7 +41,9 @@ def get(self, *, acs_access_group_id: str) -> AcsAccessGroup: :param acs_access_group_id: ID of the access group that you want to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -47,7 +53,7 @@ def list( acs_system_id: Optional[str] = None, acs_user_id: Optional[str] = None, search: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> List[AcsAccessGroup]: """Returns a list of all `access groups `_. @@ -70,7 +76,9 @@ def list_accessible_entrances( :param acs_access_group_id: ID of the access group for which you want to retrieve all accessible entrances. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -79,7 +87,9 @@ def list_users(self, *, acs_access_group_id: str) -> List[AcsUser]: :param acs_access_group_id: ID of the access group for which you want to retrieve all access system users. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -88,7 +98,7 @@ def remove_user( *, acs_access_group_id: str, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """Removes a specified `access system user `_ from a specified `access group `_. @@ -97,7 +107,8 @@ def remove_user( :param acs_user_id: ID of the access system user that you want to remove from an access group. :param user_identity_id: ID of the user identity associated with the user that you want to remove from an access group. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -106,12 +117,17 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/acs/access_groups/add_user", + has_required_parameters=True, + has_pagination=False, + ) def add_user( self, *, acs_access_group_id: str, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """Adds a specified `access system user `_ to a specified `access group `_. @@ -120,8 +136,9 @@ def add_user( :param acs_user_id: ID of the access system user that you want to add to an access group. You can only provide one of acs_user_id or user_identity_id. :param user_identity_id: ID of the desired user identity that you want to add to an access group. You can only provide one of acs_user_id or user_identity_id. If the ACS system contains an ACS user with the same ``email_address`` or ``phone_number`` as the user identity that you specify, they are linked, and the access group membership belongs to the ACS user. If the ACS system does not have a corresponding ACS user, one is created. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_access_group_id is not None: json_payload["acs_access_group_id"] = acs_access_group_id @@ -130,45 +147,79 @@ def add_user( if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id - self.client.post("/acs/access_groups/add_user", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/access_groups/add_user" + ) + + self.client.put("/acs/access_groups/add_user", json=json_payload) return None + @route_metadata( + path="/acs/access_groups/delete", + has_required_parameters=True, + has_pagination=False, + ) def delete(self, *, acs_access_group_id: str) -> None: """Deletes a specified `access group `_. - :param acs_access_group_id: ID of the access group that you want to delete.""" - json_payload = {} + :param acs_access_group_id: ID of the access group that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if acs_access_group_id is not None: - json_payload["acs_access_group_id"] = acs_access_group_id + params["acs_access_group_id"] = acs_access_group_id - self.client.post("/acs/access_groups/delete", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /acs/access_groups/delete" + ) + + self.client.delete("/acs/access_groups/delete", params=params) return None + @route_metadata( + path="/acs/access_groups/get", + has_required_parameters=True, + has_pagination=False, + ) def get(self, *, acs_access_group_id: str) -> AcsAccessGroup: """Returns a specified `access group `_. :param acs_access_group_id: ID of the access group that you want to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if acs_access_group_id is not None: - json_payload["acs_access_group_id"] = acs_access_group_id + params["acs_access_group_id"] = acs_access_group_id - res = self.client.post("/acs/access_groups/get", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /acs/access_groups/get" + ) + + res = self.client.get("/acs/access_groups/get", params=params) return AcsAccessGroup.from_dict(res["acs_access_group"]) + @route_metadata( + path="/acs/access_groups/list", + has_required_parameters=False, + has_pagination=False, + ) def list( self, *, acs_system_id: Optional[str] = None, acs_user_id: Optional[str] = None, search: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> List[AcsAccessGroup]: """Returns a list of all `access groups `_. @@ -181,21 +232,26 @@ def list( :param user_identity_id: ID of the user identity for which you want to retrieve all access groups. :returns: OK""" - json_payload = {} + params: Dict[str, Any] = {} if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id if acs_user_id is not None: - json_payload["acs_user_id"] = acs_user_id + params["acs_user_id"] = acs_user_id if search is not None: - json_payload["search"] = search + params["search"] = search if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id - res = self.client.post("/acs/access_groups/list", json=json_payload) + res = self.client.get("/acs/access_groups/list", params=params) return [AcsAccessGroup.from_dict(item) for item in res["acs_access_groups"]] + @route_metadata( + path="/acs/access_groups/list_accessible_entrances", + has_required_parameters=True, + has_pagination=False, + ) def list_accessible_entrances( self, *, acs_access_group_id: str ) -> List[AcsEntrance]: @@ -203,39 +259,63 @@ def list_accessible_entrances( :param acs_access_group_id: ID of the access group for which you want to retrieve all accessible entrances. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if acs_access_group_id is not None: - json_payload["acs_access_group_id"] = acs_access_group_id + params["acs_access_group_id"] = acs_access_group_id - res = self.client.post( - "/acs/access_groups/list_accessible_entrances", json=json_payload + if not params: + raise ValueError( + "At least one parameter is required for /acs/access_groups/list_accessible_entrances" + ) + + res = self.client.get( + "/acs/access_groups/list_accessible_entrances", params=params ) return [AcsEntrance.from_dict(item) for item in res["acs_entrances"]] + @route_metadata( + path="/acs/access_groups/list_users", + has_required_parameters=True, + has_pagination=False, + ) def list_users(self, *, acs_access_group_id: str) -> List[AcsUser]: """Returns a list of all `access system users `_ in an `access group `_. :param acs_access_group_id: ID of the access group for which you want to retrieve all access system users. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if acs_access_group_id is not None: - json_payload["acs_access_group_id"] = acs_access_group_id + params["acs_access_group_id"] = acs_access_group_id - res = self.client.post("/acs/access_groups/list_users", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /acs/access_groups/list_users" + ) + + res = self.client.get("/acs/access_groups/list_users", params=params) return [AcsUser.from_dict(item) for item in res["acs_users"]] + @route_metadata( + path="/acs/access_groups/remove_user", + has_required_parameters=True, + has_pagination=False, + ) def remove_user( self, *, acs_access_group_id: str, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """Removes a specified `access system user `_ from a specified `access group `_. @@ -244,16 +324,22 @@ def remove_user( :param acs_user_id: ID of the access system user that you want to remove from an access group. :param user_identity_id: ID of the user identity associated with the user that you want to remove from an access group. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if acs_access_group_id is not None: - json_payload["acs_access_group_id"] = acs_access_group_id + params["acs_access_group_id"] = acs_access_group_id if acs_user_id is not None: - json_payload["acs_user_id"] = acs_user_id + params["acs_user_id"] = acs_user_id if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError( + "At least one parameter is required for /acs/access_groups/remove_user" + ) - self.client.post("/acs/access_groups/remove_user", json=json_payload) + self.client.delete("/acs/access_groups/remove_user", params=params) return None diff --git a/seam/routes/acs_credentials.py b/seam/routes/acs_credentials.py index c655973e..ceff9409 100644 --- a/seam/routes/acs_credentials.py +++ b/seam/routes/acs_credentials.py @@ -1,6 +1,8 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +from ..null import Null from ..resources import AcsCredential, AcsEntrance @@ -12,7 +14,7 @@ def assign( *, acs_credential_id: str, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """Assigns a specified `credential `_ to a specified `access system user `_. @@ -21,7 +23,8 @@ def assign( :param acs_user_id: ID of the access system user to whom you want to assign a credential. You can only provide one of acs_user_id or user_identity_id. :param user_identity_id: ID of the user identity to whom you want to assign a credential. You can only provide one of acs_user_id or user_identity_id. If the ACS system contains an ACS user with the same ``email_address`` or ``phone_number`` as the user identity that you specify, they are linked, and the credential belongs to the ACS user. If the ACS system does not have a corresponding ACS user, one is created. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -40,7 +43,7 @@ def create( salto_space_metadata: Optional[Dict[str, Any]] = None, starts_at: Optional[str] = None, user_identity_id: Optional[str] = None, - visionline_metadata: Optional[Dict[str, Any]] = None + visionline_metadata: Optional[Dict[str, Any]] = None, ) -> AcsCredential: """Creates a new `credential `_ for a specified `ACS user `_. For granting access, we recommend `Access Grants `_ instead: they create and manage the underlying credentials for you, across access systems and standalone smart locks alike. Use this low-level endpoint only when you need direct control over an individual ACS credential. @@ -70,14 +73,18 @@ def create( :param visionline_metadata: Visionline-specific metadata for the new credential. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod def delete(self, *, acs_credential_id: str) -> None: """Deletes a specified `credential `_. - :param acs_credential_id: ID of the credential that you want to delete.""" + :param acs_credential_id: ID of the credential that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -86,7 +93,9 @@ def get(self, *, acs_credential_id: str) -> AcsCredential: :param acs_credential_id: ID of the credential that you want to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -99,8 +108,8 @@ def list( created_before: Optional[str] = None, is_multi_phone_sync_credential: Optional[bool] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, - search: Optional[str] = None + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, ) -> List[AcsCredential]: """Returns a list of all `credentials `_. @@ -129,7 +138,9 @@ def list_accessible_entrances(self, *, acs_credential_id: str) -> List[AcsEntran :param acs_credential_id: ID of the credential for which you want to retrieve all entrances to which the credential grants access. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -138,7 +149,7 @@ def unassign( *, acs_credential_id: str, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """Unassigns a specified `credential `_ from a specified `access system user `_. @@ -147,7 +158,8 @@ def unassign( :param acs_user_id: ID of the access system user from which you want to unassign a credential. You can only provide one of acs_user_id or user_identity_id. :param user_identity_id: ID of the user identity from which you want to unassign a credential. You can only provide one of acs_user_id or user_identity_id. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -156,7 +168,7 @@ def update( *, acs_credential_id: str, code: Optional[str] = None, - ends_at: Optional[str] = None + ends_at: Optional[str] = None, ) -> None: """Updates the code and ends at date and time for a specified `credential `_. @@ -165,7 +177,8 @@ def update( :param code: Replacement access (PIN) code for the credential that you want to update. :param ends_at: Replacement date and time at which the validity of the credential ends, in `ISO 8601 `_ format. Must be a time in the future and after the ``starts_at`` value that you set when creating the credential. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -174,12 +187,17 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/acs/credentials/assign", + has_required_parameters=True, + has_pagination=False, + ) def assign( self, *, acs_credential_id: str, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """Assigns a specified `credential `_ to a specified `access system user `_. @@ -188,8 +206,9 @@ def assign( :param acs_user_id: ID of the access system user to whom you want to assign a credential. You can only provide one of acs_user_id or user_identity_id. :param user_identity_id: ID of the user identity to whom you want to assign a credential. You can only provide one of acs_user_id or user_identity_id. If the ACS system contains an ACS user with the same ``email_address`` or ``phone_number`` as the user identity that you specify, they are linked, and the credential belongs to the ACS user. If the ACS system does not have a corresponding ACS user, one is created. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_credential_id is not None: json_payload["acs_credential_id"] = acs_credential_id @@ -198,10 +217,20 @@ def assign( if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id - self.client.post("/acs/credentials/assign", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/credentials/assign" + ) + + self.client.patch("/acs/credentials/assign", json=json_payload) return None + @route_metadata( + path="/acs/credentials/create", + has_required_parameters=True, + has_pagination=False, + ) def create( self, *, @@ -217,7 +246,7 @@ def create( salto_space_metadata: Optional[Dict[str, Any]] = None, starts_at: Optional[str] = None, user_identity_id: Optional[str] = None, - visionline_metadata: Optional[Dict[str, Any]] = None + visionline_metadata: Optional[Dict[str, Any]] = None, ) -> AcsCredential: """Creates a new `credential `_ for a specified `ACS user `_. For granting access, we recommend `Access Grants `_ instead: they create and manage the underlying credentials for you, across access systems and standalone smart locks alike. Use this low-level endpoint only when you need direct control over an individual ACS credential. @@ -247,8 +276,10 @@ def create( :param visionline_metadata: Visionline-specific metadata for the new credential. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if access_method is not None: json_payload["access_method"] = access_method @@ -281,38 +312,68 @@ def create( if visionline_metadata is not None: json_payload["visionline_metadata"] = visionline_metadata + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/credentials/create" + ) + res = self.client.post("/acs/credentials/create", json=json_payload) return AcsCredential.from_dict(res["acs_credential"]) + @route_metadata( + path="/acs/credentials/delete", + has_required_parameters=True, + has_pagination=False, + ) def delete(self, *, acs_credential_id: str) -> None: """Deletes a specified `credential `_. - :param acs_credential_id: ID of the credential that you want to delete.""" - json_payload = {} + :param acs_credential_id: ID of the credential that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if acs_credential_id is not None: - json_payload["acs_credential_id"] = acs_credential_id + params["acs_credential_id"] = acs_credential_id + + if not params: + raise ValueError( + "At least one parameter is required for /acs/credentials/delete" + ) - self.client.post("/acs/credentials/delete", json=json_payload) + self.client.delete("/acs/credentials/delete", params=params) return None + @route_metadata( + path="/acs/credentials/get", has_required_parameters=True, has_pagination=False + ) def get(self, *, acs_credential_id: str) -> AcsCredential: """Returns a specified `credential `_. :param acs_credential_id: ID of the credential that you want to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if acs_credential_id is not None: - json_payload["acs_credential_id"] = acs_credential_id + params["acs_credential_id"] = acs_credential_id - res = self.client.post("/acs/credentials/get", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /acs/credentials/get" + ) + + res = self.client.get("/acs/credentials/get", params=params) return AcsCredential.from_dict(res["acs_credential"]) + @route_metadata( + path="/acs/credentials/list", has_required_parameters=False, has_pagination=True + ) def list( self, *, @@ -322,8 +383,8 @@ def list( created_before: Optional[str] = None, is_multi_phone_sync_credential: Optional[bool] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, - search: Optional[str] = None + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, ) -> List[AcsCredential]: """Returns a list of all `credentials `_. @@ -344,54 +405,69 @@ def list( :param search: String for which to search. Filters returned credentials to include all records that satisfy a partial match using ``display_name``, ``code``, ``card_number``, ``acs_user_id`` or ``acs_credential_id``. :returns: OK""" - json_payload = {} + params: Dict[str, Any] = {} if acs_user_id is not None: - json_payload["acs_user_id"] = acs_user_id + params["acs_user_id"] = acs_user_id if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id if created_before is not None: - json_payload["created_before"] = created_before + params["created_before"] = created_before if is_multi_phone_sync_credential is not None: - json_payload["is_multi_phone_sync_credential"] = ( - is_multi_phone_sync_credential - ) + params["is_multi_phone_sync_credential"] = is_multi_phone_sync_credential if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if search is not None: - json_payload["search"] = search + params["search"] = search - res = self.client.post("/acs/credentials/list", json=json_payload) + res = self.client.get("/acs/credentials/list", params=params) return [AcsCredential.from_dict(item) for item in res["acs_credentials"]] + @route_metadata( + path="/acs/credentials/list_accessible_entrances", + has_required_parameters=True, + has_pagination=False, + ) def list_accessible_entrances(self, *, acs_credential_id: str) -> List[AcsEntrance]: """Returns a list of all `entrances `_ to which a `credential `_ grants access. :param acs_credential_id: ID of the credential for which you want to retrieve all entrances to which the credential grants access. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if acs_credential_id is not None: - json_payload["acs_credential_id"] = acs_credential_id + params["acs_credential_id"] = acs_credential_id + + if not params: + raise ValueError( + "At least one parameter is required for /acs/credentials/list_accessible_entrances" + ) - res = self.client.post( - "/acs/credentials/list_accessible_entrances", json=json_payload + res = self.client.get( + "/acs/credentials/list_accessible_entrances", params=params ) return [AcsEntrance.from_dict(item) for item in res["acs_entrances"]] + @route_metadata( + path="/acs/credentials/unassign", + has_required_parameters=True, + has_pagination=False, + ) def unassign( self, *, acs_credential_id: str, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """Unassigns a specified `credential `_ from a specified `access system user `_. @@ -400,8 +476,9 @@ def unassign( :param acs_user_id: ID of the access system user from which you want to unassign a credential. You can only provide one of acs_user_id or user_identity_id. :param user_identity_id: ID of the user identity from which you want to unassign a credential. You can only provide one of acs_user_id or user_identity_id. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_credential_id is not None: json_payload["acs_credential_id"] = acs_credential_id @@ -410,16 +487,26 @@ def unassign( if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id - self.client.post("/acs/credentials/unassign", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/credentials/unassign" + ) + + self.client.patch("/acs/credentials/unassign", json=json_payload) return None + @route_metadata( + path="/acs/credentials/update", + has_required_parameters=True, + has_pagination=False, + ) def update( self, *, acs_credential_id: str, code: Optional[str] = None, - ends_at: Optional[str] = None + ends_at: Optional[str] = None, ) -> None: """Updates the code and ends at date and time for a specified `credential `_. @@ -428,8 +515,9 @@ def update( :param code: Replacement access (PIN) code for the credential that you want to update. :param ends_at: Replacement date and time at which the validity of the credential ends, in `ISO 8601 `_ format. Must be a time in the future and after the ``starts_at`` value that you set when creating the credential. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_credential_id is not None: json_payload["acs_credential_id"] = acs_credential_id @@ -438,6 +526,11 @@ def update( if ends_at is not None: json_payload["ends_at"] = ends_at - self.client.post("/acs/credentials/update", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/credentials/update" + ) + + self.client.patch("/acs/credentials/update", json=json_payload) return None diff --git a/seam/routes/acs_encoders.py b/seam/routes/acs_encoders.py index e537a0f3..5b6d9c8a 100644 --- a/seam/routes/acs_encoders.py +++ b/seam/routes/acs_encoders.py @@ -1,6 +1,8 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +from ..null import Null from ..resources import ActionAttempt, AcsEncoder from .acs_encoders_simulate import AbstractAcsEncodersSimulate, AcsEncodersSimulate from ..modules.action_attempts import resolve_action_attempt @@ -20,7 +22,7 @@ def encode_credential( acs_encoder_id: str, access_method_id: Optional[str] = None, acs_credential_id: Optional[str] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Encodes an existing `credential `_ onto a plastic card placed on the specified `encoder `_. Either provide an ``acs_credential_id`` or an ``access_method_id`` @@ -32,7 +34,9 @@ def encode_credential( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -41,7 +45,9 @@ def get(self, *, acs_encoder_id: str) -> AcsEncoder: :param acs_encoder_id: ID of the encoder that you want to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -52,7 +58,7 @@ def list( acs_system_ids: Optional[List[str]] = None, acs_encoder_ids: Optional[List[str]] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None + page_cursor: Optional[Union[str, Null]] = None, ) -> List[AcsEncoder]: """Returns a list of all `encoders `_. @@ -75,7 +81,7 @@ def scan_credential( *, acs_encoder_id: str, salto_ks_metadata: Optional[Dict[str, Any]] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Scans an encoded `acs_credential `_ from a plastic card placed on the specified `encoder `_. @@ -85,7 +91,9 @@ def scan_credential( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -96,7 +104,7 @@ def scan_to_assign_credential( acs_user_id: Optional[str] = None, salto_ks_metadata: Optional[Dict[str, Any]] = None, user_identity_id: Optional[str] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Scans a physical card placed on the specified `encoder `_ and assigns the scanned credential to an ACS user. Provide either an ``acs_user_id`` or a ``user_identity_id``. @@ -110,7 +118,9 @@ def scan_to_assign_credential( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -124,13 +134,18 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): def simulate(self) -> AcsEncodersSimulate: return self._simulate + @route_metadata( + path="/acs/encoders/encode_credential", + has_required_parameters=True, + has_pagination=False, + ) def encode_credential( self, *, acs_encoder_id: str, access_method_id: Optional[str] = None, acs_credential_id: Optional[str] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Encodes an existing `credential `_ onto a plastic card placed on the specified `encoder `_. Either provide an ``acs_credential_id`` or an ``access_method_id`` @@ -142,8 +157,10 @@ def encode_credential( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_encoder_id is not None: json_payload["acs_encoder_id"] = acs_encoder_id @@ -152,6 +169,11 @@ def encode_credential( if acs_credential_id is not None: json_payload["acs_credential_id"] = acs_credential_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/encoders/encode_credential" + ) + res = self.client.post("/acs/encoders/encode_credential", json=json_payload) wait_for_action_attempt = ( @@ -166,21 +188,32 @@ def encode_credential( wait_for_action_attempt=wait_for_action_attempt, ) + @route_metadata( + path="/acs/encoders/get", has_required_parameters=True, has_pagination=False + ) def get(self, *, acs_encoder_id: str) -> AcsEncoder: """Returns a specified `encoder `_. :param acs_encoder_id: ID of the encoder that you want to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if acs_encoder_id is not None: - json_payload["acs_encoder_id"] = acs_encoder_id + params["acs_encoder_id"] = acs_encoder_id - res = self.client.post("/acs/encoders/get", json=json_payload) + if not params: + raise ValueError("At least one parameter is required for /acs/encoders/get") + + res = self.client.get("/acs/encoders/get", params=params) return AcsEncoder.from_dict(res["acs_encoder"]) + @route_metadata( + path="/acs/encoders/list", has_required_parameters=False, has_pagination=True + ) def list( self, *, @@ -188,7 +221,7 @@ def list( acs_system_ids: Optional[List[str]] = None, acs_encoder_ids: Optional[List[str]] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None + page_cursor: Optional[Union[str, Null]] = None, ) -> List[AcsEncoder]: """Returns a list of all `encoders `_. @@ -203,7 +236,7 @@ def list( :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. :returns: OK""" - json_payload = {} + json_payload: Dict[str, Any] = {} if acs_system_id is not None: json_payload["acs_system_id"] = acs_system_id @@ -220,12 +253,17 @@ def list( return [AcsEncoder.from_dict(item) for item in res["acs_encoders"]] + @route_metadata( + path="/acs/encoders/scan_credential", + has_required_parameters=True, + has_pagination=False, + ) def scan_credential( self, *, acs_encoder_id: str, salto_ks_metadata: Optional[Dict[str, Any]] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Scans an encoded `acs_credential `_ from a plastic card placed on the specified `encoder `_. @@ -235,14 +273,21 @@ def scan_credential( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_encoder_id is not None: json_payload["acs_encoder_id"] = acs_encoder_id if salto_ks_metadata is not None: json_payload["salto_ks_metadata"] = salto_ks_metadata + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/encoders/scan_credential" + ) + res = self.client.post("/acs/encoders/scan_credential", json=json_payload) wait_for_action_attempt = ( @@ -257,6 +302,11 @@ def scan_credential( wait_for_action_attempt=wait_for_action_attempt, ) + @route_metadata( + path="/acs/encoders/scan_to_assign_credential", + has_required_parameters=True, + has_pagination=False, + ) def scan_to_assign_credential( self, *, @@ -264,7 +314,7 @@ def scan_to_assign_credential( acs_user_id: Optional[str] = None, salto_ks_metadata: Optional[Dict[str, Any]] = None, user_identity_id: Optional[str] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Scans a physical card placed on the specified `encoder `_ and assigns the scanned credential to an ACS user. Provide either an ``acs_user_id`` or a ``user_identity_id``. @@ -278,8 +328,10 @@ def scan_to_assign_credential( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_encoder_id is not None: json_payload["acs_encoder_id"] = acs_encoder_id @@ -290,6 +342,11 @@ def scan_to_assign_credential( if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/encoders/scan_to_assign_credential" + ) + res = self.client.post( "/acs/encoders/scan_to_assign_credential", json=json_payload ) diff --git a/seam/routes/acs_encoders_simulate.py b/seam/routes/acs_encoders_simulate.py index e08aaf10..ac0d1793 100644 --- a/seam/routes/acs_encoders_simulate.py +++ b/seam/routes/acs_encoders_simulate.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata class AbstractAcsEncodersSimulate(abc.ABC): @@ -11,7 +12,7 @@ def next_credential_encode_will_fail( *, acs_encoder_id: str, error_code: Optional[str] = None, - acs_credential_id: Optional[str] = None + acs_credential_id: Optional[str] = None, ) -> None: """Simulates that the next attempt to encode a `credential `_ using the specified `encoder `_ will fail. You can only perform this action within a `sandbox workspace `_. @@ -20,7 +21,8 @@ def next_credential_encode_will_fail( :param error_code: Code of the error to simulate. :param acs_credential_id: ID of the ``acs_credential`` that will fail to be encoded onto a card in the next request. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -31,7 +33,9 @@ def next_credential_encode_will_succeed( :param acs_encoder_id: ID of the ``acs_encoder`` that will be used in the next request to encode the ``acs_credential``. - :param scenario: Scenario to simulate.""" + :param scenario: Scenario to simulate. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -40,7 +44,7 @@ def next_credential_scan_will_fail( *, acs_encoder_id: str, error_code: Optional[str] = None, - acs_credential_id_on_seam: Optional[str] = None + acs_credential_id_on_seam: Optional[str] = None, ) -> None: """Simulates that the next attempt to scan a `credential `_ using the specified `encoder `_ will fail. You can only perform this action within a `sandbox workspace `_. @@ -48,7 +52,9 @@ def next_credential_scan_will_fail( :param error_code: - :param acs_credential_id_on_seam:""" + :param acs_credential_id_on_seam: + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -57,7 +63,7 @@ def next_credential_scan_will_succeed( *, acs_encoder_id: str, acs_credential_id_on_seam: Optional[str] = None, - scenario: Optional[str] = None + scenario: Optional[str] = None, ) -> None: """Simulates that the next attempt to scan a `credential `_ using the specified `encoder `_ will succeed. You can only perform this action within a `sandbox workspace `_. @@ -65,7 +71,9 @@ def next_credential_scan_will_succeed( :param acs_credential_id_on_seam: ID of the Seam ``acs_credential`` that matches the ``acs_credential`` on the encoder in this simulation. - :param scenario: Scenario to simulate.""" + :param scenario: Scenario to simulate. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -74,12 +82,17 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/acs/encoders/simulate/next_credential_encode_will_fail", + has_required_parameters=True, + has_pagination=False, + ) def next_credential_encode_will_fail( self, *, acs_encoder_id: str, error_code: Optional[str] = None, - acs_credential_id: Optional[str] = None + acs_credential_id: Optional[str] = None, ) -> None: """Simulates that the next attempt to encode a `credential `_ using the specified `encoder `_ will fail. You can only perform this action within a `sandbox workspace `_. @@ -88,8 +101,9 @@ def next_credential_encode_will_fail( :param error_code: Code of the error to simulate. :param acs_credential_id: ID of the ``acs_credential`` that will fail to be encoded onto a card in the next request. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_encoder_id is not None: json_payload["acs_encoder_id"] = acs_encoder_id @@ -98,12 +112,22 @@ def next_credential_encode_will_fail( if acs_credential_id is not None: json_payload["acs_credential_id"] = acs_credential_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/encoders/simulate/next_credential_encode_will_fail" + ) + self.client.post( "/acs/encoders/simulate/next_credential_encode_will_fail", json=json_payload ) return None + @route_metadata( + path="/acs/encoders/simulate/next_credential_encode_will_succeed", + has_required_parameters=True, + has_pagination=False, + ) def next_credential_encode_will_succeed( self, *, acs_encoder_id: str, scenario: Optional[str] = None ) -> None: @@ -111,14 +135,21 @@ def next_credential_encode_will_succeed( :param acs_encoder_id: ID of the ``acs_encoder`` that will be used in the next request to encode the ``acs_credential``. - :param scenario: Scenario to simulate.""" - json_payload = {} + :param scenario: Scenario to simulate. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_encoder_id is not None: json_payload["acs_encoder_id"] = acs_encoder_id if scenario is not None: json_payload["scenario"] = scenario + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/encoders/simulate/next_credential_encode_will_succeed" + ) + self.client.post( "/acs/encoders/simulate/next_credential_encode_will_succeed", json=json_payload, @@ -126,12 +157,17 @@ def next_credential_encode_will_succeed( return None + @route_metadata( + path="/acs/encoders/simulate/next_credential_scan_will_fail", + has_required_parameters=True, + has_pagination=False, + ) def next_credential_scan_will_fail( self, *, acs_encoder_id: str, error_code: Optional[str] = None, - acs_credential_id_on_seam: Optional[str] = None + acs_credential_id_on_seam: Optional[str] = None, ) -> None: """Simulates that the next attempt to scan a `credential `_ using the specified `encoder `_ will fail. You can only perform this action within a `sandbox workspace `_. @@ -139,8 +175,10 @@ def next_credential_scan_will_fail( :param error_code: - :param acs_credential_id_on_seam:""" - json_payload = {} + :param acs_credential_id_on_seam: + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_encoder_id is not None: json_payload["acs_encoder_id"] = acs_encoder_id @@ -149,18 +187,28 @@ def next_credential_scan_will_fail( if acs_credential_id_on_seam is not None: json_payload["acs_credential_id_on_seam"] = acs_credential_id_on_seam + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/encoders/simulate/next_credential_scan_will_fail" + ) + self.client.post( "/acs/encoders/simulate/next_credential_scan_will_fail", json=json_payload ) return None + @route_metadata( + path="/acs/encoders/simulate/next_credential_scan_will_succeed", + has_required_parameters=True, + has_pagination=False, + ) def next_credential_scan_will_succeed( self, *, acs_encoder_id: str, acs_credential_id_on_seam: Optional[str] = None, - scenario: Optional[str] = None + scenario: Optional[str] = None, ) -> None: """Simulates that the next attempt to scan a `credential `_ using the specified `encoder `_ will succeed. You can only perform this action within a `sandbox workspace `_. @@ -168,8 +216,10 @@ def next_credential_scan_will_succeed( :param acs_credential_id_on_seam: ID of the Seam ``acs_credential`` that matches the ``acs_credential`` on the encoder in this simulation. - :param scenario: Scenario to simulate.""" - json_payload = {} + :param scenario: Scenario to simulate. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_encoder_id is not None: json_payload["acs_encoder_id"] = acs_encoder_id @@ -178,6 +228,11 @@ def next_credential_scan_will_succeed( if scenario is not None: json_payload["scenario"] = scenario + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/encoders/simulate/next_credential_scan_will_succeed" + ) + self.client.post( "/acs/encoders/simulate/next_credential_scan_will_succeed", json=json_payload, diff --git a/seam/routes/acs_entrances.py b/seam/routes/acs_entrances.py index 104779c0..ddb863d4 100644 --- a/seam/routes/acs_entrances.py +++ b/seam/routes/acs_entrances.py @@ -1,6 +1,8 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +from ..null import Null from ..resources import AcsEntrance, AcsCredential, ActionAttempt from ..modules.action_attempts import resolve_action_attempt @@ -13,7 +15,9 @@ def get(self, *, acs_entrance_id: str) -> AcsEntrance: :param acs_entrance_id: ID of the entrance that you want to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -22,7 +26,7 @@ def grant_access( *, acs_entrance_id: str, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """Grants a specified `access system user `_ access to a specified `access system entrance `_. @@ -31,7 +35,8 @@ def grant_access( :param acs_user_id: ID of the access system user to whom you want to grant access to an entrance. You can only provide one of acs_user_id or user_identity_id. :param user_identity_id: ID of the user identity to whom you want to grant access to an entrance. You can only provide one of acs_user_id or user_identity_id. If the ACS system contains an ACS user with the same ``email_address`` or ``phone_number`` as the user identity that you specify, they are linked, and the access group membership belongs to the ACS user. If the ACS system does not have a corresponding ACS user, one is created. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -45,10 +50,10 @@ def list( connected_account_id: Optional[str] = None, customer_key: Optional[str] = None, limit: Optional[int] = None, - location_id: Optional[str] = None, - page_cursor: Optional[str] = None, + location_id: Optional[Union[str, Null]] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, - space_id: Optional[str] = None + space_id: Optional[str] = None, ) -> List[AcsEntrance]: """Returns a list of all `access system entrances `_. @@ -87,7 +92,9 @@ def list_credentials_with_access( :param include_if: Conditions that credentials must meet to be included in the returned list. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -96,7 +103,7 @@ def unlock( *, acs_credential_id: str, acs_entrance_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Remotely unlocks a specified `entrance `_ using a cloud_key credential. Returns an action attempt that tracks the progress of the unlock operation. @@ -106,7 +113,9 @@ def unlock( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -115,27 +124,42 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/acs/entrances/get", has_required_parameters=True, has_pagination=False + ) def get(self, *, acs_entrance_id: str) -> AcsEntrance: """Returns a specified `access system entrance `_. :param acs_entrance_id: ID of the entrance that you want to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if acs_entrance_id is not None: - json_payload["acs_entrance_id"] = acs_entrance_id + params["acs_entrance_id"] = acs_entrance_id - res = self.client.post("/acs/entrances/get", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /acs/entrances/get" + ) + + res = self.client.get("/acs/entrances/get", params=params) return AcsEntrance.from_dict(res["acs_entrance"]) + @route_metadata( + path="/acs/entrances/grant_access", + has_required_parameters=True, + has_pagination=False, + ) def grant_access( self, *, acs_entrance_id: str, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """Grants a specified `access system user `_ access to a specified `access system entrance `_. @@ -144,8 +168,9 @@ def grant_access( :param acs_user_id: ID of the access system user to whom you want to grant access to an entrance. You can only provide one of acs_user_id or user_identity_id. :param user_identity_id: ID of the user identity to whom you want to grant access to an entrance. You can only provide one of acs_user_id or user_identity_id. If the ACS system contains an ACS user with the same ``email_address`` or ``phone_number`` as the user identity that you specify, they are linked, and the access group membership belongs to the ACS user. If the ACS system does not have a corresponding ACS user, one is created. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_entrance_id is not None: json_payload["acs_entrance_id"] = acs_entrance_id @@ -154,10 +179,18 @@ def grant_access( if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/entrances/grant_access" + ) + self.client.post("/acs/entrances/grant_access", json=json_payload) return None + @route_metadata( + path="/acs/entrances/list", has_required_parameters=False, has_pagination=True + ) def list( self, *, @@ -168,10 +201,10 @@ def list( connected_account_id: Optional[str] = None, customer_key: Optional[str] = None, limit: Optional[int] = None, - location_id: Optional[str] = None, - page_cursor: Optional[str] = None, + location_id: Optional[Union[str, Null]] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, - space_id: Optional[str] = None + space_id: Optional[str] = None, ) -> List[AcsEntrance]: """Returns a list of all `access system entrances `_. @@ -198,7 +231,7 @@ def list( :param space_id: ID of the space for which you want to list entrances. :returns: OK""" - json_payload = {} + json_payload: Dict[str, Any] = {} if access_method_id is not None: json_payload["access_method_id"] = access_method_id @@ -227,6 +260,11 @@ def list( return [AcsEntrance.from_dict(item) for item in res["acs_entrances"]] + @route_metadata( + path="/acs/entrances/list_credentials_with_access", + has_required_parameters=True, + has_pagination=False, + ) def list_credentials_with_access( self, *, acs_entrance_id: str, include_if: Optional[List[str]] = None ) -> List[AcsCredential]: @@ -236,26 +274,36 @@ def list_credentials_with_access( :param include_if: Conditions that credentials must meet to be included in the returned list. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_entrance_id is not None: json_payload["acs_entrance_id"] = acs_entrance_id if include_if is not None: json_payload["include_if"] = include_if + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/entrances/list_credentials_with_access" + ) + res = self.client.post( "/acs/entrances/list_credentials_with_access", json=json_payload ) return [AcsCredential.from_dict(item) for item in res["acs_credentials"]] + @route_metadata( + path="/acs/entrances/unlock", has_required_parameters=True, has_pagination=False + ) def unlock( self, *, acs_credential_id: str, acs_entrance_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Remotely unlocks a specified `entrance `_ using a cloud_key credential. Returns an action attempt that tracks the progress of the unlock operation. @@ -265,14 +313,21 @@ def unlock( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_credential_id is not None: json_payload["acs_credential_id"] = acs_credential_id if acs_entrance_id is not None: json_payload["acs_entrance_id"] = acs_entrance_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/entrances/unlock" + ) + res = self.client.post("/acs/entrances/unlock", json=json_payload) wait_for_action_attempt = ( diff --git a/seam/routes/acs_systems.py b/seam/routes/acs_systems.py index b4ca7d3d..7eb41612 100644 --- a/seam/routes/acs_systems.py +++ b/seam/routes/acs_systems.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata from ..resources import AcsSystem @@ -12,7 +13,9 @@ def get(self, *, acs_system_id: str) -> AcsSystem: :param acs_system_id: ID of the access system that you want to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -21,7 +24,7 @@ def list( *, connected_account_id: Optional[str] = None, customer_key: Optional[str] = None, - search: Optional[str] = None + search: Optional[str] = None, ) -> List[AcsSystem]: """Returns a list of all `access systems `_. @@ -46,7 +49,9 @@ def list_compatible_credential_manager_acs_systems( :param acs_system_id: ID of the access system for which you want to retrieve all compatible credential manager systems. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -55,7 +60,7 @@ def report_devices( *, acs_system_id: str, acs_encoders: Optional[List[Dict[str, Any]]] = None, - acs_entrances: Optional[List[Dict[str, Any]]] = None + acs_entrances: Optional[List[Dict[str, Any]]] = None, ) -> None: """Reports ACS system device status including encoders and entrances. @@ -63,7 +68,9 @@ def report_devices( :param acs_encoders: Array of ACS encoders to report - :param acs_entrances: Array of ACS entrances to report""" + :param acs_entrances: Array of ACS entrances to report + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -72,27 +79,38 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/acs/systems/get", has_required_parameters=True, has_pagination=False + ) def get(self, *, acs_system_id: str) -> AcsSystem: """Returns a specified `access system `_. :param acs_system_id: ID of the access system that you want to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id - res = self.client.post("/acs/systems/get", json=json_payload) + if not params: + raise ValueError("At least one parameter is required for /acs/systems/get") + + res = self.client.get("/acs/systems/get", params=params) return AcsSystem.from_dict(res["acs_system"]) + @route_metadata( + path="/acs/systems/list", has_required_parameters=False, has_pagination=False + ) def list( self, *, connected_account_id: Optional[str] = None, customer_key: Optional[str] = None, - search: Optional[str] = None + search: Optional[str] = None, ) -> List[AcsSystem]: """Returns a list of all `access systems `_. @@ -105,19 +123,24 @@ def list( :param search: String for which to search. Filters returned access systems to include all records that satisfy a partial match using ``name`` or ``acs_system_id``. :returns: OK""" - json_payload = {} + params: Dict[str, Any] = {} if connected_account_id is not None: - json_payload["connected_account_id"] = connected_account_id + params["connected_account_id"] = connected_account_id if customer_key is not None: - json_payload["customer_key"] = customer_key + params["customer_key"] = customer_key if search is not None: - json_payload["search"] = search + params["search"] = search - res = self.client.post("/acs/systems/list", json=json_payload) + res = self.client.get("/acs/systems/list", params=params) return [AcsSystem.from_dict(item) for item in res["acs_systems"]] + @route_metadata( + path="/acs/systems/list_compatible_credential_manager_acs_systems", + has_required_parameters=True, + has_pagination=False, + ) def list_compatible_credential_manager_acs_systems( self, *, acs_system_id: str ) -> List[AcsSystem]: @@ -127,25 +150,36 @@ def list_compatible_credential_manager_acs_systems( :param acs_system_id: ID of the access system for which you want to retrieve all compatible credential manager systems. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id - res = self.client.post( - "/acs/systems/list_compatible_credential_manager_acs_systems", - json=json_payload, + if not params: + raise ValueError( + "At least one parameter is required for /acs/systems/list_compatible_credential_manager_acs_systems" + ) + + res = self.client.get( + "/acs/systems/list_compatible_credential_manager_acs_systems", params=params ) return [AcsSystem.from_dict(item) for item in res["acs_systems"]] + @route_metadata( + path="/acs/systems/report_devices", + has_required_parameters=True, + has_pagination=False, + ) def report_devices( self, *, acs_system_id: str, acs_encoders: Optional[List[Dict[str, Any]]] = None, - acs_entrances: Optional[List[Dict[str, Any]]] = None + acs_entrances: Optional[List[Dict[str, Any]]] = None, ) -> None: """Reports ACS system device status including encoders and entrances. @@ -153,8 +187,10 @@ def report_devices( :param acs_encoders: Array of ACS encoders to report - :param acs_entrances: Array of ACS entrances to report""" - json_payload = {} + :param acs_entrances: Array of ACS entrances to report + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_system_id is not None: json_payload["acs_system_id"] = acs_system_id @@ -163,6 +199,11 @@ def report_devices( if acs_entrances is not None: json_payload["acs_entrances"] = acs_entrances + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/systems/report_devices" + ) + self.client.post("/acs/systems/report_devices", json=json_payload) return None diff --git a/seam/routes/acs_users.py b/seam/routes/acs_users.py index 7f59c38e..1da621eb 100644 --- a/seam/routes/acs_users.py +++ b/seam/routes/acs_users.py @@ -1,6 +1,8 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +from ..null import Null from ..resources import AcsUser, AcsEntrance @@ -15,7 +17,8 @@ def add_to_access_group( :param acs_access_group_id: ID of the access group to which you want to add an access system user. :param acs_user_id: ID of the access system user that you want to add to an access group. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -29,7 +32,7 @@ def create( email: Optional[str] = None, email_address: Optional[str] = None, phone_number: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> AcsUser: """Creates a new `access system user `_. @@ -49,7 +52,9 @@ def create( :param user_identity_id: ID of the user identity with which you want to associate the new access system user. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -58,7 +63,7 @@ def delete( *, acs_system_id: Optional[str] = None, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """Deletes a specified `access system user `_ and invalidates the access system user's `credentials `_. @@ -67,7 +72,8 @@ def delete( :param acs_user_id: ID of the access system user that you want to delete. You must provide either acs_user_id or user_identity_id :param user_identity_id: ID of the user identity that you want to delete. You must provide either acs_user_id or user_identity_id. If you provide user_identity_id, you must also provide acs_system_id. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -76,7 +82,7 @@ def get( *, acs_user_id: Optional[str] = None, acs_system_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> AcsUser: """Returns a specified `access system user `_. @@ -86,7 +92,9 @@ def get( :param user_identity_id: ID of the user identity that you want to get. You can only provide acs_user_id or user_identity_id. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -96,11 +104,11 @@ def list( acs_system_id: Optional[str] = None, created_before: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, user_identity_email_address: Optional[str] = None, user_identity_id: Optional[str] = None, - user_identity_phone_number: Optional[str] = None + user_identity_phone_number: Optional[str] = None, ) -> List[AcsUser]: """Returns a list of all `access system users `_. @@ -129,7 +137,7 @@ def list_accessible_entrances( *, acs_system_id: Optional[str] = None, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> List[AcsEntrance]: """Lists the `entrances `_ to which a specified `access system user `_ has access. @@ -139,7 +147,9 @@ def list_accessible_entrances( :param user_identity_id: ID of the user identity for whom you want to list accessible entrances. You can only provide acs_user_id or user_identity_id. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -148,7 +158,7 @@ def remove_from_access_group( *, acs_access_group_id: str, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """Removes a specified `access system user `_ from a specified `access group `_. @@ -157,7 +167,8 @@ def remove_from_access_group( :param acs_user_id: ID of the access system user that you want to remove from an access group. You can only provide acs_user_id or user_identity_id. :param user_identity_id: ID of the user identity that you want to remove from an access group. You can only provide acs_user_id or user_identity_id. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -166,7 +177,7 @@ def revoke_access_to_all_entrances( *, acs_system_id: Optional[str] = None, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """Revokes access to all `entrances `_ for a specified `access system user `_. @@ -175,7 +186,8 @@ def revoke_access_to_all_entrances( :param acs_user_id: ID of the access system user for whom you want to revoke access. You can only provide acs_user_id or user_identity_id. :param user_identity_id: ID of the user identity for whom you want to revoke access. You can only provide acs_user_id or user_identity_id. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -184,7 +196,7 @@ def suspend( *, acs_system_id: Optional[str] = None, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """`Suspends `_ a specified `access system user `_. Suspending an access system user revokes their access temporarily. To restore an access system user's access, you can `unsuspend `_ them. @@ -193,7 +205,8 @@ def suspend( :param acs_user_id: ID of the access system user that you want to suspend. You can only provide acs_user_id or the combination of acs_system_id and user_identity_id. :param user_identity_id: ID of the user identity that you want to suspend. You can only provide acs_user_id or the combination of acs_system_id and user_identity_id. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -202,7 +215,7 @@ def unsuspend( *, acs_system_id: Optional[str] = None, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """`Unsuspends `_ a specified suspended `access system user `_. While `suspending an access system user `_ revokes their access temporarily, unsuspending the access system user restores their access. @@ -211,14 +224,15 @@ def unsuspend( :param acs_user_id: ID of the access system user that you want to unsuspend. You can only provide acs_user_id or the combination of acs_system_id and user_identity_id. :param user_identity_id: ID of the user identity that you want to unsuspend. You can only provide acs_user_id or the combination of acs_system_id and user_identity_id. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod def update( self, *, - access_schedule: Optional[Dict[str, Any]] = None, + access_schedule: Optional[Union[Dict[str, Any], Null]] = None, acs_system_id: Optional[str] = None, acs_user_id: Optional[str] = None, email: Optional[str] = None, @@ -226,7 +240,7 @@ def update( full_name: Optional[str] = None, hid_acs_system_id: Optional[str] = None, phone_number: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """Updates the properties of a specified `access system user `_. @@ -247,7 +261,8 @@ def update( :param phone_number: Phone number of the `access system user `_ in E.164 format (for example, ``+15555550100``). :param user_identity_id: ID of the user identity that you want to update. You can only provide acs_user_id or user_identity_id. If you provide user_identity_id, you must also provide acs_system_id. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -256,6 +271,11 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/acs/users/add_to_access_group", + has_required_parameters=True, + has_pagination=False, + ) def add_to_access_group( self, *, acs_access_group_id: str, acs_user_id: str ) -> None: @@ -264,18 +284,27 @@ def add_to_access_group( :param acs_access_group_id: ID of the access group to which you want to add an access system user. :param acs_user_id: ID of the access system user that you want to add to an access group. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_access_group_id is not None: json_payload["acs_access_group_id"] = acs_access_group_id if acs_user_id is not None: json_payload["acs_user_id"] = acs_user_id - self.client.post("/acs/users/add_to_access_group", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/users/add_to_access_group" + ) + + self.client.put("/acs/users/add_to_access_group", json=json_payload) return None + @route_metadata( + path="/acs/users/create", has_required_parameters=True, has_pagination=False + ) def create( self, *, @@ -286,7 +315,7 @@ def create( email: Optional[str] = None, email_address: Optional[str] = None, phone_number: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> AcsUser: """Creates a new `access system user `_. @@ -306,8 +335,10 @@ def create( :param user_identity_id: ID of the user identity with which you want to associate the new access system user. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_system_id is not None: json_payload["acs_system_id"] = acs_system_id @@ -326,16 +357,22 @@ def create( if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id + if not json_payload: + raise ValueError("At least one parameter is required for /acs/users/create") + res = self.client.post("/acs/users/create", json=json_payload) return AcsUser.from_dict(res["acs_user"]) + @route_metadata( + path="/acs/users/delete", has_required_parameters=True, has_pagination=False + ) def delete( self, *, acs_system_id: Optional[str] = None, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """Deletes a specified `access system user `_ and invalidates the access system user's `credentials `_. @@ -344,26 +381,33 @@ def delete( :param acs_user_id: ID of the access system user that you want to delete. You must provide either acs_user_id or user_identity_id :param user_identity_id: ID of the user identity that you want to delete. You must provide either acs_user_id or user_identity_id. If you provide user_identity_id, you must also provide acs_system_id. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id if acs_user_id is not None: - json_payload["acs_user_id"] = acs_user_id + params["acs_user_id"] = acs_user_id if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError("At least one parameter is required for /acs/users/delete") - self.client.post("/acs/users/delete", json=json_payload) + self.client.delete("/acs/users/delete", params=params) return None + @route_metadata( + path="/acs/users/get", has_required_parameters=True, has_pagination=False + ) def get( self, *, acs_user_id: Optional[str] = None, acs_system_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> AcsUser: """Returns a specified `access system user `_. @@ -373,31 +417,39 @@ def get( :param user_identity_id: ID of the user identity that you want to get. You can only provide acs_user_id or user_identity_id. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if acs_user_id is not None: - json_payload["acs_user_id"] = acs_user_id + params["acs_user_id"] = acs_user_id if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError("At least one parameter is required for /acs/users/get") - res = self.client.post("/acs/users/get", json=json_payload) + res = self.client.get("/acs/users/get", params=params) return AcsUser.from_dict(res["acs_user"]) + @route_metadata( + path="/acs/users/list", has_required_parameters=False, has_pagination=True + ) def list( self, *, acs_system_id: Optional[str] = None, created_before: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, user_identity_email_address: Optional[str] = None, user_identity_id: Optional[str] = None, - user_identity_phone_number: Optional[str] = None + user_identity_phone_number: Optional[str] = None, ) -> List[AcsUser]: """Returns a list of all `access system users `_. @@ -418,35 +470,40 @@ def list( :param user_identity_phone_number: Phone number of the user identity for which you want to retrieve all access system users, in `E.164 format `_ (for example, ``+15555550100``). :returns: OK""" - json_payload = {} + params: Dict[str, Any] = {} if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id if created_before is not None: - json_payload["created_before"] = created_before + params["created_before"] = created_before if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if search is not None: - json_payload["search"] = search + params["search"] = search if user_identity_email_address is not None: - json_payload["user_identity_email_address"] = user_identity_email_address + params["user_identity_email_address"] = user_identity_email_address if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id if user_identity_phone_number is not None: - json_payload["user_identity_phone_number"] = user_identity_phone_number + params["user_identity_phone_number"] = user_identity_phone_number - res = self.client.post("/acs/users/list", json=json_payload) + res = self.client.get("/acs/users/list", params=params) return [AcsUser.from_dict(item) for item in res["acs_users"]] + @route_metadata( + path="/acs/users/list_accessible_entrances", + has_required_parameters=True, + has_pagination=False, + ) def list_accessible_entrances( self, *, acs_system_id: Optional[str] = None, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> List[AcsEntrance]: """Lists the `entrances `_ to which a specified `access system user `_ has access. @@ -456,28 +513,38 @@ def list_accessible_entrances( :param user_identity_id: ID of the user identity for whom you want to list accessible entrances. You can only provide acs_user_id or user_identity_id. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if acs_system_id is not None: - json_payload["acs_system_id"] = acs_system_id + params["acs_system_id"] = acs_system_id if acs_user_id is not None: - json_payload["acs_user_id"] = acs_user_id + params["acs_user_id"] = acs_user_id if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError( + "At least one parameter is required for /acs/users/list_accessible_entrances" + ) - res = self.client.post( - "/acs/users/list_accessible_entrances", json=json_payload - ) + res = self.client.get("/acs/users/list_accessible_entrances", params=params) return [AcsEntrance.from_dict(item) for item in res["acs_entrances"]] + @route_metadata( + path="/acs/users/remove_from_access_group", + has_required_parameters=True, + has_pagination=False, + ) def remove_from_access_group( self, *, acs_access_group_id: str, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """Removes a specified `access system user `_ from a specified `access group `_. @@ -486,26 +553,37 @@ def remove_from_access_group( :param acs_user_id: ID of the access system user that you want to remove from an access group. You can only provide acs_user_id or user_identity_id. :param user_identity_id: ID of the user identity that you want to remove from an access group. You can only provide acs_user_id or user_identity_id. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if acs_access_group_id is not None: - json_payload["acs_access_group_id"] = acs_access_group_id + params["acs_access_group_id"] = acs_access_group_id if acs_user_id is not None: - json_payload["acs_user_id"] = acs_user_id + params["acs_user_id"] = acs_user_id if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id - self.client.post("/acs/users/remove_from_access_group", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /acs/users/remove_from_access_group" + ) + + self.client.delete("/acs/users/remove_from_access_group", params=params) return None + @route_metadata( + path="/acs/users/revoke_access_to_all_entrances", + has_required_parameters=True, + has_pagination=False, + ) def revoke_access_to_all_entrances( self, *, acs_system_id: Optional[str] = None, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """Revokes access to all `entrances `_ for a specified `access system user `_. @@ -514,8 +592,9 @@ def revoke_access_to_all_entrances( :param acs_user_id: ID of the access system user for whom you want to revoke access. You can only provide acs_user_id or user_identity_id. :param user_identity_id: ID of the user identity for whom you want to revoke access. You can only provide acs_user_id or user_identity_id. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_system_id is not None: json_payload["acs_system_id"] = acs_system_id @@ -524,16 +603,24 @@ def revoke_access_to_all_entrances( if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/users/revoke_access_to_all_entrances" + ) + self.client.post("/acs/users/revoke_access_to_all_entrances", json=json_payload) return None + @route_metadata( + path="/acs/users/suspend", has_required_parameters=True, has_pagination=False + ) def suspend( self, *, acs_system_id: Optional[str] = None, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """`Suspends `_ a specified `access system user `_. Suspending an access system user revokes their access temporarily. To restore an access system user's access, you can `unsuspend `_ them. @@ -542,8 +629,9 @@ def suspend( :param acs_user_id: ID of the access system user that you want to suspend. You can only provide acs_user_id or the combination of acs_system_id and user_identity_id. :param user_identity_id: ID of the user identity that you want to suspend. You can only provide acs_user_id or the combination of acs_system_id and user_identity_id. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_system_id is not None: json_payload["acs_system_id"] = acs_system_id @@ -552,16 +640,24 @@ def suspend( if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/users/suspend" + ) + self.client.post("/acs/users/suspend", json=json_payload) return None + @route_metadata( + path="/acs/users/unsuspend", has_required_parameters=True, has_pagination=False + ) def unsuspend( self, *, acs_system_id: Optional[str] = None, acs_user_id: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """`Unsuspends `_ a specified suspended `access system user `_. While `suspending an access system user `_ revokes their access temporarily, unsuspending the access system user restores their access. @@ -570,8 +666,9 @@ def unsuspend( :param acs_user_id: ID of the access system user that you want to unsuspend. You can only provide acs_user_id or the combination of acs_system_id and user_identity_id. :param user_identity_id: ID of the user identity that you want to unsuspend. You can only provide acs_user_id or the combination of acs_system_id and user_identity_id. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_system_id is not None: json_payload["acs_system_id"] = acs_system_id @@ -580,14 +677,22 @@ def unsuspend( if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /acs/users/unsuspend" + ) + self.client.post("/acs/users/unsuspend", json=json_payload) return None + @route_metadata( + path="/acs/users/update", has_required_parameters=True, has_pagination=False + ) def update( self, *, - access_schedule: Optional[Dict[str, Any]] = None, + access_schedule: Optional[Union[Dict[str, Any], Null]] = None, acs_system_id: Optional[str] = None, acs_user_id: Optional[str] = None, email: Optional[str] = None, @@ -595,7 +700,7 @@ def update( full_name: Optional[str] = None, hid_acs_system_id: Optional[str] = None, phone_number: Optional[str] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> None: """Updates the properties of a specified `access system user `_. @@ -616,8 +721,9 @@ def update( :param phone_number: Phone number of the `access system user `_ in E.164 format (for example, ``+15555550100``). :param user_identity_id: ID of the user identity that you want to update. You can only provide acs_user_id or user_identity_id. If you provide user_identity_id, you must also provide acs_system_id. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if access_schedule is not None: json_payload["access_schedule"] = access_schedule @@ -638,6 +744,9 @@ def update( if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id - self.client.post("/acs/users/update", json=json_payload) + if not json_payload: + raise ValueError("At least one parameter is required for /acs/users/update") + + self.client.patch("/acs/users/update", json=json_payload) return None diff --git a/seam/routes/action_attempts.py b/seam/routes/action_attempts.py index e14dfe8e..8f0c221c 100644 --- a/seam/routes/action_attempts.py +++ b/seam/routes/action_attempts.py @@ -1,6 +1,8 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +from ..null import Null from ..resources import ActionAttempt from ..modules.action_attempts import resolve_action_attempt @@ -12,7 +14,7 @@ def get( self, *, action_attempt_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Returns a specified `action attempt `_. @@ -20,7 +22,9 @@ def get( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -30,7 +34,7 @@ def list( action_attempt_ids: Optional[List[str]] = None, device_id: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = None + page_cursor: Optional[Union[str, Null]] = None, ) -> List[ActionAttempt]: """Returns a list of the `action attempts `_ that you specify as an array of ``action_attempt_id``s. @@ -51,11 +55,14 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/action_attempts/get", has_required_parameters=True, has_pagination=False + ) def get( self, *, action_attempt_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Returns a specified `action attempt `_. @@ -63,13 +70,20 @@ def get( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if action_attempt_id is not None: - json_payload["action_attempt_id"] = action_attempt_id + params["action_attempt_id"] = action_attempt_id + + if not params: + raise ValueError( + "At least one parameter is required for /action_attempts/get" + ) - res = self.client.post("/action_attempts/get", json=json_payload) + res = self.client.get("/action_attempts/get", params=params) wait_for_action_attempt = ( self.defaults.get("wait_for_action_attempt") @@ -83,13 +97,16 @@ def get( wait_for_action_attempt=wait_for_action_attempt, ) + @route_metadata( + path="/action_attempts/list", has_required_parameters=False, has_pagination=True + ) def list( self, *, action_attempt_ids: Optional[List[str]] = None, device_id: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = None + page_cursor: Optional[Union[str, Null]] = None, ) -> List[ActionAttempt]: """Returns a list of the `action attempts `_ that you specify as an array of ``action_attempt_id``s. @@ -102,7 +119,7 @@ def list( :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. :returns: OK""" - json_payload = {} + json_payload: Dict[str, Any] = {} if action_attempt_ids is not None: json_payload["action_attempt_ids"] = action_attempt_ids diff --git a/seam/routes/client_sessions.py b/seam/routes/client_sessions.py index 29f996af..62da3156 100644 --- a/seam/routes/client_sessions.py +++ b/seam/routes/client_sessions.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata from ..resources import ClientSession @@ -17,7 +18,7 @@ def create( expires_at: Optional[str] = None, user_identifier_key: Optional[str] = None, user_identity_id: Optional[str] = None, - user_identity_ids: Optional[List[str]] = None + user_identity_ids: Optional[List[str]] = None, ) -> ClientSession: """Creates a new `client session `_. @@ -44,7 +45,9 @@ def create( def delete(self, *, client_session_id: str) -> None: """Deletes a `client session `_. - :param client_session_id: ID of the client session that you want to delete.""" + :param client_session_id: ID of the client session that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -52,7 +55,7 @@ def get( self, *, client_session_id: Optional[str] = None, - user_identifier_key: Optional[str] = None + user_identifier_key: Optional[str] = None, ) -> ClientSession: """Returns a specified `client session `_. @@ -72,7 +75,7 @@ def get_or_create( expires_at: Optional[str] = None, user_identifier_key: Optional[str] = None, user_identity_id: Optional[str] = None, - user_identity_ids: Optional[List[str]] = None + user_identity_ids: Optional[List[str]] = None, ) -> ClientSession: """Returns a `client session `_ with specific characteristics or creates a new client session with these characteristics if it does not yet exist. @@ -100,7 +103,7 @@ def grant_access( connected_account_ids: Optional[List[str]] = None, user_identifier_key: Optional[str] = None, user_identity_id: Optional[str] = None, - user_identity_ids: Optional[List[str]] = None + user_identity_ids: Optional[List[str]] = None, ) -> None: """Grants a `client session `_ access to one or more resources, such as `Connect Webviews `_, `user identities `_, and so on. @@ -115,7 +118,8 @@ def grant_access( :param user_identity_id: ID of the `user identity `_ that you want to associate with the client session. :param user_identity_ids: Deprecated: Use ``user_identity_id``. IDs of the `user identities `_ that you want to associate with the client session. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -126,7 +130,7 @@ def list( connect_webview_id: Optional[str] = None, user_identifier_key: Optional[str] = None, user_identity_id: Optional[str] = None, - without_user_identifier_key: Optional[bool] = None + without_user_identifier_key: Optional[bool] = None, ) -> List[ClientSession]: """Returns a list of all `client sessions `_. @@ -149,7 +153,9 @@ def revoke(self, *, client_session_id: str) -> None: Note that `deleting a client session `_ is a separate action. - :param client_session_id: ID of the client session that you want to revoke.""" + :param client_session_id: ID of the client session that you want to revoke. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -158,6 +164,11 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/client_sessions/create", + has_required_parameters=False, + has_pagination=False, + ) def create( self, *, @@ -168,7 +179,7 @@ def create( expires_at: Optional[str] = None, user_identifier_key: Optional[str] = None, user_identity_id: Optional[str] = None, - user_identity_ids: Optional[List[str]] = None + user_identity_ids: Optional[List[str]] = None, ) -> ClientSession: """Creates a new `client session `_. @@ -189,7 +200,7 @@ def create( :param user_identity_ids: Deprecated: Use ``user_identity_id`` instead. IDs of the `user identities `_ that you want to associate with the client session. :returns: OK""" - json_payload = {} + json_payload: Dict[str, Any] = {} if connect_webview_ids is not None: json_payload["connect_webview_ids"] = connect_webview_ids @@ -208,28 +219,43 @@ def create( if user_identity_ids is not None: json_payload["user_identity_ids"] = user_identity_ids - res = self.client.post("/client_sessions/create", json=json_payload) + res = self.client.put("/client_sessions/create", json=json_payload) return ClientSession.from_dict(res["client_session"]) + @route_metadata( + path="/client_sessions/delete", + has_required_parameters=True, + has_pagination=False, + ) def delete(self, *, client_session_id: str) -> None: """Deletes a `client session `_. - :param client_session_id: ID of the client session that you want to delete.""" - json_payload = {} + :param client_session_id: ID of the client session that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if client_session_id is not None: - json_payload["client_session_id"] = client_session_id + params["client_session_id"] = client_session_id + + if not params: + raise ValueError( + "At least one parameter is required for /client_sessions/delete" + ) - self.client.post("/client_sessions/delete", json=json_payload) + self.client.delete("/client_sessions/delete", params=params) return None + @route_metadata( + path="/client_sessions/get", has_required_parameters=False, has_pagination=False + ) def get( self, *, client_session_id: Optional[str] = None, - user_identifier_key: Optional[str] = None + user_identifier_key: Optional[str] = None, ) -> ClientSession: """Returns a specified `client session `_. @@ -238,17 +264,22 @@ def get( :param user_identifier_key: User identifier key associated with the client session that you want to get. :returns: OK""" - json_payload = {} + params: Dict[str, Any] = {} if client_session_id is not None: - json_payload["client_session_id"] = client_session_id + params["client_session_id"] = client_session_id if user_identifier_key is not None: - json_payload["user_identifier_key"] = user_identifier_key + params["user_identifier_key"] = user_identifier_key - res = self.client.post("/client_sessions/get", json=json_payload) + res = self.client.get("/client_sessions/get", params=params) return ClientSession.from_dict(res["client_session"]) + @route_metadata( + path="/client_sessions/get_or_create", + has_required_parameters=False, + has_pagination=False, + ) def get_or_create( self, *, @@ -257,7 +288,7 @@ def get_or_create( expires_at: Optional[str] = None, user_identifier_key: Optional[str] = None, user_identity_id: Optional[str] = None, - user_identity_ids: Optional[List[str]] = None + user_identity_ids: Optional[List[str]] = None, ) -> ClientSession: """Returns a `client session `_ with specific characteristics or creates a new client session with these characteristics if it does not yet exist. @@ -274,7 +305,7 @@ def get_or_create( :param user_identity_ids: Deprecated: Use ``user_identity_id``. IDs of the `user identities `_ that you want to associate with the client session. :returns: OK""" - json_payload = {} + json_payload: Dict[str, Any] = {} if connect_webview_ids is not None: json_payload["connect_webview_ids"] = connect_webview_ids @@ -293,6 +324,11 @@ def get_or_create( return ClientSession.from_dict(res["client_session"]) + @route_metadata( + path="/client_sessions/grant_access", + has_required_parameters=True, + has_pagination=False, + ) def grant_access( self, *, @@ -301,7 +337,7 @@ def grant_access( connected_account_ids: Optional[List[str]] = None, user_identifier_key: Optional[str] = None, user_identity_id: Optional[str] = None, - user_identity_ids: Optional[List[str]] = None + user_identity_ids: Optional[List[str]] = None, ) -> None: """Grants a `client session `_ access to one or more resources, such as `Connect Webviews `_, `user identities `_, and so on. @@ -316,8 +352,9 @@ def grant_access( :param user_identity_id: ID of the `user identity `_ that you want to associate with the client session. :param user_identity_ids: Deprecated: Use ``user_identity_id``. IDs of the `user identities `_ that you want to associate with the client session. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if client_session_id is not None: json_payload["client_session_id"] = client_session_id @@ -332,10 +369,20 @@ def grant_access( if user_identity_ids is not None: json_payload["user_identity_ids"] = user_identity_ids - self.client.post("/client_sessions/grant_access", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /client_sessions/grant_access" + ) + + self.client.patch("/client_sessions/grant_access", json=json_payload) return None + @route_metadata( + path="/client_sessions/list", + has_required_parameters=False, + has_pagination=False, + ) def list( self, *, @@ -343,7 +390,7 @@ def list( connect_webview_id: Optional[str] = None, user_identifier_key: Optional[str] = None, user_identity_id: Optional[str] = None, - without_user_identifier_key: Optional[bool] = None + without_user_identifier_key: Optional[bool] = None, ) -> List[ClientSession]: """Returns a list of all `client sessions `_. @@ -358,34 +405,46 @@ def list( :param without_user_identifier_key: Indicates whether to retrieve only client sessions without associated user identifier keys. :returns: OK""" - json_payload = {} + params: Dict[str, Any] = {} if client_session_id is not None: - json_payload["client_session_id"] = client_session_id + params["client_session_id"] = client_session_id if connect_webview_id is not None: - json_payload["connect_webview_id"] = connect_webview_id + params["connect_webview_id"] = connect_webview_id if user_identifier_key is not None: - json_payload["user_identifier_key"] = user_identifier_key + params["user_identifier_key"] = user_identifier_key if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id if without_user_identifier_key is not None: - json_payload["without_user_identifier_key"] = without_user_identifier_key + params["without_user_identifier_key"] = without_user_identifier_key - res = self.client.post("/client_sessions/list", json=json_payload) + res = self.client.get("/client_sessions/list", params=params) return [ClientSession.from_dict(item) for item in res["client_sessions"]] + @route_metadata( + path="/client_sessions/revoke", + has_required_parameters=True, + has_pagination=False, + ) def revoke(self, *, client_session_id: str) -> None: """Revokes a `client session `_. Note that `deleting a client session `_ is a separate action. - :param client_session_id: ID of the client session that you want to revoke.""" - json_payload = {} + :param client_session_id: ID of the client session that you want to revoke. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if client_session_id is not None: json_payload["client_session_id"] = client_session_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /client_sessions/revoke" + ) + self.client.post("/client_sessions/revoke", json=json_payload) return None diff --git a/seam/routes/connect_webviews.py b/seam/routes/connect_webviews.py index d6b32d6e..053ed685 100644 --- a/seam/routes/connect_webviews.py +++ b/seam/routes/connect_webviews.py @@ -1,6 +1,8 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +from ..null import Null from ..resources import ConnectWebview @@ -19,7 +21,7 @@ def create( customer_key: Optional[str] = None, excluded_providers: Optional[List[str]] = None, provider_category: Optional[str] = None, - wait_for_device_creation: Optional[bool] = None + wait_for_device_creation: Optional[bool] = None, ) -> ConnectWebview: """Creates a new `Connect Webview `_. @@ -58,7 +60,9 @@ def delete(self, *, connect_webview_id: str) -> None: You do not need to delete a Connect Webview once a user completes it. Instead, you can simply ignore completed Connect Webviews. - :param connect_webview_id: ID of the Connect Webview that you want to delete.""" + :param connect_webview_id: ID of the Connect Webview that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -69,7 +73,9 @@ def get(self, *, connect_webview_id: str) -> ConnectWebview: :param connect_webview_id: ID of the Connect Webview that you want to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -79,9 +85,9 @@ def list( custom_metadata_has: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, - user_identifier_key: Optional[str] = None + user_identifier_key: Optional[str] = None, ) -> List[ConnectWebview]: """Returns a list of all `Connect Webviews `_. @@ -106,6 +112,11 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/connect_webviews/create", + has_required_parameters=False, + has_pagination=False, + ) def create( self, *, @@ -118,7 +129,7 @@ def create( customer_key: Optional[str] = None, excluded_providers: Optional[List[str]] = None, provider_category: Optional[str] = None, - wait_for_device_creation: Optional[bool] = None + wait_for_device_creation: Optional[bool] = None, ) -> ConnectWebview: """Creates a new `Connect Webview `_. @@ -149,7 +160,7 @@ def create( :param wait_for_device_creation: Indicates whether Seam should finish syncing all devices in a newly-connected account before completing the associated Connect Webview. See also: `Customize the Behavior Settings of Your Connect Webviews `_. :returns: OK""" - json_payload = {} + json_payload: Dict[str, Any] = {} if accepted_capabilities is not None: json_payload["accepted_capabilities"] = accepted_capabilities @@ -178,21 +189,36 @@ def create( return ConnectWebview.from_dict(res["connect_webview"]) + @route_metadata( + path="/connect_webviews/delete", + has_required_parameters=True, + has_pagination=False, + ) def delete(self, *, connect_webview_id: str) -> None: """Deletes a `Connect Webview `_. You do not need to delete a Connect Webview once a user completes it. Instead, you can simply ignore completed Connect Webviews. - :param connect_webview_id: ID of the Connect Webview that you want to delete.""" - json_payload = {} + :param connect_webview_id: ID of the Connect Webview that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if connect_webview_id is not None: - json_payload["connect_webview_id"] = connect_webview_id + params["connect_webview_id"] = connect_webview_id + + if not params: + raise ValueError( + "At least one parameter is required for /connect_webviews/delete" + ) - self.client.post("/connect_webviews/delete", json=json_payload) + self.client.delete("/connect_webviews/delete", params=params) return None + @route_metadata( + path="/connect_webviews/get", has_required_parameters=True, has_pagination=False + ) def get(self, *, connect_webview_id: str) -> ConnectWebview: """Returns a specified `Connect Webview `_. @@ -200,25 +226,37 @@ def get(self, *, connect_webview_id: str) -> ConnectWebview: :param connect_webview_id: ID of the Connect Webview that you want to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if connect_webview_id is not None: - json_payload["connect_webview_id"] = connect_webview_id + params["connect_webview_id"] = connect_webview_id + + if not params: + raise ValueError( + "At least one parameter is required for /connect_webviews/get" + ) - res = self.client.post("/connect_webviews/get", json=json_payload) + res = self.client.get("/connect_webviews/get", params=params) return ConnectWebview.from_dict(res["connect_webview"]) + @route_metadata( + path="/connect_webviews/list", + has_required_parameters=False, + has_pagination=True, + ) def list( self, *, custom_metadata_has: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, - user_identifier_key: Optional[str] = None + user_identifier_key: Optional[str] = None, ) -> List[ConnectWebview]: """Returns a list of all `Connect Webviews `_. @@ -235,7 +273,7 @@ def list( :param user_identifier_key: Your user ID for the user by which you want to filter Connect Webviews. :returns: OK""" - json_payload = {} + json_payload: Dict[str, Any] = {} if custom_metadata_has is not None: json_payload["custom_metadata_has"] = custom_metadata_has diff --git a/seam/routes/connected_accounts.py b/seam/routes/connected_accounts.py index bcc979fb..7c50bfd2 100644 --- a/seam/routes/connected_accounts.py +++ b/seam/routes/connected_accounts.py @@ -1,6 +1,8 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +from ..null import Null from ..resources import ConnectedAccount from .connected_accounts_simulate import ( AbstractConnectedAccountsSimulate, @@ -24,7 +26,8 @@ def delete(self, *, connected_account_id: str) -> None: For example, if you delete a connected account with a device that has an access code, Seam sends a ``connected_account.deleted`` event, a ``device.deleted`` event, and an ``access_code.deleted`` event, but Seam does not remove the access code from the device. :param connected_account_id: ID of the connected account that you want to delete. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -37,7 +40,9 @@ def get( :param email: Email address associated with the connected account that you want to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -47,10 +52,10 @@ def list( custom_metadata_has: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, space_id: Optional[str] = None, - user_identifier_key: Optional[str] = None + user_identifier_key: Optional[str] = None, ) -> List[ConnectedAccount]: """Returns a list of all `connected accounts `_. @@ -76,7 +81,8 @@ def sync(self, *, connected_account_id: str) -> None: """Request a `connected account `_ sync attempt for the specified ``connected_account_id``. :param connected_account_id: ID of the connected account that you want to sync. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -88,7 +94,7 @@ def update( automatically_manage_new_devices: Optional[bool] = None, custom_metadata: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, - display_name: Optional[str] = None + display_name: Optional[str] = None, ) -> None: """Updates a `connected account `_. @@ -103,7 +109,8 @@ def update( :param customer_key: The customer key to associate with this connected account. If provided, the connected account and all resources under the connected account will be moved to this customer. May only be provided if the connected account is not already associated with a customer. :param display_name: Human-readable name for the connected account, shown in the dashboard. For example, ``Booking from Airbnb House 1``. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -117,6 +124,11 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): def simulate(self) -> ConnectedAccountsSimulate: return self._simulate + @route_metadata( + path="/connected_accounts/delete", + has_required_parameters=True, + has_pagination=False, + ) def delete(self, *, connected_account_id: str) -> None: """Deletes a specified `connected account `_. @@ -125,16 +137,27 @@ def delete(self, *, connected_account_id: str) -> None: For example, if you delete a connected account with a device that has an access code, Seam sends a ``connected_account.deleted`` event, a ``device.deleted`` event, and an ``access_code.deleted`` event, but Seam does not remove the access code from the device. :param connected_account_id: ID of the connected account that you want to delete. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if connected_account_id is not None: - json_payload["connected_account_id"] = connected_account_id + params["connected_account_id"] = connected_account_id - self.client.post("/connected_accounts/delete", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /connected_accounts/delete" + ) + + self.client.delete("/connected_accounts/delete", params=params) return None + @route_metadata( + path="/connected_accounts/get", + has_required_parameters=True, + has_pagination=False, + ) def get( self, *, connected_account_id: Optional[str] = None, email: Optional[str] = None ) -> ConnectedAccount: @@ -144,28 +167,40 @@ def get( :param email: Email address associated with the connected account that you want to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if connected_account_id is not None: - json_payload["connected_account_id"] = connected_account_id + params["connected_account_id"] = connected_account_id if email is not None: - json_payload["email"] = email + params["email"] = email - res = self.client.post("/connected_accounts/get", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /connected_accounts/get" + ) + + res = self.client.get("/connected_accounts/get", params=params) return ConnectedAccount.from_dict(res["connected_account"]) + @route_metadata( + path="/connected_accounts/list", + has_required_parameters=False, + has_pagination=True, + ) def list( self, *, custom_metadata_has: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, space_id: Optional[str] = None, - user_identifier_key: Optional[str] = None + user_identifier_key: Optional[str] = None, ) -> List[ConnectedAccount]: """Returns a list of all `connected accounts `_. @@ -184,7 +219,7 @@ def list( :param user_identifier_key: Your user ID for the user by which you want to filter connected accounts. :returns: OK""" - json_payload = {} + json_payload: Dict[str, Any] = {} if custom_metadata_has is not None: json_payload["custom_metadata_has"] = custom_metadata_has @@ -205,20 +240,36 @@ def list( return [ConnectedAccount.from_dict(item) for item in res["connected_accounts"]] + @route_metadata( + path="/connected_accounts/sync", + has_required_parameters=True, + has_pagination=False, + ) def sync(self, *, connected_account_id: str) -> None: """Request a `connected account `_ sync attempt for the specified ``connected_account_id``. :param connected_account_id: ID of the connected account that you want to sync. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if connected_account_id is not None: json_payload["connected_account_id"] = connected_account_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /connected_accounts/sync" + ) + self.client.post("/connected_accounts/sync", json=json_payload) return None + @route_metadata( + path="/connected_accounts/update", + has_required_parameters=True, + has_pagination=False, + ) def update( self, *, @@ -227,7 +278,7 @@ def update( automatically_manage_new_devices: Optional[bool] = None, custom_metadata: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, - display_name: Optional[str] = None + display_name: Optional[str] = None, ) -> None: """Updates a `connected account `_. @@ -242,8 +293,9 @@ def update( :param customer_key: The customer key to associate with this connected account. If provided, the connected account and all resources under the connected account will be moved to this customer. May only be provided if the connected account is not already associated with a customer. :param display_name: Human-readable name for the connected account, shown in the dashboard. For example, ``Booking from Airbnb House 1``. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if connected_account_id is not None: json_payload["connected_account_id"] = connected_account_id @@ -260,6 +312,11 @@ def update( if display_name is not None: json_payload["display_name"] = display_name - self.client.post("/connected_accounts/update", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /connected_accounts/update" + ) + + self.client.patch("/connected_accounts/update", json=json_payload) return None diff --git a/seam/routes/connected_accounts_simulate.py b/seam/routes/connected_accounts_simulate.py index 3766b03b..76df8f62 100644 --- a/seam/routes/connected_accounts_simulate.py +++ b/seam/routes/connected_accounts_simulate.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata class AbstractConnectedAccountsSimulate(abc.ABC): @@ -10,7 +11,8 @@ def disconnect(self, *, connected_account_id: str) -> None: """Simulates a connected account becoming disconnected from Seam. Only applicable for `sandbox workspaces `_. :param connected_account_id: ID of the connected account you want to simulate as disconnected. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -19,16 +21,27 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/connected_accounts/simulate/disconnect", + has_required_parameters=True, + has_pagination=False, + ) def disconnect(self, *, connected_account_id: str) -> None: """Simulates a connected account becoming disconnected from Seam. Only applicable for `sandbox workspaces `_. :param connected_account_id: ID of the connected account you want to simulate as disconnected. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if connected_account_id is not None: json_payload["connected_account_id"] = connected_account_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /connected_accounts/simulate/disconnect" + ) + self.client.post("/connected_accounts/simulate/disconnect", json=json_payload) return None diff --git a/seam/routes/customers.py b/seam/routes/customers.py index bdbf4f54..494848c5 100644 --- a/seam/routes/customers.py +++ b/seam/routes/customers.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata from ..resources import CustomerPortal @@ -20,7 +21,7 @@ def create_portal( locale: Optional[str] = None, navigation_mode: Optional[str] = None, read_only: Optional[bool] = None, - customer_data: Optional[Dict[str, Any]] = None + customer_data: Optional[Dict[str, Any]] = None, ) -> CustomerPortal: """Creates a new customer portal magic link with configurable features. @@ -71,7 +72,7 @@ def delete_data( tenant_keys: Optional[List[str]] = None, unit_keys: Optional[List[str]] = None, user_identity_keys: Optional[List[str]] = None, - user_keys: Optional[List[str]] = None + user_keys: Optional[List[str]] = None, ) -> None: """Deletes customer data including resources like spaces, properties, rooms, users, etc. This will delete the partner resources and any related Seam resources (user identities, access grants, spaces). @@ -138,7 +139,7 @@ def push_data( tenants: Optional[List[Dict[str, Any]]] = None, units: Optional[List[Dict[str, Any]]] = None, user_identities: Optional[List[Dict[str, Any]]] = None, - users: Optional[List[Dict[str, Any]]] = None + users: Optional[List[Dict[str, Any]]] = None, ) -> None: """Pushes customer data including resources like spaces, properties, rooms, users, etc. @@ -180,7 +181,9 @@ def push_data( :param user_identities: List of user identities. - :param users: List of users.""" + :param users: List of users. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -189,6 +192,11 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/customers/create_portal", + has_required_parameters=False, + has_pagination=False, + ) def create_portal( self, *, @@ -202,7 +210,7 @@ def create_portal( locale: Optional[str] = None, navigation_mode: Optional[str] = None, read_only: Optional[bool] = None, - customer_data: Optional[Dict[str, Any]] = None + customer_data: Optional[Dict[str, Any]] = None, ) -> CustomerPortal: """Creates a new customer portal magic link with configurable features. @@ -229,7 +237,7 @@ def create_portal( :param customer_data: :returns: OK""" - json_payload = {} + json_payload: Dict[str, Any] = {} if customer_resources_filters is not None: json_payload["customer_resources_filters"] = customer_resources_filters @@ -258,6 +266,11 @@ def create_portal( return CustomerPortal.from_dict(res["customer_portal"]) + @route_metadata( + path="/customers/delete_data", + has_required_parameters=False, + has_pagination=False, + ) def delete_data( self, *, @@ -279,7 +292,7 @@ def delete_data( tenant_keys: Optional[List[str]] = None, unit_keys: Optional[List[str]] = None, user_identity_keys: Optional[List[str]] = None, - user_keys: Optional[List[str]] = None + user_keys: Optional[List[str]] = None, ) -> None: """Deletes customer data including resources like spaces, properties, rooms, users, etc. This will delete the partner resources and any related Seam resources (user identities, access grants, spaces). @@ -321,7 +334,7 @@ def delete_data( :param user_identity_keys: List of user identity keys to delete. :param user_keys: List of user keys to delete.""" - json_payload = {} + json_payload: Dict[str, Any] = {} if access_grant_keys is not None: json_payload["access_grant_keys"] = access_grant_keys @@ -366,6 +379,9 @@ def delete_data( return None + @route_metadata( + path="/customers/push_data", has_required_parameters=True, has_pagination=False + ) def push_data( self, *, @@ -388,7 +404,7 @@ def push_data( tenants: Optional[List[Dict[str, Any]]] = None, units: Optional[List[Dict[str, Any]]] = None, user_identities: Optional[List[Dict[str, Any]]] = None, - users: Optional[List[Dict[str, Any]]] = None + users: Optional[List[Dict[str, Any]]] = None, ) -> None: """Pushes customer data including resources like spaces, properties, rooms, users, etc. @@ -430,8 +446,10 @@ def push_data( :param user_identities: List of user identities. - :param users: List of users.""" - json_payload = {} + :param users: List of users. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if customer_key is not None: json_payload["customer_key"] = customer_key @@ -474,6 +492,11 @@ def push_data( if users is not None: json_payload["users"] = users + if not json_payload: + raise ValueError( + "At least one parameter is required for /customers/push_data" + ) + self.client.post("/customers/push_data", json=json_payload) return None diff --git a/seam/routes/devices.py b/seam/routes/devices.py index 433a8d40..e1b6dfbb 100644 --- a/seam/routes/devices.py +++ b/seam/routes/devices.py @@ -1,6 +1,8 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +from ..null import Null from ..resources import Device, DeviceProvider from .devices_simulate import AbstractDevicesSimulate, DevicesSimulate from .devices_unmanaged import AbstractDevicesUnmanaged, DevicesUnmanaged @@ -30,7 +32,9 @@ def get( :param name: Name of the device that you want to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -48,11 +52,11 @@ def list( device_types: Optional[List[str]] = None, limit: Optional[float] = None, manufacturer: Optional[str] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, space_id: Optional[str] = None, - unstable_location_id: Optional[str] = None, - user_identifier_key: Optional[str] = None + unstable_location_id: Optional[Union[str, Null]] = None, + user_identifier_key: Optional[str] = None, ) -> List[Device]: """Returns a list of all `devices `_. @@ -110,7 +114,9 @@ def list_device_providers( def report_provider_metadata(self, *, devices: List[Dict[str, Any]]) -> None: """Updates provider-specific metadata for devices. - :param devices: Array of devices with provider metadata to update""" + :param devices: Array of devices with provider metadata to update + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -121,8 +127,8 @@ def update( backup_access_code_pool_enabled: Optional[bool] = None, custom_metadata: Optional[Dict[str, Any]] = None, is_managed: Optional[bool] = None, - name: Optional[str] = None, - properties: Optional[Dict[str, Any]] = None + name: Optional[Union[str, Null]] = None, + properties: Optional[Dict[str, Any]] = None, ) -> None: """Updates a specified `device `_. @@ -138,7 +144,9 @@ def update( :param name: Name for the device. - :param properties:""" + :param properties: + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -157,6 +165,9 @@ def simulate(self) -> DevicesSimulate: def unmanaged(self) -> DevicesUnmanaged: return self._unmanaged + @route_metadata( + path="/devices/get", has_required_parameters=True, has_pagination=False + ) def get( self, *, device_id: Optional[str] = None, name: Optional[str] = None ) -> Device: @@ -168,18 +179,26 @@ def get( :param name: Name of the device that you want to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if name is not None: - json_payload["name"] = name + params["name"] = name + + if not params: + raise ValueError("At least one parameter is required for /devices/get") - res = self.client.post("/devices/get", json=json_payload) + res = self.client.get("/devices/get", params=params) return Device.from_dict(res["device"]) + @route_metadata( + path="/devices/list", has_required_parameters=False, has_pagination=True + ) def list( self, *, @@ -194,11 +213,11 @@ def list( device_types: Optional[List[str]] = None, limit: Optional[float] = None, manufacturer: Optional[str] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, space_id: Optional[str] = None, - unstable_location_id: Optional[str] = None, - user_identifier_key: Optional[str] = None + unstable_location_id: Optional[Union[str, Null]] = None, + user_identifier_key: Optional[str] = None, ) -> List[Device]: """Returns a list of all `devices `_. @@ -235,7 +254,7 @@ def list( :param user_identifier_key: Your own internal user ID for the user for which you want to list devices. :returns: OK""" - json_payload = {} + json_payload: Dict[str, Any] = {} if connect_webview_id is not None: json_payload["connect_webview_id"] = connect_webview_id @@ -274,6 +293,11 @@ def list( return [Device.from_dict(item) for item in res["devices"]] + @route_metadata( + path="/devices/list_device_providers", + has_required_parameters=False, + has_pagination=False, + ) def list_device_providers( self, *, provider_category: Optional[str] = None ) -> List[DeviceProvider]: @@ -286,28 +310,43 @@ def list_device_providers( :param provider_category: Category for which you want to list providers. :returns: OK""" - json_payload = {} + params: Dict[str, Any] = {} if provider_category is not None: - json_payload["provider_category"] = provider_category + params["provider_category"] = provider_category - res = self.client.post("/devices/list_device_providers", json=json_payload) + res = self.client.get("/devices/list_device_providers", params=params) return [DeviceProvider.from_dict(item) for item in res["device_providers"]] + @route_metadata( + path="/devices/report_provider_metadata", + has_required_parameters=True, + has_pagination=False, + ) def report_provider_metadata(self, *, devices: List[Dict[str, Any]]) -> None: """Updates provider-specific metadata for devices. - :param devices: Array of devices with provider metadata to update""" - json_payload = {} + :param devices: Array of devices with provider metadata to update + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if devices is not None: json_payload["devices"] = devices + if not json_payload: + raise ValueError( + "At least one parameter is required for /devices/report_provider_metadata" + ) + self.client.post("/devices/report_provider_metadata", json=json_payload) return None + @route_metadata( + path="/devices/update", has_required_parameters=True, has_pagination=False + ) def update( self, *, @@ -315,8 +354,8 @@ def update( backup_access_code_pool_enabled: Optional[bool] = None, custom_metadata: Optional[Dict[str, Any]] = None, is_managed: Optional[bool] = None, - name: Optional[str] = None, - properties: Optional[Dict[str, Any]] = None + name: Optional[Union[str, Null]] = None, + properties: Optional[Dict[str, Any]] = None, ) -> None: """Updates a specified `device `_. @@ -332,8 +371,10 @@ def update( :param name: Name for the device. - :param properties:""" - json_payload = {} + :param properties: + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id @@ -350,6 +391,9 @@ def update( if properties is not None: json_payload["properties"] = properties - self.client.post("/devices/update", json=json_payload) + if not json_payload: + raise ValueError("At least one parameter is required for /devices/update") + + self.client.patch("/devices/update", json=json_payload) return None diff --git a/seam/routes/devices_simulate.py b/seam/routes/devices_simulate.py index 2ebaa239..dfdfdfed 100644 --- a/seam/routes/devices_simulate.py +++ b/seam/routes/devices_simulate.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata class AbstractDevicesSimulate(abc.ABC): @@ -10,7 +11,8 @@ def connect(self, *, device_id: str) -> None: """Simulates connecting a device to Seam. Only applicable for `sandbox devices `_. See also `Testing Your App Against Device Disconnection and Removal `_. :param device_id: ID of the device that you want to simulate connecting to Seam. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -20,7 +22,9 @@ def connect_to_hub(self, *, device_id: str) -> None: implemented for August and TTLock locks. This will clear the ``hub_disconnected`` error on the device. - :param device_id: ID of the device whose hub you want to reconnect.""" + :param device_id: ID of the device whose hub you want to reconnect. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -28,7 +32,8 @@ def disconnect(self, *, device_id: str) -> None: """Simulates disconnecting a device from Seam. Only applicable for `sandbox devices `_. See also `Testing Your App Against Device Disconnection and Removal `_. :param device_id: ID of the device that you want to simulate disconnecting from Seam. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -39,7 +44,9 @@ def disconnect_from_hub(self, *, device_id: str) -> None: This will set the ``hub_disconnected`` error on the device, or mark the IglooHome bridge offline in sandbox. - :param device_id: ID of the device whose hub you want to disconnect.""" + :param device_id: ID of the device whose hub you want to disconnect. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -50,7 +57,9 @@ def paid_subscription(self, *, device_id: str, is_expired: bool) -> None: :param device_id: - :param is_expired:""" + :param is_expired: + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -58,7 +67,8 @@ def remove(self, *, device_id: str) -> None: """Simulates removing a device from Seam. Only applicable for `sandbox devices `_. See also `Testing Your App Against Device Disconnection and Removal `_. :param device_id: ID of the device that you want to simulate removing from Seam. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -67,50 +77,89 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/devices/simulate/connect", + has_required_parameters=True, + has_pagination=False, + ) def connect(self, *, device_id: str) -> None: """Simulates connecting a device to Seam. Only applicable for `sandbox devices `_. See also `Testing Your App Against Device Disconnection and Removal `_. :param device_id: ID of the device that you want to simulate connecting to Seam. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /devices/simulate/connect" + ) + self.client.post("/devices/simulate/connect", json=json_payload) return None + @route_metadata( + path="/devices/simulate/connect_to_hub", + has_required_parameters=True, + has_pagination=False, + ) def connect_to_hub(self, *, device_id: str) -> None: """Simulates bringing the Wi‑Fi hub (bridge) back online for a device. Only applicable for sandbox workspaces and currently implemented for August and TTLock locks. This will clear the ``hub_disconnected`` error on the device. - :param device_id: ID of the device whose hub you want to reconnect.""" - json_payload = {} + :param device_id: ID of the device whose hub you want to reconnect. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /devices/simulate/connect_to_hub" + ) + self.client.post("/devices/simulate/connect_to_hub", json=json_payload) return None + @route_metadata( + path="/devices/simulate/disconnect", + has_required_parameters=True, + has_pagination=False, + ) def disconnect(self, *, device_id: str) -> None: """Simulates disconnecting a device from Seam. Only applicable for `sandbox devices `_. See also `Testing Your App Against Device Disconnection and Removal `_. :param device_id: ID of the device that you want to simulate disconnecting from Seam. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /devices/simulate/disconnect" + ) + self.client.post("/devices/simulate/disconnect", json=json_payload) return None + @route_metadata( + path="/devices/simulate/disconnect_from_hub", + has_required_parameters=True, + has_pagination=False, + ) def disconnect_from_hub(self, *, device_id: str) -> None: """Simulates taking the Wi‑Fi hub (bridge) offline for a device. Only applicable for sandbox workspaces and currently @@ -118,16 +167,28 @@ def disconnect_from_hub(self, *, device_id: str) -> None: This will set the ``hub_disconnected`` error on the device, or mark the IglooHome bridge offline in sandbox. - :param device_id: ID of the device whose hub you want to disconnect.""" - json_payload = {} + :param device_id: ID of the device whose hub you want to disconnect. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /devices/simulate/disconnect_from_hub" + ) + self.client.post("/devices/simulate/disconnect_from_hub", json=json_payload) return None + @route_metadata( + path="/devices/simulate/paid_subscription", + has_required_parameters=True, + has_pagination=False, + ) def paid_subscription(self, *, device_id: str, is_expired: bool) -> None: """Toggle the simulated Nuki Smart Hosting subscription for a device (sandbox only). Send ``is_expired: true`` to simulate an expired subscription, or ``false`` to simulate an active subscription. @@ -135,28 +196,46 @@ def paid_subscription(self, *, device_id: str, is_expired: bool) -> None: :param device_id: - :param is_expired:""" - json_payload = {} + :param is_expired: + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id if is_expired is not None: json_payload["is_expired"] = is_expired + if not json_payload: + raise ValueError( + "At least one parameter is required for /devices/simulate/paid_subscription" + ) + self.client.post("/devices/simulate/paid_subscription", json=json_payload) return None + @route_metadata( + path="/devices/simulate/remove", + has_required_parameters=True, + has_pagination=False, + ) def remove(self, *, device_id: str) -> None: """Simulates removing a device from Seam. Only applicable for `sandbox devices `_. See also `Testing Your App Against Device Disconnection and Removal `_. :param device_id: ID of the device that you want to simulate removing from Seam. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /devices/simulate/remove" + ) + self.client.post("/devices/simulate/remove", json=json_payload) return None diff --git a/seam/routes/devices_unmanaged.py b/seam/routes/devices_unmanaged.py index 9c0e865f..12987d60 100644 --- a/seam/routes/devices_unmanaged.py +++ b/seam/routes/devices_unmanaged.py @@ -1,6 +1,8 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +from ..null import Null from ..resources import UnmanagedDevice @@ -20,7 +22,9 @@ def get( :param name: Name of the unmanaged device that you want to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -31,18 +35,14 @@ def list( connected_account_id: Optional[str] = None, connected_account_ids: Optional[List[str]] = None, created_before: Optional[str] = None, - custom_metadata_has: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, device_ids: Optional[List[str]] = None, device_type: Optional[str] = None, device_types: Optional[List[str]] = None, limit: Optional[float] = None, manufacturer: Optional[str] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, - space_id: Optional[str] = None, - unstable_location_id: Optional[str] = None, - user_identifier_key: Optional[str] = None ) -> List[UnmanagedDevice]: """Returns a list of all `unmanaged devices `_. @@ -56,8 +56,6 @@ def list( :param created_before: Timestamp by which to limit returned devices. Returns devices created before this timestamp. - :param custom_metadata_has: Set of key:value `custom metadata `_ pairs for which you want to list devices. - :param customer_key: Customer key for which you want to list devices. :param device_ids: Array of device IDs for which you want to list devices. @@ -74,12 +72,6 @@ def list( :param search: String for which to search. Filters returned devices to include all records that satisfy a partial match using ``device_id`` (full or partial UUID prefix, minimum 4 characters), ``connected_account_id``, ``display_name``, ``custom_metadata`` or ``location.location_name``. - :param space_id: ID of the space for which you want to list devices. - - :param unstable_location_id: Deprecated: Use ``space_id``. - - :param user_identifier_key: Your own internal user ID for the user for which you want to list devices. - :returns: OK""" raise NotImplementedError() @@ -89,7 +81,7 @@ def update( *, device_id: str, custom_metadata: Optional[Dict[str, Any]] = None, - is_managed: Optional[bool] = None + is_managed: Optional[bool] = None, ) -> None: """Updates a specified `unmanaged device `_. To convert an unmanaged device to managed, set ``is_managed`` to ``true``. @@ -100,7 +92,8 @@ def update( :param custom_metadata: Custom metadata that you want to associate with the device. Supports up to 50 JSON key:value pairs. :param is_managed: Indicates whether the device is managed. Set this parameter to ``true`` to convert an unmanaged device to managed. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -109,6 +102,11 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/devices/unmanaged/get", + has_required_parameters=True, + has_pagination=False, + ) def get( self, *, device_id: Optional[str] = None, name: Optional[str] = None ) -> UnmanagedDevice: @@ -122,18 +120,30 @@ def get( :param name: Name of the unmanaged device that you want to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if name is not None: - json_payload["name"] = name + params["name"] = name + + if not params: + raise ValueError( + "At least one parameter is required for /devices/unmanaged/get" + ) - res = self.client.post("/devices/unmanaged/get", json=json_payload) + res = self.client.get("/devices/unmanaged/get", params=params) return UnmanagedDevice.from_dict(res["device"]) + @route_metadata( + path="/devices/unmanaged/list", + has_required_parameters=False, + has_pagination=True, + ) def list( self, *, @@ -141,18 +151,14 @@ def list( connected_account_id: Optional[str] = None, connected_account_ids: Optional[List[str]] = None, created_before: Optional[str] = None, - custom_metadata_has: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, device_ids: Optional[List[str]] = None, device_type: Optional[str] = None, device_types: Optional[List[str]] = None, limit: Optional[float] = None, manufacturer: Optional[str] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, - space_id: Optional[str] = None, - unstable_location_id: Optional[str] = None, - user_identifier_key: Optional[str] = None ) -> List[UnmanagedDevice]: """Returns a list of all `unmanaged devices `_. @@ -166,8 +172,6 @@ def list( :param created_before: Timestamp by which to limit returned devices. Returns devices created before this timestamp. - :param custom_metadata_has: Set of key:value `custom metadata `_ pairs for which you want to list devices. - :param customer_key: Customer key for which you want to list devices. :param device_ids: Array of device IDs for which you want to list devices. @@ -184,14 +188,8 @@ def list( :param search: String for which to search. Filters returned devices to include all records that satisfy a partial match using ``device_id`` (full or partial UUID prefix, minimum 4 characters), ``connected_account_id``, ``display_name``, ``custom_metadata`` or ``location.location_name``. - :param space_id: ID of the space for which you want to list devices. - - :param unstable_location_id: Deprecated: Use ``space_id``. - - :param user_identifier_key: Your own internal user ID for the user for which you want to list devices. - :returns: OK""" - json_payload = {} + json_payload: Dict[str, Any] = {} if connect_webview_id is not None: json_payload["connect_webview_id"] = connect_webview_id @@ -201,8 +199,6 @@ def list( json_payload["connected_account_ids"] = connected_account_ids if created_before is not None: json_payload["created_before"] = created_before - if custom_metadata_has is not None: - json_payload["custom_metadata_has"] = custom_metadata_has if customer_key is not None: json_payload["customer_key"] = customer_key if device_ids is not None: @@ -219,23 +215,22 @@ def list( json_payload["page_cursor"] = page_cursor if search is not None: json_payload["search"] = search - if space_id is not None: - json_payload["space_id"] = space_id - if unstable_location_id is not None: - json_payload["unstable_location_id"] = unstable_location_id - if user_identifier_key is not None: - json_payload["user_identifier_key"] = user_identifier_key res = self.client.post("/devices/unmanaged/list", json=json_payload) return [UnmanagedDevice.from_dict(item) for item in res["devices"]] + @route_metadata( + path="/devices/unmanaged/update", + has_required_parameters=True, + has_pagination=False, + ) def update( self, *, device_id: str, custom_metadata: Optional[Dict[str, Any]] = None, - is_managed: Optional[bool] = None + is_managed: Optional[bool] = None, ) -> None: """Updates a specified `unmanaged device `_. To convert an unmanaged device to managed, set ``is_managed`` to ``true``. @@ -246,8 +241,9 @@ def update( :param custom_metadata: Custom metadata that you want to associate with the device. Supports up to 50 JSON key:value pairs. :param is_managed: Indicates whether the device is managed. Set this parameter to ``true`` to convert an unmanaged device to managed. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id @@ -256,6 +252,11 @@ def update( if is_managed is not None: json_payload["is_managed"] = is_managed - self.client.post("/devices/unmanaged/update", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /devices/unmanaged/update" + ) + + self.client.patch("/devices/unmanaged/update", json=json_payload) return None diff --git a/seam/routes/events.py b/seam/routes/events.py index 30ce08e9..8613b639 100644 --- a/seam/routes/events.py +++ b/seam/routes/events.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata from ..resources import SeamEvent @@ -12,7 +13,7 @@ def get( *, event_id: Optional[str] = None, device_id: Optional[str] = None, - event_type: Optional[str] = None + event_type: Optional[str] = None, ) -> SeamEvent: """Returns a specified event. This endpoint returns the same event that would be sent to a `webhook `_, but it enables you to retrieve an event that already took place. @@ -22,7 +23,9 @@ def get( :param event_type: Type of the event that you want to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -42,7 +45,7 @@ def list( acs_system_id: Optional[str] = None, acs_system_ids: Optional[List[str]] = None, acs_user_id: Optional[str] = None, - between: Optional[List[Dict[str, Any]]] = None, + between: Optional[List[str]] = None, connect_webview_id: Optional[str] = None, connected_account_id: Optional[str] = None, customer_key: Optional[str] = None, @@ -56,7 +59,7 @@ def list( space_id: Optional[str] = None, space_ids: Optional[List[str]] = None, unstable_offset: Optional[float] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> List[SeamEvent]: """Returns a list of all events. This endpoint returns the same events that would be sent to a `webhook `_, but it enables you to filter or see events that already took place. @@ -116,7 +119,9 @@ def list( :param user_identity_id: ID of the user identity for which you want to list events. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -125,12 +130,15 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/events/get", has_required_parameters=True, has_pagination=False + ) def get( self, *, event_id: Optional[str] = None, device_id: Optional[str] = None, - event_type: Optional[str] = None + event_type: Optional[str] = None, ) -> SeamEvent: """Returns a specified event. This endpoint returns the same event that would be sent to a `webhook `_, but it enables you to retrieve an event that already took place. @@ -140,20 +148,28 @@ def get( :param event_type: Type of the event that you want to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if event_id is not None: - json_payload["event_id"] = event_id + params["event_id"] = event_id if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if event_type is not None: - json_payload["event_type"] = event_type + params["event_type"] = event_type + + if not params: + raise ValueError("At least one parameter is required for /events/get") - res = self.client.post("/events/get", json=json_payload) + res = self.client.get("/events/get", params=params) return SeamEvent.from_dict(res["event"]) + @route_metadata( + path="/events/list", has_required_parameters=True, has_pagination=False + ) def list( self, *, @@ -170,7 +186,7 @@ def list( acs_system_id: Optional[str] = None, acs_system_ids: Optional[List[str]] = None, acs_user_id: Optional[str] = None, - between: Optional[List[Dict[str, Any]]] = None, + between: Optional[List[str]] = None, connect_webview_id: Optional[str] = None, connected_account_id: Optional[str] = None, customer_key: Optional[str] = None, @@ -184,7 +200,7 @@ def list( space_id: Optional[str] = None, space_ids: Optional[List[str]] = None, unstable_offset: Optional[float] = None, - user_identity_id: Optional[str] = None + user_identity_id: Optional[str] = None, ) -> List[SeamEvent]: """Returns a list of all events. This endpoint returns the same events that would be sent to a `webhook `_, but it enables you to filter or see events that already took place. @@ -244,8 +260,10 @@ def list( :param user_identity_id: ID of the user identity for which you want to list events. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if access_code_id is not None: json_payload["access_code_id"] = access_code_id @@ -304,6 +322,9 @@ def list( if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id + if not json_payload: + raise ValueError("At least one parameter is required for /events/list") + res = self.client.post("/events/list", json=json_payload) return [SeamEvent.from_dict(item) for item in res["events"]] diff --git a/seam/routes/instant_keys.py b/seam/routes/instant_keys.py index b1c3bf23..48787987 100644 --- a/seam/routes/instant_keys.py +++ b/seam/routes/instant_keys.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata from ..resources import InstantKey @@ -10,7 +11,9 @@ class AbstractInstantKeys(abc.ABC): def delete(self, *, instant_key_id: str) -> None: """Deletes a specified `Instant Key `_. - :param instant_key_id: ID of the Instant Key that you want to delete.""" + :param instant_key_id: ID of the Instant Key that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -18,7 +21,7 @@ def get( self, *, instant_key_id: Optional[str] = None, - instant_key_url: Optional[str] = None + instant_key_url: Optional[str] = None, ) -> InstantKey: """Gets an `instant key `_. @@ -26,7 +29,9 @@ def get( :param instant_key_url: URL of the instant key to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -44,24 +49,37 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/instant_keys/delete", has_required_parameters=True, has_pagination=False + ) def delete(self, *, instant_key_id: str) -> None: """Deletes a specified `Instant Key `_. - :param instant_key_id: ID of the Instant Key that you want to delete.""" - json_payload = {} + :param instant_key_id: ID of the Instant Key that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if instant_key_id is not None: - json_payload["instant_key_id"] = instant_key_id + params["instant_key_id"] = instant_key_id - self.client.post("/instant_keys/delete", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /instant_keys/delete" + ) + + self.client.delete("/instant_keys/delete", params=params) return None + @route_metadata( + path="/instant_keys/get", has_required_parameters=True, has_pagination=False + ) def get( self, *, instant_key_id: Optional[str] = None, - instant_key_url: Optional[str] = None + instant_key_url: Optional[str] = None, ) -> InstantKey: """Gets an `instant key `_. @@ -69,29 +87,37 @@ def get( :param instant_key_url: URL of the instant key to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if instant_key_id is not None: - json_payload["instant_key_id"] = instant_key_id + params["instant_key_id"] = instant_key_id if instant_key_url is not None: - json_payload["instant_key_url"] = instant_key_url + params["instant_key_url"] = instant_key_url + + if not params: + raise ValueError("At least one parameter is required for /instant_keys/get") - res = self.client.post("/instant_keys/get", json=json_payload) + res = self.client.get("/instant_keys/get", params=params) return InstantKey.from_dict(res["instant_key"]) + @route_metadata( + path="/instant_keys/list", has_required_parameters=False, has_pagination=False + ) def list(self, *, user_identity_id: Optional[str] = None) -> List[InstantKey]: """Returns a list of all `instant keys `_. :param user_identity_id: ID of the user identity by which you want to filter the list of Instant Keys. :returns: OK""" - json_payload = {} + params: Dict[str, Any] = {} if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id - res = self.client.post("/instant_keys/list", json=json_payload) + res = self.client.get("/instant_keys/list", params=params) return [InstantKey.from_dict(item) for item in res["instant_keys"]] diff --git a/seam/routes/locks.py b/seam/routes/locks.py index 23a41b0d..fdf7843f 100644 --- a/seam/routes/locks.py +++ b/seam/routes/locks.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata from ..resources import ActionAttempt, Device from .locks_simulate import AbstractLocksSimulate, LocksSimulate from ..modules.action_attempts import resolve_action_attempt @@ -20,7 +21,7 @@ def configure_auto_lock( auto_lock_enabled: bool, device_id: str, auto_lock_delay_seconds: Optional[float] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Configures the auto-lock setting for a specified `lock `_. @@ -32,7 +33,9 @@ def configure_auto_lock( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -47,6 +50,8 @@ def get( :returns: OK + :raises ValueError: At least one parameter must be provided. + .. deprecated:: Use ``/devices/get`` instead.""" raise NotImplementedError() @@ -57,20 +62,10 @@ def list( *, connect_webview_id: Optional[str] = None, connected_account_id: Optional[str] = None, - connected_account_ids: Optional[List[str]] = None, - created_before: Optional[str] = None, - custom_metadata_has: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, - device_ids: Optional[List[str]] = None, device_type: Optional[str] = None, device_types: Optional[List[str]] = None, - limit: Optional[float] = None, manufacturer: Optional[str] = None, - page_cursor: Optional[str] = None, - search: Optional[str] = None, - space_id: Optional[str] = None, - unstable_location_id: Optional[str] = None, - user_identifier_key: Optional[str] = None ) -> List[Device]: """Returns a list of all `locks `_. @@ -78,34 +73,14 @@ def list( :param connected_account_id: ID of the connected account for which you want to list devices. - :param connected_account_ids: Array of IDs of the connected accounts for which you want to list devices. - - :param created_before: Timestamp by which to limit returned devices. Returns devices created before this timestamp. - - :param custom_metadata_has: Set of key:value `custom metadata `_ pairs for which you want to list devices. - :param customer_key: Customer key for which you want to list devices. - :param device_ids: Array of device IDs for which you want to list devices. - :param device_type: Device type of the locks that you want to list. :param device_types: Device types of the locks that you want to list. - :param limit: Numerical limit on the number of devices to return. - :param manufacturer: Manufacturer of the locks that you want to list. - :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. - - :param search: String for which to search. Filters returned devices to include all records that satisfy a partial match using ``device_id`` (full or partial UUID prefix, minimum 4 characters), ``connected_account_id``, ``display_name``, ``custom_metadata`` or ``location.location_name``. - - :param space_id: ID of the space for which you want to list devices. - - :param unstable_location_id: Deprecated: Use ``space_id``. - - :param user_identifier_key: Your own internal user ID for the user for which you want to list devices. - :returns: OK""" raise NotImplementedError() @@ -114,7 +89,7 @@ def lock_door( self, *, device_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Locks a `lock `_. See also `Locking and Unlocking Smart Locks `_. @@ -122,7 +97,9 @@ def lock_door( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -130,7 +107,7 @@ def unlock_door( self, *, device_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Unlocks a `lock `_. See also `Locking and Unlocking Smart Locks `_. @@ -138,7 +115,9 @@ def unlock_door( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -152,13 +131,18 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): def simulate(self) -> LocksSimulate: return self._simulate + @route_metadata( + path="/locks/configure_auto_lock", + has_required_parameters=True, + has_pagination=False, + ) def configure_auto_lock( self, *, auto_lock_enabled: bool, device_id: str, auto_lock_delay_seconds: Optional[float] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Configures the auto-lock setting for a specified `lock `_. @@ -170,8 +154,10 @@ def configure_auto_lock( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if auto_lock_enabled is not None: json_payload["auto_lock_enabled"] = auto_lock_enabled @@ -180,6 +166,11 @@ def configure_auto_lock( if auto_lock_delay_seconds is not None: json_payload["auto_lock_delay_seconds"] = auto_lock_delay_seconds + if not json_payload: + raise ValueError( + "At least one parameter is required for /locks/configure_auto_lock" + ) + res = self.client.post("/locks/configure_auto_lock", json=json_payload) wait_for_action_attempt = ( @@ -194,6 +185,9 @@ def configure_auto_lock( wait_for_action_attempt=wait_for_action_attempt, ) + @route_metadata( + path="/locks/get", has_required_parameters=True, has_pagination=False + ) def get( self, *, device_id: Optional[str] = None, name: Optional[str] = None ) -> Device: @@ -205,38 +199,36 @@ def get( :returns: OK + :raises ValueError: At least one parameter must be provided. + .. deprecated:: Use ``/devices/get`` instead.""" - json_payload = {} + params: Dict[str, Any] = {} if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if name is not None: - json_payload["name"] = name + params["name"] = name - res = self.client.post("/locks/get", json=json_payload) + if not params: + raise ValueError("At least one parameter is required for /locks/get") + + res = self.client.get("/locks/get", params=params) return Device.from_dict(res["device"]) + @route_metadata( + path="/locks/list", has_required_parameters=False, has_pagination=False + ) def list( self, *, connect_webview_id: Optional[str] = None, connected_account_id: Optional[str] = None, - connected_account_ids: Optional[List[str]] = None, - created_before: Optional[str] = None, - custom_metadata_has: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, - device_ids: Optional[List[str]] = None, device_type: Optional[str] = None, device_types: Optional[List[str]] = None, - limit: Optional[float] = None, manufacturer: Optional[str] = None, - page_cursor: Optional[str] = None, - search: Optional[str] = None, - space_id: Optional[str] = None, - unstable_location_id: Optional[str] = None, - user_identifier_key: Optional[str] = None ) -> List[Device]: """Returns a list of all `locks `_. @@ -244,79 +236,42 @@ def list( :param connected_account_id: ID of the connected account for which you want to list devices. - :param connected_account_ids: Array of IDs of the connected accounts for which you want to list devices. - - :param created_before: Timestamp by which to limit returned devices. Returns devices created before this timestamp. - - :param custom_metadata_has: Set of key:value `custom metadata `_ pairs for which you want to list devices. - :param customer_key: Customer key for which you want to list devices. - :param device_ids: Array of device IDs for which you want to list devices. - :param device_type: Device type of the locks that you want to list. :param device_types: Device types of the locks that you want to list. - :param limit: Numerical limit on the number of devices to return. - :param manufacturer: Manufacturer of the locks that you want to list. - :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. - - :param search: String for which to search. Filters returned devices to include all records that satisfy a partial match using ``device_id`` (full or partial UUID prefix, minimum 4 characters), ``connected_account_id``, ``display_name``, ``custom_metadata`` or ``location.location_name``. - - :param space_id: ID of the space for which you want to list devices. - - :param unstable_location_id: Deprecated: Use ``space_id``. - - :param user_identifier_key: Your own internal user ID for the user for which you want to list devices. - :returns: OK""" - json_payload = {} + json_payload: Dict[str, Any] = {} if connect_webview_id is not None: json_payload["connect_webview_id"] = connect_webview_id if connected_account_id is not None: json_payload["connected_account_id"] = connected_account_id - if connected_account_ids is not None: - json_payload["connected_account_ids"] = connected_account_ids - if created_before is not None: - json_payload["created_before"] = created_before - if custom_metadata_has is not None: - json_payload["custom_metadata_has"] = custom_metadata_has if customer_key is not None: json_payload["customer_key"] = customer_key - if device_ids is not None: - json_payload["device_ids"] = device_ids if device_type is not None: json_payload["device_type"] = device_type if device_types is not None: json_payload["device_types"] = device_types - if limit is not None: - json_payload["limit"] = limit if manufacturer is not None: json_payload["manufacturer"] = manufacturer - if page_cursor is not None: - json_payload["page_cursor"] = page_cursor - if search is not None: - json_payload["search"] = search - if space_id is not None: - json_payload["space_id"] = space_id - if unstable_location_id is not None: - json_payload["unstable_location_id"] = unstable_location_id - if user_identifier_key is not None: - json_payload["user_identifier_key"] = user_identifier_key res = self.client.post("/locks/list", json=json_payload) return [Device.from_dict(item) for item in res["devices"]] + @route_metadata( + path="/locks/lock_door", has_required_parameters=True, has_pagination=False + ) def lock_door( self, *, device_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Locks a `lock `_. See also `Locking and Unlocking Smart Locks `_. @@ -324,12 +279,17 @@ def lock_door( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id + if not json_payload: + raise ValueError("At least one parameter is required for /locks/lock_door") + res = self.client.post("/locks/lock_door", json=json_payload) wait_for_action_attempt = ( @@ -344,11 +304,14 @@ def lock_door( wait_for_action_attempt=wait_for_action_attempt, ) + @route_metadata( + path="/locks/unlock_door", has_required_parameters=True, has_pagination=False + ) def unlock_door( self, *, device_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Unlocks a `lock `_. See also `Locking and Unlocking Smart Locks `_. @@ -356,12 +319,19 @@ def unlock_door( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /locks/unlock_door" + ) + res = self.client.post("/locks/unlock_door", json=json_payload) wait_for_action_attempt = ( diff --git a/seam/routes/locks_simulate.py b/seam/routes/locks_simulate.py index 937cb37d..6979c9c2 100644 --- a/seam/routes/locks_simulate.py +++ b/seam/routes/locks_simulate.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata from ..resources import ActionAttempt from ..modules.action_attempts import resolve_action_attempt @@ -13,7 +14,7 @@ def keypad_code_entry( *, code: str, device_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Simulates the entry of a code on a keypad. You can only perform this action for `August `_ devices within `sandbox workspaces `_. @@ -23,7 +24,9 @@ def keypad_code_entry( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -31,7 +34,7 @@ def manual_lock_via_keypad( self, *, device_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Simulates a manual lock action using a keypad. You can only perform this action for `August `_ devices within `sandbox workspaces `_. @@ -39,7 +42,9 @@ def manual_lock_via_keypad( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -48,12 +53,17 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/locks/simulate/keypad_code_entry", + has_required_parameters=True, + has_pagination=False, + ) def keypad_code_entry( self, *, code: str, device_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Simulates the entry of a code on a keypad. You can only perform this action for `August `_ devices within `sandbox workspaces `_. @@ -63,14 +73,21 @@ def keypad_code_entry( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if code is not None: json_payload["code"] = code if device_id is not None: json_payload["device_id"] = device_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /locks/simulate/keypad_code_entry" + ) + res = self.client.post("/locks/simulate/keypad_code_entry", json=json_payload) wait_for_action_attempt = ( @@ -85,11 +102,16 @@ def keypad_code_entry( wait_for_action_attempt=wait_for_action_attempt, ) + @route_metadata( + path="/locks/simulate/manual_lock_via_keypad", + has_required_parameters=True, + has_pagination=False, + ) def manual_lock_via_keypad( self, *, device_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Simulates a manual lock action using a keypad. You can only perform this action for `August `_ devices within `sandbox workspaces `_. @@ -97,12 +119,19 @@ def manual_lock_via_keypad( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /locks/simulate/manual_lock_via_keypad" + ) + res = self.client.post( "/locks/simulate/manual_lock_via_keypad", json=json_payload ) diff --git a/seam/routes/noise_sensors.py b/seam/routes/noise_sensors.py index efd90f41..6ab4492b 100644 --- a/seam/routes/noise_sensors.py +++ b/seam/routes/noise_sensors.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata from ..resources import Device from .noise_sensors_noise_thresholds import ( AbstractNoiseSensorsNoiseThresholds, @@ -27,20 +28,10 @@ def list( *, connect_webview_id: Optional[str] = None, connected_account_id: Optional[str] = None, - connected_account_ids: Optional[List[str]] = None, - created_before: Optional[str] = None, - custom_metadata_has: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, - device_ids: Optional[List[str]] = None, device_type: Optional[str] = None, device_types: Optional[List[str]] = None, - limit: Optional[float] = None, manufacturer: Optional[str] = None, - page_cursor: Optional[str] = None, - search: Optional[str] = None, - space_id: Optional[str] = None, - unstable_location_id: Optional[str] = None, - user_identifier_key: Optional[str] = None ) -> List[Device]: """Returns a list of all `noise sensors `_. @@ -48,34 +39,14 @@ def list( :param connected_account_id: ID of the connected account for which you want to list devices. - :param connected_account_ids: Array of IDs of the connected accounts for which you want to list devices. - - :param created_before: Timestamp by which to limit returned devices. Returns devices created before this timestamp. - - :param custom_metadata_has: Set of key:value `custom metadata `_ pairs for which you want to list devices. - :param customer_key: Customer key for which you want to list devices. - :param device_ids: Array of device IDs for which you want to list devices. - :param device_type: Device type of the noise sensors that you want to list. :param device_types: Device types of the noise sensors that you want to list. - :param limit: Numerical limit on the number of devices to return. - :param manufacturer: Manufacturers of the noise sensors that you want to list. - :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. - - :param search: String for which to search. Filters returned devices to include all records that satisfy a partial match using ``device_id`` (full or partial UUID prefix, minimum 4 characters), ``connected_account_id``, ``display_name``, ``custom_metadata`` or ``location.location_name``. - - :param space_id: ID of the space for which you want to list devices. - - :param unstable_location_id: Deprecated: Use ``space_id``. - - :param user_identifier_key: Your own internal user ID for the user for which you want to list devices. - :returns: OK""" raise NotImplementedError() @@ -97,25 +68,18 @@ def noise_thresholds(self) -> NoiseSensorsNoiseThresholds: def simulate(self) -> NoiseSensorsSimulate: return self._simulate + @route_metadata( + path="/noise_sensors/list", has_required_parameters=False, has_pagination=False + ) def list( self, *, connect_webview_id: Optional[str] = None, connected_account_id: Optional[str] = None, - connected_account_ids: Optional[List[str]] = None, - created_before: Optional[str] = None, - custom_metadata_has: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, - device_ids: Optional[List[str]] = None, device_type: Optional[str] = None, device_types: Optional[List[str]] = None, - limit: Optional[float] = None, manufacturer: Optional[str] = None, - page_cursor: Optional[str] = None, - search: Optional[str] = None, - space_id: Optional[str] = None, - unstable_location_id: Optional[str] = None, - user_identifier_key: Optional[str] = None ) -> List[Device]: """Returns a list of all `noise sensors `_. @@ -123,69 +87,29 @@ def list( :param connected_account_id: ID of the connected account for which you want to list devices. - :param connected_account_ids: Array of IDs of the connected accounts for which you want to list devices. - - :param created_before: Timestamp by which to limit returned devices. Returns devices created before this timestamp. - - :param custom_metadata_has: Set of key:value `custom metadata `_ pairs for which you want to list devices. - :param customer_key: Customer key for which you want to list devices. - :param device_ids: Array of device IDs for which you want to list devices. - :param device_type: Device type of the noise sensors that you want to list. :param device_types: Device types of the noise sensors that you want to list. - :param limit: Numerical limit on the number of devices to return. - :param manufacturer: Manufacturers of the noise sensors that you want to list. - :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. - - :param search: String for which to search. Filters returned devices to include all records that satisfy a partial match using ``device_id`` (full or partial UUID prefix, minimum 4 characters), ``connected_account_id``, ``display_name``, ``custom_metadata`` or ``location.location_name``. - - :param space_id: ID of the space for which you want to list devices. - - :param unstable_location_id: Deprecated: Use ``space_id``. - - :param user_identifier_key: Your own internal user ID for the user for which you want to list devices. - :returns: OK""" - json_payload = {} + json_payload: Dict[str, Any] = {} if connect_webview_id is not None: json_payload["connect_webview_id"] = connect_webview_id if connected_account_id is not None: json_payload["connected_account_id"] = connected_account_id - if connected_account_ids is not None: - json_payload["connected_account_ids"] = connected_account_ids - if created_before is not None: - json_payload["created_before"] = created_before - if custom_metadata_has is not None: - json_payload["custom_metadata_has"] = custom_metadata_has if customer_key is not None: json_payload["customer_key"] = customer_key - if device_ids is not None: - json_payload["device_ids"] = device_ids if device_type is not None: json_payload["device_type"] = device_type if device_types is not None: json_payload["device_types"] = device_types - if limit is not None: - json_payload["limit"] = limit if manufacturer is not None: json_payload["manufacturer"] = manufacturer - if page_cursor is not None: - json_payload["page_cursor"] = page_cursor - if search is not None: - json_payload["search"] = search - if space_id is not None: - json_payload["space_id"] = space_id - if unstable_location_id is not None: - json_payload["unstable_location_id"] = unstable_location_id - if user_identifier_key is not None: - json_payload["user_identifier_key"] = user_identifier_key res = self.client.post("/noise_sensors/list", json=json_payload) diff --git a/seam/routes/noise_sensors_noise_thresholds.py b/seam/routes/noise_sensors_noise_thresholds.py index 8c5c9f02..54c90ce8 100644 --- a/seam/routes/noise_sensors_noise_thresholds.py +++ b/seam/routes/noise_sensors_noise_thresholds.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata from ..resources import NoiseThreshold @@ -15,7 +16,7 @@ def create( starts_daily_at: str, name: Optional[str] = None, noise_threshold_decibels: Optional[float] = None, - noise_threshold_nrs: Optional[float] = None + noise_threshold_nrs: Optional[float] = None, ) -> NoiseThreshold: """Creates a new `noise threshold `_ for a `noise sensor `_. Thresholds represent the limits of noise tolerated at a property, which can be customized for each hour of the day. Each device has its own default thresholds, but you can use the Seam API to modify them. @@ -31,7 +32,9 @@ def create( :param noise_threshold_nrs: Noise level in Noiseaware Noise Risk Score (NRS) for the new noise threshold. This parameter is only relevant for `Noiseaware sensors `_. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -40,7 +43,9 @@ def delete(self, *, device_id: str, noise_threshold_id: str) -> None: :param device_id: ID of the device that contains the noise threshold that you want to delete. - :param noise_threshold_id: ID of the noise threshold that you want to delete.""" + :param noise_threshold_id: ID of the noise threshold that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -49,7 +54,9 @@ def get(self, *, noise_threshold_id: str) -> NoiseThreshold: :param noise_threshold_id: ID of the noise threshold that you want to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -58,7 +65,9 @@ def list(self, *, device_id: str) -> List[NoiseThreshold]: :param device_id: ID of the device for which you want to list noise thresholds. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -71,7 +80,7 @@ def update( name: Optional[str] = None, noise_threshold_decibels: Optional[float] = None, noise_threshold_nrs: Optional[float] = None, - starts_daily_at: Optional[str] = None + starts_daily_at: Optional[str] = None, ) -> None: """Updates a `noise threshold `_ for a `noise sensor `_. @@ -88,7 +97,8 @@ def update( :param noise_threshold_nrs: Noise level in Noiseaware Noise Risk Score (NRS) for the noise threshold. This parameter is only relevant for `Noiseaware sensors `_. :param starts_daily_at: Time at which the noise threshold should become active daily. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -97,6 +107,11 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/noise_sensors/noise_thresholds/create", + has_required_parameters=True, + has_pagination=False, + ) def create( self, *, @@ -105,7 +120,7 @@ def create( starts_daily_at: str, name: Optional[str] = None, noise_threshold_decibels: Optional[float] = None, - noise_threshold_nrs: Optional[float] = None + noise_threshold_nrs: Optional[float] = None, ) -> NoiseThreshold: """Creates a new `noise threshold `_ for a `noise sensor `_. Thresholds represent the limits of noise tolerated at a property, which can be customized for each hour of the day. Each device has its own default thresholds, but you can use the Seam API to modify them. @@ -121,8 +136,10 @@ def create( :param noise_threshold_nrs: Noise level in Noiseaware Noise Risk Score (NRS) for the new noise threshold. This parameter is only relevant for `Noiseaware sensors `_. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id @@ -137,61 +154,105 @@ def create( if noise_threshold_nrs is not None: json_payload["noise_threshold_nrs"] = noise_threshold_nrs + if not json_payload: + raise ValueError( + "At least one parameter is required for /noise_sensors/noise_thresholds/create" + ) + res = self.client.post( "/noise_sensors/noise_thresholds/create", json=json_payload ) return NoiseThreshold.from_dict(res["noise_threshold"]) + @route_metadata( + path="/noise_sensors/noise_thresholds/delete", + has_required_parameters=True, + has_pagination=False, + ) def delete(self, *, device_id: str, noise_threshold_id: str) -> None: """Deletes a `noise threshold `_ from a `noise sensor `_. :param device_id: ID of the device that contains the noise threshold that you want to delete. - :param noise_threshold_id: ID of the noise threshold that you want to delete.""" - json_payload = {} + :param noise_threshold_id: ID of the noise threshold that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if noise_threshold_id is not None: - json_payload["noise_threshold_id"] = noise_threshold_id + params["noise_threshold_id"] = noise_threshold_id - self.client.post("/noise_sensors/noise_thresholds/delete", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /noise_sensors/noise_thresholds/delete" + ) + + self.client.delete("/noise_sensors/noise_thresholds/delete", params=params) return None + @route_metadata( + path="/noise_sensors/noise_thresholds/get", + has_required_parameters=True, + has_pagination=False, + ) def get(self, *, noise_threshold_id: str) -> NoiseThreshold: """Returns a specified `noise threshold `_ for a `noise sensor `_. :param noise_threshold_id: ID of the noise threshold that you want to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if noise_threshold_id is not None: - json_payload["noise_threshold_id"] = noise_threshold_id + params["noise_threshold_id"] = noise_threshold_id - res = self.client.post("/noise_sensors/noise_thresholds/get", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /noise_sensors/noise_thresholds/get" + ) + + res = self.client.get("/noise_sensors/noise_thresholds/get", params=params) return NoiseThreshold.from_dict(res["noise_threshold"]) + @route_metadata( + path="/noise_sensors/noise_thresholds/list", + has_required_parameters=True, + has_pagination=False, + ) def list(self, *, device_id: str) -> List[NoiseThreshold]: """Returns a list of all `noise thresholds `_ for a `noise sensor `_. :param device_id: ID of the device for which you want to list noise thresholds. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id - res = self.client.post( - "/noise_sensors/noise_thresholds/list", json=json_payload - ) + if not params: + raise ValueError( + "At least one parameter is required for /noise_sensors/noise_thresholds/list" + ) + + res = self.client.get("/noise_sensors/noise_thresholds/list", params=params) return [NoiseThreshold.from_dict(item) for item in res["noise_thresholds"]] + @route_metadata( + path="/noise_sensors/noise_thresholds/update", + has_required_parameters=True, + has_pagination=False, + ) def update( self, *, @@ -201,7 +262,7 @@ def update( name: Optional[str] = None, noise_threshold_decibels: Optional[float] = None, noise_threshold_nrs: Optional[float] = None, - starts_daily_at: Optional[str] = None + starts_daily_at: Optional[str] = None, ) -> None: """Updates a `noise threshold `_ for a `noise sensor `_. @@ -218,8 +279,9 @@ def update( :param noise_threshold_nrs: Noise level in Noiseaware Noise Risk Score (NRS) for the noise threshold. This parameter is only relevant for `Noiseaware sensors `_. :param starts_daily_at: Time at which the noise threshold should become active daily. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id @@ -236,6 +298,11 @@ def update( if starts_daily_at is not None: json_payload["starts_daily_at"] = starts_daily_at - self.client.post("/noise_sensors/noise_thresholds/update", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /noise_sensors/noise_thresholds/update" + ) + + self.client.put("/noise_sensors/noise_thresholds/update", json=json_payload) return None diff --git a/seam/routes/noise_sensors_simulate.py b/seam/routes/noise_sensors_simulate.py index 1ce320f2..6c527582 100644 --- a/seam/routes/noise_sensors_simulate.py +++ b/seam/routes/noise_sensors_simulate.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata class AbstractNoiseSensorsSimulate(abc.ABC): @@ -10,7 +11,8 @@ def trigger_noise_threshold(self, *, device_id: str) -> None: """Simulates the triggering of a `noise threshold `_ for a `noise sensor `_ in a `sandbox workspace `_. :param device_id: ID of the device for which you want to simulate the triggering of a noise threshold. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -19,16 +21,27 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/noise_sensors/simulate/trigger_noise_threshold", + has_required_parameters=True, + has_pagination=False, + ) def trigger_noise_threshold(self, *, device_id: str) -> None: """Simulates the triggering of a `noise threshold `_ for a `noise sensor `_ in a `sandbox workspace `_. :param device_id: ID of the device for which you want to simulate the triggering of a noise threshold. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /noise_sensors/simulate/trigger_noise_threshold" + ) + self.client.post( "/noise_sensors/simulate/trigger_noise_threshold", json=json_payload ) diff --git a/seam/routes/phones.py b/seam/routes/phones.py index 2c19692f..a00d49f4 100644 --- a/seam/routes/phones.py +++ b/seam/routes/phones.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata from ..resources import Phone from .phones_simulate import AbstractPhonesSimulate, PhonesSimulate @@ -16,7 +17,9 @@ def simulate(self) -> AbstractPhonesSimulate: def deactivate(self, *, device_id: str) -> None: """Deactivates a phone, which is useful, for example, if a user has lost their phone. For more information, see `App User Lost Phone Process `_. - :param device_id: Device ID of the phone that you want to deactivate.""" + :param device_id: Device ID of the phone that you want to deactivate. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -25,7 +28,9 @@ def get(self, *, device_id: str) -> Phone: :param device_id: Device ID of the phone that you want to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -33,7 +38,7 @@ def list( self, *, acs_credential_id: Optional[str] = None, - owner_user_identity_id: Optional[str] = None + owner_user_identity_id: Optional[str] = None, ) -> List[Phone]: """Returns a list of all `phones `_. To filter the list of returned phones by a specific owner user identity or credential, include the ``owner_user_identity_id`` or ``acs_credential_id``, respectively, in the request body. @@ -55,39 +60,60 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): def simulate(self) -> PhonesSimulate: return self._simulate + @route_metadata( + path="/phones/deactivate", has_required_parameters=True, has_pagination=False + ) def deactivate(self, *, device_id: str) -> None: """Deactivates a phone, which is useful, for example, if a user has lost their phone. For more information, see `App User Lost Phone Process `_. - :param device_id: Device ID of the phone that you want to deactivate.""" - json_payload = {} + :param device_id: Device ID of the phone that you want to deactivate. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id - self.client.post("/phones/deactivate", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /phones/deactivate" + ) + + self.client.delete("/phones/deactivate", params=params) return None + @route_metadata( + path="/phones/get", has_required_parameters=True, has_pagination=False + ) def get(self, *, device_id: str) -> Phone: """Returns a specified `phone `_. :param device_id: Device ID of the phone that you want to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id + + if not params: + raise ValueError("At least one parameter is required for /phones/get") - res = self.client.post("/phones/get", json=json_payload) + res = self.client.get("/phones/get", params=params) return Phone.from_dict(res["phone"]) + @route_metadata( + path="/phones/list", has_required_parameters=False, has_pagination=False + ) def list( self, *, acs_credential_id: Optional[str] = None, - owner_user_identity_id: Optional[str] = None + owner_user_identity_id: Optional[str] = None, ) -> List[Phone]: """Returns a list of all `phones `_. To filter the list of returned phones by a specific owner user identity or credential, include the ``owner_user_identity_id`` or ``acs_credential_id``, respectively, in the request body. @@ -96,13 +122,13 @@ def list( :param owner_user_identity_id: ID of the user identity that represents the owner by which you want to filter the list of returned phones. :returns: OK""" - json_payload = {} + params: Dict[str, Any] = {} if acs_credential_id is not None: - json_payload["acs_credential_id"] = acs_credential_id + params["acs_credential_id"] = acs_credential_id if owner_user_identity_id is not None: - json_payload["owner_user_identity_id"] = owner_user_identity_id + params["owner_user_identity_id"] = owner_user_identity_id - res = self.client.post("/phones/list", json=json_payload) + res = self.client.get("/phones/list", params=params) return [Phone.from_dict(item) for item in res["phones"]] diff --git a/seam/routes/phones_simulate.py b/seam/routes/phones_simulate.py index 3144dba7..58b47aca 100644 --- a/seam/routes/phones_simulate.py +++ b/seam/routes/phones_simulate.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata from ..resources import Phone @@ -13,7 +14,7 @@ def create_sandbox_phone( user_identity_id: str, assa_abloy_metadata: Optional[Dict[str, Any]] = None, custom_sdk_installation_id: Optional[str] = None, - phone_metadata: Optional[Dict[str, Any]] = None + phone_metadata: Optional[Dict[str, Any]] = None, ) -> Phone: """Creates a new simulated phone in a `sandbox workspace `_. See also `Creating a Simulated Phone for a User Identity `_. @@ -25,7 +26,9 @@ def create_sandbox_phone( :param phone_metadata: Metadata that you want to associate with the simulated phone. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -34,13 +37,18 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/phones/simulate/create_sandbox_phone", + has_required_parameters=True, + has_pagination=False, + ) def create_sandbox_phone( self, *, user_identity_id: str, assa_abloy_metadata: Optional[Dict[str, Any]] = None, custom_sdk_installation_id: Optional[str] = None, - phone_metadata: Optional[Dict[str, Any]] = None + phone_metadata: Optional[Dict[str, Any]] = None, ) -> Phone: """Creates a new simulated phone in a `sandbox workspace `_. See also `Creating a Simulated Phone for a User Identity `_. @@ -52,8 +60,10 @@ def create_sandbox_phone( :param phone_metadata: Metadata that you want to associate with the simulated phone. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id @@ -64,6 +74,11 @@ def create_sandbox_phone( if phone_metadata is not None: json_payload["phone_metadata"] = phone_metadata + if not json_payload: + raise ValueError( + "At least one parameter is required for /phones/simulate/create_sandbox_phone" + ) + res = self.client.post( "/phones/simulate/create_sandbox_phone", json=json_payload ) diff --git a/seam/routes/spaces.py b/seam/routes/spaces.py index 4ebb7ed4..56f80f37 100644 --- a/seam/routes/spaces.py +++ b/seam/routes/spaces.py @@ -1,6 +1,8 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +from ..null import Null from ..resources import Space, Batch @@ -12,7 +14,9 @@ def add_acs_entrances(self, *, acs_entrance_ids: List[str], space_id: str) -> No :param acs_entrance_ids: IDs of the entrances that you want to add to the space. - :param space_id: ID of the space to which you want to add entrances.""" + :param space_id: ID of the space to which you want to add entrances. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -24,7 +28,8 @@ def add_connected_account( :param connected_account_id: ID of the connected account that you want to add to the space. :param space_id: ID of the space to which you want to add the connected account. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -33,7 +38,9 @@ def add_devices(self, *, device_ids: List[str], space_id: str) -> None: :param device_ids: IDs of the devices that you want to add to the space. - :param space_id: ID of the space to which you want to add devices.""" + :param space_id: ID of the space to which you want to add devices. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -46,7 +53,7 @@ def create( customer_data: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, device_ids: Optional[List[str]] = None, - space_key: Optional[str] = None + space_key: Optional[str] = None, ) -> Space: """Creates a new space. @@ -64,14 +71,18 @@ def create( :param space_key: Unique key for the space within the workspace. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod def delete(self, *, space_id: str) -> None: """Deletes a space. - :param space_id: ID of the space that you want to delete.""" + :param space_id: ID of the space that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -84,7 +95,9 @@ def get( :param space_key: Unique key of the space that you want to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -94,7 +107,7 @@ def get_related( exclude: Optional[List[str]] = None, include: Optional[List[str]] = None, space_ids: Optional[List[str]] = None, - space_keys: Optional[List[str]] = None + space_keys: Optional[List[str]] = None, ) -> Batch: """Gets all related resources for one or more Spaces. @@ -106,7 +119,9 @@ def get_related( :param space_keys: Keys of the spaces that you want to get along with their related resources. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -115,9 +130,9 @@ def list( *, customer_key: Optional[str] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, - space_key: Optional[str] = None + space_key: Optional[str] = None, ) -> List[Space]: """Returns a list of all spaces. @@ -142,7 +157,9 @@ def remove_acs_entrances( :param acs_entrance_ids: IDs of the entrances that you want to remove from the space. - :param space_id: ID of the space from which you want to remove entrances.""" + :param space_id: ID of the space from which you want to remove entrances. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -154,7 +171,8 @@ def remove_connected_account( :param connected_account_id: ID of the connected account that you want to remove from the space. :param space_id: ID of the space from which you want to remove the connected account. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -163,7 +181,9 @@ def remove_devices(self, *, device_ids: List[str], space_id: str) -> None: :param device_ids: IDs of the devices that you want to remove from the space. - :param space_id: ID of the space from which you want to remove devices.""" + :param space_id: ID of the space from which you want to remove devices. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -175,7 +195,7 @@ def update( device_ids: Optional[List[str]] = None, name: Optional[str] = None, space_id: Optional[str] = None, - space_key: Optional[str] = None + space_key: Optional[str] = None, ) -> Space: """Updates an existing space. @@ -200,23 +220,40 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/spaces/add_acs_entrances", + has_required_parameters=True, + has_pagination=False, + ) def add_acs_entrances(self, *, acs_entrance_ids: List[str], space_id: str) -> None: """Adds `entrances `_ to a specific space. :param acs_entrance_ids: IDs of the entrances that you want to add to the space. - :param space_id: ID of the space to which you want to add entrances.""" - json_payload = {} + :param space_id: ID of the space to which you want to add entrances. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_entrance_ids is not None: json_payload["acs_entrance_ids"] = acs_entrance_ids if space_id is not None: json_payload["space_id"] = space_id - self.client.post("/spaces/add_acs_entrances", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /spaces/add_acs_entrances" + ) + + self.client.put("/spaces/add_acs_entrances", json=json_payload) return None + @route_metadata( + path="/spaces/add_connected_account", + has_required_parameters=True, + has_pagination=False, + ) def add_connected_account( self, *, connected_account_id: str, space_id: str ) -> None: @@ -225,35 +262,54 @@ def add_connected_account( :param connected_account_id: ID of the connected account that you want to add to the space. :param space_id: ID of the space to which you want to add the connected account. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if connected_account_id is not None: json_payload["connected_account_id"] = connected_account_id if space_id is not None: json_payload["space_id"] = space_id - self.client.post("/spaces/add_connected_account", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /spaces/add_connected_account" + ) + + self.client.put("/spaces/add_connected_account", json=json_payload) return None + @route_metadata( + path="/spaces/add_devices", has_required_parameters=True, has_pagination=False + ) def add_devices(self, *, device_ids: List[str], space_id: str) -> None: """Adds devices to a specific space. :param device_ids: IDs of the devices that you want to add to the space. - :param space_id: ID of the space to which you want to add devices.""" - json_payload = {} + :param space_id: ID of the space to which you want to add devices. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_ids is not None: json_payload["device_ids"] = device_ids if space_id is not None: json_payload["space_id"] = space_id - self.client.post("/spaces/add_devices", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /spaces/add_devices" + ) + + self.client.put("/spaces/add_devices", json=json_payload) return None + @route_metadata( + path="/spaces/create", has_required_parameters=True, has_pagination=False + ) def create( self, *, @@ -263,7 +319,7 @@ def create( customer_data: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, device_ids: Optional[List[str]] = None, - space_key: Optional[str] = None + space_key: Optional[str] = None, ) -> Space: """Creates a new space. @@ -281,8 +337,10 @@ def create( :param space_key: Unique key for the space within the workspace. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if name is not None: json_payload["name"] = name @@ -299,23 +357,37 @@ def create( if space_key is not None: json_payload["space_key"] = space_key + if not json_payload: + raise ValueError("At least one parameter is required for /spaces/create") + res = self.client.post("/spaces/create", json=json_payload) return Space.from_dict(res["space"]) + @route_metadata( + path="/spaces/delete", has_required_parameters=True, has_pagination=False + ) def delete(self, *, space_id: str) -> None: """Deletes a space. - :param space_id: ID of the space that you want to delete.""" - json_payload = {} + :param space_id: ID of the space that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if space_id is not None: - json_payload["space_id"] = space_id + params["space_id"] = space_id + + if not params: + raise ValueError("At least one parameter is required for /spaces/delete") - self.client.post("/spaces/delete", json=json_payload) + self.client.delete("/spaces/delete", params=params) return None + @route_metadata( + path="/spaces/get", has_required_parameters=True, has_pagination=False + ) def get( self, *, space_id: Optional[str] = None, space_key: Optional[str] = None ) -> Space: @@ -325,25 +397,33 @@ def get( :param space_key: Unique key of the space that you want to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if space_id is not None: - json_payload["space_id"] = space_id + params["space_id"] = space_id if space_key is not None: - json_payload["space_key"] = space_key + params["space_key"] = space_key + + if not params: + raise ValueError("At least one parameter is required for /spaces/get") - res = self.client.post("/spaces/get", json=json_payload) + res = self.client.get("/spaces/get", params=params) return Space.from_dict(res["space"]) + @route_metadata( + path="/spaces/get_related", has_required_parameters=True, has_pagination=False + ) def get_related( self, *, exclude: Optional[List[str]] = None, include: Optional[List[str]] = None, space_ids: Optional[List[str]] = None, - space_keys: Optional[List[str]] = None + space_keys: Optional[List[str]] = None, ) -> Batch: """Gets all related resources for one or more Spaces. @@ -355,8 +435,10 @@ def get_related( :param space_keys: Keys of the spaces that you want to get along with their related resources. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if exclude is not None: json_payload["exclude"] = exclude @@ -367,18 +449,26 @@ def get_related( if space_keys is not None: json_payload["space_keys"] = space_keys + if not json_payload: + raise ValueError( + "At least one parameter is required for /spaces/get_related" + ) + res = self.client.post("/spaces/get_related", json=json_payload) return Batch.from_dict(res["batch"]) + @route_metadata( + path="/spaces/list", has_required_parameters=False, has_pagination=True + ) def list( self, *, customer_key: Optional[str] = None, limit: Optional[float] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, - space_key: Optional[str] = None + space_key: Optional[str] = None, ) -> List[Space]: """Returns a list of all spaces. @@ -393,23 +483,28 @@ def list( :param space_key: Filter spaces by space_key. :returns: OK""" - json_payload = {} + params: Dict[str, Any] = {} if customer_key is not None: - json_payload["customer_key"] = customer_key + params["customer_key"] = customer_key if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if search is not None: - json_payload["search"] = search + params["search"] = search if space_key is not None: - json_payload["space_key"] = space_key + params["space_key"] = space_key - res = self.client.post("/spaces/list", json=json_payload) + res = self.client.get("/spaces/list", params=params) return [Space.from_dict(item) for item in res["spaces"]] + @route_metadata( + path="/spaces/remove_acs_entrances", + has_required_parameters=True, + has_pagination=False, + ) def remove_acs_entrances( self, *, acs_entrance_ids: List[str], space_id: str ) -> None: @@ -417,18 +512,30 @@ def remove_acs_entrances( :param acs_entrance_ids: IDs of the entrances that you want to remove from the space. - :param space_id: ID of the space from which you want to remove entrances.""" - json_payload = {} + :param space_id: ID of the space from which you want to remove entrances. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_entrance_ids is not None: json_payload["acs_entrance_ids"] = acs_entrance_ids if space_id is not None: json_payload["space_id"] = space_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /spaces/remove_acs_entrances" + ) + self.client.post("/spaces/remove_acs_entrances", json=json_payload) return None + @route_metadata( + path="/spaces/remove_connected_account", + has_required_parameters=True, + has_pagination=False, + ) def remove_connected_account( self, *, connected_account_id: str, space_id: str ) -> None: @@ -437,35 +544,56 @@ def remove_connected_account( :param connected_account_id: ID of the connected account that you want to remove from the space. :param space_id: ID of the space from which you want to remove the connected account. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if connected_account_id is not None: - json_payload["connected_account_id"] = connected_account_id + params["connected_account_id"] = connected_account_id if space_id is not None: - json_payload["space_id"] = space_id + params["space_id"] = space_id + + if not params: + raise ValueError( + "At least one parameter is required for /spaces/remove_connected_account" + ) - self.client.post("/spaces/remove_connected_account", json=json_payload) + self.client.delete("/spaces/remove_connected_account", params=params) return None + @route_metadata( + path="/spaces/remove_devices", + has_required_parameters=True, + has_pagination=False, + ) def remove_devices(self, *, device_ids: List[str], space_id: str) -> None: """Removes devices from a specific space. :param device_ids: IDs of the devices that you want to remove from the space. - :param space_id: ID of the space from which you want to remove devices.""" - json_payload = {} + :param space_id: ID of the space from which you want to remove devices. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_ids is not None: json_payload["device_ids"] = device_ids if space_id is not None: json_payload["space_id"] = space_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /spaces/remove_devices" + ) + self.client.post("/spaces/remove_devices", json=json_payload) return None + @route_metadata( + path="/spaces/update", has_required_parameters=False, has_pagination=False + ) def update( self, *, @@ -474,7 +602,7 @@ def update( device_ids: Optional[List[str]] = None, name: Optional[str] = None, space_id: Optional[str] = None, - space_key: Optional[str] = None + space_key: Optional[str] = None, ) -> Space: """Updates an existing space. @@ -491,7 +619,7 @@ def update( :param space_key: Unique key of the space that you want to update. :returns: OK""" - json_payload = {} + json_payload: Dict[str, Any] = {} if acs_entrance_ids is not None: json_payload["acs_entrance_ids"] = acs_entrance_ids @@ -506,6 +634,6 @@ def update( if space_key is not None: json_payload["space_key"] = space_key - res = self.client.post("/spaces/update", json=json_payload) + res = self.client.patch("/spaces/update", json=json_payload) return Space.from_dict(res["space"]) diff --git a/seam/routes/thermostats.py b/seam/routes/thermostats.py index f0741076..231090d3 100644 --- a/seam/routes/thermostats.py +++ b/seam/routes/thermostats.py @@ -1,6 +1,8 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +from ..null import Null from ..resources import ActionAttempt, Device from .thermostats_daily_programs import ( AbstractThermostatsDailyPrograms, @@ -34,7 +36,7 @@ def activate_climate_preset( *, climate_preset_key: str, device_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Activates a specified `climate preset `_ for a specified `thermostat `_. @@ -44,7 +46,9 @@ def activate_climate_preset( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -54,7 +58,7 @@ def cool( device_id: str, cooling_set_point_celsius: Optional[float] = None, cooling_set_point_fahrenheit: Optional[float] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Sets a specified `thermostat `_ to `cool mode `_. @@ -66,7 +70,9 @@ def cool( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -84,7 +90,7 @@ def create_climate_preset( heating_set_point_fahrenheit: Optional[float] = None, hvac_mode_setting: Optional[str] = None, manual_override_allowed: Optional[bool] = None, - name: Optional[str] = None + name: Optional[Union[str, Null]] = None, ) -> None: """Creates a `climate preset `_ for a specified `thermostat `_. @@ -111,7 +117,8 @@ def create_climate_preset( :param manual_override_allowed: Deprecated: Use 'thermostat_schedule.is_override_allowed' Indicates whether a person at the thermostat or using the API can change the thermostat's settings. :param name: User-friendly name to identify the `climate preset `_. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -121,7 +128,8 @@ def delete_climate_preset(self, *, climate_preset_key: str, device_id: str) -> N :param climate_preset_key: Climate preset key of the climate preset that you want to delete. :param device_id: ID of the thermostat device for which you want to delete a climate preset. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -131,7 +139,7 @@ def heat( device_id: str, heating_set_point_celsius: Optional[float] = None, heating_set_point_fahrenheit: Optional[float] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Sets a specified `thermostat `_ to `heat mode `_. @@ -143,7 +151,9 @@ def heat( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -155,7 +165,7 @@ def heat_cool( cooling_set_point_fahrenheit: Optional[float] = None, heating_set_point_celsius: Optional[float] = None, heating_set_point_fahrenheit: Optional[float] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Sets a specified `thermostat `_ to `heat-cool ("auto") mode `_. @@ -171,7 +181,9 @@ def heat_cool( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -180,20 +192,10 @@ def list( *, connect_webview_id: Optional[str] = None, connected_account_id: Optional[str] = None, - connected_account_ids: Optional[List[str]] = None, - created_before: Optional[str] = None, - custom_metadata_has: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, - device_ids: Optional[List[str]] = None, device_type: Optional[str] = None, device_types: Optional[List[str]] = None, - limit: Optional[float] = None, manufacturer: Optional[str] = None, - page_cursor: Optional[str] = None, - search: Optional[str] = None, - space_id: Optional[str] = None, - unstable_location_id: Optional[str] = None, - user_identifier_key: Optional[str] = None ) -> List[Device]: """Returns a list of all `thermostats `_. @@ -201,34 +203,14 @@ def list( :param connected_account_id: ID of the connected account for which you want to list devices. - :param connected_account_ids: Array of IDs of the connected accounts for which you want to list devices. - - :param created_before: Timestamp by which to limit returned devices. Returns devices created before this timestamp. - - :param custom_metadata_has: Set of key:value `custom metadata `_ pairs for which you want to list devices. - :param customer_key: Customer key for which you want to list devices. - :param device_ids: Array of device IDs for which you want to list devices. - :param device_type: Device type by which you want to filter thermostat devices. :param device_types: Array of device types by which you want to filter thermostat devices. - :param limit: Numerical limit on the number of devices to return. - :param manufacturer: Manufacturer by which you want to filter thermostat devices. - :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. - - :param search: String for which to search. Filters returned devices to include all records that satisfy a partial match using ``device_id`` (full or partial UUID prefix, minimum 4 characters), ``connected_account_id``, ``display_name``, ``custom_metadata`` or ``location.location_name``. - - :param space_id: ID of the space for which you want to list devices. - - :param unstable_location_id: Deprecated: Use ``space_id``. - - :param user_identifier_key: Your own internal user ID for the user for which you want to list devices. - :returns: OK""" raise NotImplementedError() @@ -237,7 +219,7 @@ def off( self, *, device_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Sets a specified `thermostat `_ to `"off" mode `_. @@ -245,7 +227,9 @@ def off( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -257,7 +241,8 @@ def set_fallback_climate_preset( :param climate_preset_key: Climate preset key of the climate preset that you want to set as the fallback climate preset. :param device_id: ID of the thermostat device for which you want to set the fallback climate preset. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -267,7 +252,7 @@ def set_fan_mode( device_id: str, fan_mode: Optional[str] = None, fan_mode_setting: Optional[str] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Sets the `fan mode setting `_ for a specified `thermostat `_. @@ -279,7 +264,9 @@ def set_fan_mode( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -292,7 +279,7 @@ def set_hvac_mode( cooling_set_point_fahrenheit: Optional[float] = None, heating_set_point_celsius: Optional[float] = None, heating_set_point_fahrenheit: Optional[float] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Sets the `HVAC mode `_ for a specified `thermostat `_. @@ -310,7 +297,9 @@ def set_hvac_mode( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -318,10 +307,10 @@ def set_temperature_threshold( self, *, device_id: str, - lower_limit_celsius: Optional[float] = None, - lower_limit_fahrenheit: Optional[float] = None, - upper_limit_celsius: Optional[float] = None, - upper_limit_fahrenheit: Optional[float] = None + lower_limit_celsius: Optional[Union[float, Null]] = None, + lower_limit_fahrenheit: Optional[Union[float, Null]] = None, + upper_limit_celsius: Optional[Union[float, Null]] = None, + upper_limit_fahrenheit: Optional[Union[float, Null]] = None, ) -> None: """Sets a `temperature threshold `_ for a specified thermostat. Seam emits a ``thermostat.temperature_threshold_exceeded`` event and adds a warning on a thermostat if it reports a temperature outside the threshold range. @@ -334,7 +323,8 @@ def set_temperature_threshold( :param upper_limit_celsius: Upper temperature limit in in °C. Seam alerts you if the reported temperature is higher than this value. You can specify either ``upper_limit`` but not both. :param upper_limit_fahrenheit: Upper temperature limit in in °C. Seam alerts you if the reported temperature is higher than this value. You can specify either ``upper_limit`` but not both. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -352,7 +342,7 @@ def update_climate_preset( heating_set_point_fahrenheit: Optional[float] = None, hvac_mode_setting: Optional[str] = None, manual_override_allowed: Optional[bool] = None, - name: Optional[str] = None + name: Optional[Union[str, Null]] = None, ) -> None: """Updates a specified `climate preset `_ for a specified `thermostat `_. @@ -379,7 +369,8 @@ def update_climate_preset( :param manual_override_allowed: Deprecated: Use 'thermostat_schedule.is_override_allowed' Indicates whether a person at the thermostat can change the thermostat's settings. See `Specifying Manual Override Permissions `_. :param name: User-friendly name to identify the `climate preset `_. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -387,14 +378,14 @@ def update_weekly_program( self, *, device_id: str, - friday_program_id: Optional[str] = None, - monday_program_id: Optional[str] = None, - saturday_program_id: Optional[str] = None, - sunday_program_id: Optional[str] = None, - thursday_program_id: Optional[str] = None, - tuesday_program_id: Optional[str] = None, - wednesday_program_id: Optional[str] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + friday_program_id: Optional[Union[str, Null]] = None, + monday_program_id: Optional[Union[str, Null]] = None, + saturday_program_id: Optional[Union[str, Null]] = None, + sunday_program_id: Optional[Union[str, Null]] = None, + thursday_program_id: Optional[Union[str, Null]] = None, + tuesday_program_id: Optional[Union[str, Null]] = None, + wednesday_program_id: Optional[Union[str, Null]] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Updates the thermostat weekly program for a thermostat device. To configure a weekly program, specify the ID of the daily program that you want to use for each day of the week. When you update a weekly program, the set of programs that you specify overwrites any previous weekly program for the thermostat. @@ -416,7 +407,9 @@ def update_weekly_program( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -442,12 +435,17 @@ def schedules(self) -> ThermostatsSchedules: def simulate(self) -> ThermostatsSimulate: return self._simulate + @route_metadata( + path="/thermostats/activate_climate_preset", + has_required_parameters=True, + has_pagination=False, + ) def activate_climate_preset( self, *, climate_preset_key: str, device_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Activates a specified `climate preset `_ for a specified `thermostat `_. @@ -457,14 +455,21 @@ def activate_climate_preset( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if climate_preset_key is not None: json_payload["climate_preset_key"] = climate_preset_key if device_id is not None: json_payload["device_id"] = device_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/activate_climate_preset" + ) + res = self.client.post( "/thermostats/activate_climate_preset", json=json_payload ) @@ -481,13 +486,16 @@ def activate_climate_preset( wait_for_action_attempt=wait_for_action_attempt, ) + @route_metadata( + path="/thermostats/cool", has_required_parameters=True, has_pagination=False + ) def cool( self, *, device_id: str, cooling_set_point_celsius: Optional[float] = None, cooling_set_point_fahrenheit: Optional[float] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Sets a specified `thermostat `_ to `cool mode `_. @@ -499,8 +507,10 @@ def cool( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id @@ -509,6 +519,9 @@ def cool( if cooling_set_point_fahrenheit is not None: json_payload["cooling_set_point_fahrenheit"] = cooling_set_point_fahrenheit + if not json_payload: + raise ValueError("At least one parameter is required for /thermostats/cool") + res = self.client.post("/thermostats/cool", json=json_payload) wait_for_action_attempt = ( @@ -523,6 +536,11 @@ def cool( wait_for_action_attempt=wait_for_action_attempt, ) + @route_metadata( + path="/thermostats/create_climate_preset", + has_required_parameters=True, + has_pagination=False, + ) def create_climate_preset( self, *, @@ -537,7 +555,7 @@ def create_climate_preset( heating_set_point_fahrenheit: Optional[float] = None, hvac_mode_setting: Optional[str] = None, manual_override_allowed: Optional[bool] = None, - name: Optional[str] = None + name: Optional[Union[str, Null]] = None, ) -> None: """Creates a `climate preset `_ for a specified `thermostat `_. @@ -564,8 +582,9 @@ def create_climate_preset( :param manual_override_allowed: Deprecated: Use 'thermostat_schedule.is_override_allowed' Indicates whether a person at the thermostat or using the API can change the thermostat's settings. :param name: User-friendly name to identify the `climate preset `_. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if climate_preset_key is not None: json_payload["climate_preset_key"] = climate_preset_key @@ -592,35 +611,54 @@ def create_climate_preset( if name is not None: json_payload["name"] = name + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/create_climate_preset" + ) + self.client.post("/thermostats/create_climate_preset", json=json_payload) return None + @route_metadata( + path="/thermostats/delete_climate_preset", + has_required_parameters=True, + has_pagination=False, + ) def delete_climate_preset(self, *, climate_preset_key: str, device_id: str) -> None: """Deletes a specified `climate preset `_ for a specified `thermostat `_. :param climate_preset_key: Climate preset key of the climate preset that you want to delete. :param device_id: ID of the thermostat device for which you want to delete a climate preset. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if climate_preset_key is not None: - json_payload["climate_preset_key"] = climate_preset_key + params["climate_preset_key"] = climate_preset_key if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id - self.client.post("/thermostats/delete_climate_preset", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /thermostats/delete_climate_preset" + ) + + self.client.delete("/thermostats/delete_climate_preset", params=params) return None + @route_metadata( + path="/thermostats/heat", has_required_parameters=True, has_pagination=False + ) def heat( self, *, device_id: str, heating_set_point_celsius: Optional[float] = None, heating_set_point_fahrenheit: Optional[float] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Sets a specified `thermostat `_ to `heat mode `_. @@ -632,8 +670,10 @@ def heat( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id @@ -642,6 +682,9 @@ def heat( if heating_set_point_fahrenheit is not None: json_payload["heating_set_point_fahrenheit"] = heating_set_point_fahrenheit + if not json_payload: + raise ValueError("At least one parameter is required for /thermostats/heat") + res = self.client.post("/thermostats/heat", json=json_payload) wait_for_action_attempt = ( @@ -656,6 +699,11 @@ def heat( wait_for_action_attempt=wait_for_action_attempt, ) + @route_metadata( + path="/thermostats/heat_cool", + has_required_parameters=True, + has_pagination=False, + ) def heat_cool( self, *, @@ -664,7 +712,7 @@ def heat_cool( cooling_set_point_fahrenheit: Optional[float] = None, heating_set_point_celsius: Optional[float] = None, heating_set_point_fahrenheit: Optional[float] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Sets a specified `thermostat `_ to `heat-cool ("auto") mode `_. @@ -680,8 +728,10 @@ def heat_cool( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id @@ -694,6 +744,11 @@ def heat_cool( if heating_set_point_fahrenheit is not None: json_payload["heating_set_point_fahrenheit"] = heating_set_point_fahrenheit + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/heat_cool" + ) + res = self.client.post("/thermostats/heat_cool", json=json_payload) wait_for_action_attempt = ( @@ -708,25 +763,18 @@ def heat_cool( wait_for_action_attempt=wait_for_action_attempt, ) + @route_metadata( + path="/thermostats/list", has_required_parameters=False, has_pagination=False + ) def list( self, *, connect_webview_id: Optional[str] = None, connected_account_id: Optional[str] = None, - connected_account_ids: Optional[List[str]] = None, - created_before: Optional[str] = None, - custom_metadata_has: Optional[Dict[str, Any]] = None, customer_key: Optional[str] = None, - device_ids: Optional[List[str]] = None, device_type: Optional[str] = None, device_types: Optional[List[str]] = None, - limit: Optional[float] = None, manufacturer: Optional[str] = None, - page_cursor: Optional[str] = None, - search: Optional[str] = None, - space_id: Optional[str] = None, - unstable_location_id: Optional[str] = None, - user_identifier_key: Optional[str] = None ) -> List[Device]: """Returns a list of all `thermostats `_. @@ -734,79 +782,42 @@ def list( :param connected_account_id: ID of the connected account for which you want to list devices. - :param connected_account_ids: Array of IDs of the connected accounts for which you want to list devices. - - :param created_before: Timestamp by which to limit returned devices. Returns devices created before this timestamp. - - :param custom_metadata_has: Set of key:value `custom metadata `_ pairs for which you want to list devices. - :param customer_key: Customer key for which you want to list devices. - :param device_ids: Array of device IDs for which you want to list devices. - :param device_type: Device type by which you want to filter thermostat devices. :param device_types: Array of device types by which you want to filter thermostat devices. - :param limit: Numerical limit on the number of devices to return. - :param manufacturer: Manufacturer by which you want to filter thermostat devices. - :param page_cursor: Identifies the specific page of results to return, obtained from the previous page's ``next_page_cursor``. - - :param search: String for which to search. Filters returned devices to include all records that satisfy a partial match using ``device_id`` (full or partial UUID prefix, minimum 4 characters), ``connected_account_id``, ``display_name``, ``custom_metadata`` or ``location.location_name``. - - :param space_id: ID of the space for which you want to list devices. - - :param unstable_location_id: Deprecated: Use ``space_id``. - - :param user_identifier_key: Your own internal user ID for the user for which you want to list devices. - :returns: OK""" - json_payload = {} + json_payload: Dict[str, Any] = {} if connect_webview_id is not None: json_payload["connect_webview_id"] = connect_webview_id if connected_account_id is not None: json_payload["connected_account_id"] = connected_account_id - if connected_account_ids is not None: - json_payload["connected_account_ids"] = connected_account_ids - if created_before is not None: - json_payload["created_before"] = created_before - if custom_metadata_has is not None: - json_payload["custom_metadata_has"] = custom_metadata_has if customer_key is not None: json_payload["customer_key"] = customer_key - if device_ids is not None: - json_payload["device_ids"] = device_ids if device_type is not None: json_payload["device_type"] = device_type if device_types is not None: json_payload["device_types"] = device_types - if limit is not None: - json_payload["limit"] = limit if manufacturer is not None: json_payload["manufacturer"] = manufacturer - if page_cursor is not None: - json_payload["page_cursor"] = page_cursor - if search is not None: - json_payload["search"] = search - if space_id is not None: - json_payload["space_id"] = space_id - if unstable_location_id is not None: - json_payload["unstable_location_id"] = unstable_location_id - if user_identifier_key is not None: - json_payload["user_identifier_key"] = user_identifier_key res = self.client.post("/thermostats/list", json=json_payload) return [Device.from_dict(item) for item in res["devices"]] + @route_metadata( + path="/thermostats/off", has_required_parameters=True, has_pagination=False + ) def off( self, *, device_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Sets a specified `thermostat `_ to `"off" mode `_. @@ -814,12 +825,17 @@ def off( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id + if not json_payload: + raise ValueError("At least one parameter is required for /thermostats/off") + res = self.client.post("/thermostats/off", json=json_payload) wait_for_action_attempt = ( @@ -834,6 +850,11 @@ def off( wait_for_action_attempt=wait_for_action_attempt, ) + @route_metadata( + path="/thermostats/set_fallback_climate_preset", + has_required_parameters=True, + has_pagination=False, + ) def set_fallback_climate_preset( self, *, climate_preset_key: str, device_id: str ) -> None: @@ -842,25 +863,36 @@ def set_fallback_climate_preset( :param climate_preset_key: Climate preset key of the climate preset that you want to set as the fallback climate preset. :param device_id: ID of the thermostat device for which you want to set the fallback climate preset. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if climate_preset_key is not None: json_payload["climate_preset_key"] = climate_preset_key if device_id is not None: json_payload["device_id"] = device_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/set_fallback_climate_preset" + ) + self.client.post("/thermostats/set_fallback_climate_preset", json=json_payload) return None + @route_metadata( + path="/thermostats/set_fan_mode", + has_required_parameters=True, + has_pagination=False, + ) def set_fan_mode( self, *, device_id: str, fan_mode: Optional[str] = None, fan_mode_setting: Optional[str] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Sets the `fan mode setting `_ for a specified `thermostat `_. @@ -872,8 +904,10 @@ def set_fan_mode( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id @@ -882,6 +916,11 @@ def set_fan_mode( if fan_mode_setting is not None: json_payload["fan_mode_setting"] = fan_mode_setting + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/set_fan_mode" + ) + res = self.client.post("/thermostats/set_fan_mode", json=json_payload) wait_for_action_attempt = ( @@ -896,6 +935,11 @@ def set_fan_mode( wait_for_action_attempt=wait_for_action_attempt, ) + @route_metadata( + path="/thermostats/set_hvac_mode", + has_required_parameters=True, + has_pagination=False, + ) def set_hvac_mode( self, *, @@ -905,7 +949,7 @@ def set_hvac_mode( cooling_set_point_fahrenheit: Optional[float] = None, heating_set_point_celsius: Optional[float] = None, heating_set_point_fahrenheit: Optional[float] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Sets the `HVAC mode `_ for a specified `thermostat `_. @@ -923,8 +967,10 @@ def set_hvac_mode( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id @@ -939,6 +985,11 @@ def set_hvac_mode( if heating_set_point_fahrenheit is not None: json_payload["heating_set_point_fahrenheit"] = heating_set_point_fahrenheit + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/set_hvac_mode" + ) + res = self.client.post("/thermostats/set_hvac_mode", json=json_payload) wait_for_action_attempt = ( @@ -953,14 +1004,19 @@ def set_hvac_mode( wait_for_action_attempt=wait_for_action_attempt, ) + @route_metadata( + path="/thermostats/set_temperature_threshold", + has_required_parameters=True, + has_pagination=False, + ) def set_temperature_threshold( self, *, device_id: str, - lower_limit_celsius: Optional[float] = None, - lower_limit_fahrenheit: Optional[float] = None, - upper_limit_celsius: Optional[float] = None, - upper_limit_fahrenheit: Optional[float] = None + lower_limit_celsius: Optional[Union[float, Null]] = None, + lower_limit_fahrenheit: Optional[Union[float, Null]] = None, + upper_limit_celsius: Optional[Union[float, Null]] = None, + upper_limit_fahrenheit: Optional[Union[float, Null]] = None, ) -> None: """Sets a `temperature threshold `_ for a specified thermostat. Seam emits a ``thermostat.temperature_threshold_exceeded`` event and adds a warning on a thermostat if it reports a temperature outside the threshold range. @@ -973,8 +1029,9 @@ def set_temperature_threshold( :param upper_limit_celsius: Upper temperature limit in in °C. Seam alerts you if the reported temperature is higher than this value. You can specify either ``upper_limit`` but not both. :param upper_limit_fahrenheit: Upper temperature limit in in °C. Seam alerts you if the reported temperature is higher than this value. You can specify either ``upper_limit`` but not both. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id @@ -987,10 +1044,20 @@ def set_temperature_threshold( if upper_limit_fahrenheit is not None: json_payload["upper_limit_fahrenheit"] = upper_limit_fahrenheit - self.client.post("/thermostats/set_temperature_threshold", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/set_temperature_threshold" + ) + + self.client.patch("/thermostats/set_temperature_threshold", json=json_payload) return None + @route_metadata( + path="/thermostats/update_climate_preset", + has_required_parameters=True, + has_pagination=False, + ) def update_climate_preset( self, *, @@ -1005,7 +1072,7 @@ def update_climate_preset( heating_set_point_fahrenheit: Optional[float] = None, hvac_mode_setting: Optional[str] = None, manual_override_allowed: Optional[bool] = None, - name: Optional[str] = None + name: Optional[Union[str, Null]] = None, ) -> None: """Updates a specified `climate preset `_ for a specified `thermostat `_. @@ -1032,8 +1099,9 @@ def update_climate_preset( :param manual_override_allowed: Deprecated: Use 'thermostat_schedule.is_override_allowed' Indicates whether a person at the thermostat can change the thermostat's settings. See `Specifying Manual Override Permissions `_. :param name: User-friendly name to identify the `climate preset `_. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if climate_preset_key is not None: json_payload["climate_preset_key"] = climate_preset_key @@ -1060,22 +1128,32 @@ def update_climate_preset( if name is not None: json_payload["name"] = name - self.client.post("/thermostats/update_climate_preset", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/update_climate_preset" + ) + + self.client.patch("/thermostats/update_climate_preset", json=json_payload) return None + @route_metadata( + path="/thermostats/update_weekly_program", + has_required_parameters=True, + has_pagination=False, + ) def update_weekly_program( self, *, device_id: str, - friday_program_id: Optional[str] = None, - monday_program_id: Optional[str] = None, - saturday_program_id: Optional[str] = None, - sunday_program_id: Optional[str] = None, - thursday_program_id: Optional[str] = None, - tuesday_program_id: Optional[str] = None, - wednesday_program_id: Optional[str] = None, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + friday_program_id: Optional[Union[str, Null]] = None, + monday_program_id: Optional[Union[str, Null]] = None, + saturday_program_id: Optional[Union[str, Null]] = None, + sunday_program_id: Optional[Union[str, Null]] = None, + thursday_program_id: Optional[Union[str, Null]] = None, + tuesday_program_id: Optional[Union[str, Null]] = None, + wednesday_program_id: Optional[Union[str, Null]] = None, + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Updates the thermostat weekly program for a thermostat device. To configure a weekly program, specify the ID of the daily program that you want to use for each day of the week. When you update a weekly program, the set of programs that you specify overwrites any previous weekly program for the thermostat. @@ -1097,8 +1175,10 @@ def update_weekly_program( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id @@ -1117,6 +1197,11 @@ def update_weekly_program( if wednesday_program_id is not None: json_payload["wednesday_program_id"] = wednesday_program_id + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/update_weekly_program" + ) + res = self.client.post("/thermostats/update_weekly_program", json=json_payload) wait_for_action_attempt = ( diff --git a/seam/routes/thermostats_daily_programs.py b/seam/routes/thermostats_daily_programs.py index 56e886da..cff60944 100644 --- a/seam/routes/thermostats_daily_programs.py +++ b/seam/routes/thermostats_daily_programs.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata from ..resources import ThermostatDailyProgram, ActionAttempt from ..modules.action_attempts import resolve_action_attempt @@ -19,7 +20,9 @@ def create( :param periods: Array of thermostat daily program periods. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -27,7 +30,8 @@ def delete(self, *, thermostat_daily_program_id: str) -> None: """Deletes a thermostat daily program. :param thermostat_daily_program_id: ID of the thermostat daily program that you want to delete. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -37,7 +41,7 @@ def update( name: str, periods: List[Dict[str, Any]], thermostat_daily_program_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Updates a specified thermostat daily program. The periods that you specify overwrite any existing periods for the daily program. @@ -49,7 +53,9 @@ def update( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -58,6 +64,11 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/thermostats/daily_programs/create", + has_required_parameters=True, + has_pagination=False, + ) def create( self, *, device_id: str, name: str, periods: List[Dict[str, Any]] ) -> ThermostatDailyProgram: @@ -69,8 +80,10 @@ def create( :param periods: Array of thermostat daily program periods. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id @@ -79,31 +92,52 @@ def create( if periods is not None: json_payload["periods"] = periods + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/daily_programs/create" + ) + res = self.client.post("/thermostats/daily_programs/create", json=json_payload) return ThermostatDailyProgram.from_dict(res["thermostat_daily_program"]) + @route_metadata( + path="/thermostats/daily_programs/delete", + has_required_parameters=True, + has_pagination=False, + ) def delete(self, *, thermostat_daily_program_id: str) -> None: """Deletes a thermostat daily program. :param thermostat_daily_program_id: ID of the thermostat daily program that you want to delete. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if thermostat_daily_program_id is not None: - json_payload["thermostat_daily_program_id"] = thermostat_daily_program_id + params["thermostat_daily_program_id"] = thermostat_daily_program_id + + if not params: + raise ValueError( + "At least one parameter is required for /thermostats/daily_programs/delete" + ) - self.client.post("/thermostats/daily_programs/delete", json=json_payload) + self.client.delete("/thermostats/daily_programs/delete", params=params) return None + @route_metadata( + path="/thermostats/daily_programs/update", + has_required_parameters=True, + has_pagination=False, + ) def update( self, *, name: str, periods: List[Dict[str, Any]], thermostat_daily_program_id: str, - wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None + wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None, ) -> ActionAttempt: """Updates a specified thermostat daily program. The periods that you specify overwrite any existing periods for the daily program. @@ -115,8 +149,10 @@ def update( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if name is not None: json_payload["name"] = name @@ -125,7 +161,12 @@ def update( if thermostat_daily_program_id is not None: json_payload["thermostat_daily_program_id"] = thermostat_daily_program_id - res = self.client.post("/thermostats/daily_programs/update", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/daily_programs/update" + ) + + res = self.client.patch("/thermostats/daily_programs/update", json=json_payload) wait_for_action_attempt = ( self.defaults.get("wait_for_action_attempt") diff --git a/seam/routes/thermostats_schedules.py b/seam/routes/thermostats_schedules.py index 24935b3e..7c4787ad 100644 --- a/seam/routes/thermostats_schedules.py +++ b/seam/routes/thermostats_schedules.py @@ -1,6 +1,8 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +from ..null import Null from ..resources import ThermostatSchedule @@ -15,8 +17,8 @@ def create( ends_at: str, starts_at: str, is_override_allowed: Optional[bool] = None, - max_override_period_minutes: Optional[int] = None, - name: Optional[str] = None + max_override_period_minutes: Optional[Union[int, Null]] = None, + name: Optional[str] = None, ) -> ThermostatSchedule: """Creates a new `thermostat schedule `_ for a specified `thermostat `_. @@ -34,7 +36,9 @@ def create( :param name: Name of the thermostat schedule. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -42,7 +46,8 @@ def delete(self, *, thermostat_schedule_id: str) -> None: """Deletes a `thermostat schedule `_ for a specified `thermostat `_. :param thermostat_schedule_id: ID of the thermostat schedule that you want to delete. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -51,7 +56,9 @@ def get(self, *, thermostat_schedule_id: str) -> ThermostatSchedule: :param thermostat_schedule_id: ID of the thermostat schedule that you want to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -64,7 +71,9 @@ def list( :param user_identifier_key: User identifier key by which to filter the list of returned thermostat schedules. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -75,9 +84,9 @@ def update( climate_preset_key: Optional[str] = None, ends_at: Optional[str] = None, is_override_allowed: Optional[bool] = None, - max_override_period_minutes: Optional[int] = None, + max_override_period_minutes: Optional[Union[int, Null]] = None, name: Optional[str] = None, - starts_at: Optional[str] = None + starts_at: Optional[str] = None, ) -> None: """Updates a specified `thermostat schedule `_. @@ -94,7 +103,8 @@ def update( :param name: Name of the thermostat schedule. :param starts_at: Date and time at which the thermostat schedule starts, in `ISO 8601 `_ format. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -103,6 +113,11 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/thermostats/schedules/create", + has_required_parameters=True, + has_pagination=False, + ) def create( self, *, @@ -111,8 +126,8 @@ def create( ends_at: str, starts_at: str, is_override_allowed: Optional[bool] = None, - max_override_period_minutes: Optional[int] = None, - name: Optional[str] = None + max_override_period_minutes: Optional[Union[int, Null]] = None, + name: Optional[str] = None, ) -> ThermostatSchedule: """Creates a new `thermostat schedule `_ for a specified `thermostat `_. @@ -130,8 +145,10 @@ def create( :param name: Name of the thermostat schedule. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if climate_preset_key is not None: json_payload["climate_preset_key"] = climate_preset_key @@ -148,39 +165,72 @@ def create( if name is not None: json_payload["name"] = name + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/schedules/create" + ) + res = self.client.post("/thermostats/schedules/create", json=json_payload) return ThermostatSchedule.from_dict(res["thermostat_schedule"]) + @route_metadata( + path="/thermostats/schedules/delete", + has_required_parameters=True, + has_pagination=False, + ) def delete(self, *, thermostat_schedule_id: str) -> None: """Deletes a `thermostat schedule `_ for a specified `thermostat `_. :param thermostat_schedule_id: ID of the thermostat schedule that you want to delete. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if thermostat_schedule_id is not None: - json_payload["thermostat_schedule_id"] = thermostat_schedule_id + params["thermostat_schedule_id"] = thermostat_schedule_id + + if not params: + raise ValueError( + "At least one parameter is required for /thermostats/schedules/delete" + ) - self.client.post("/thermostats/schedules/delete", json=json_payload) + self.client.delete("/thermostats/schedules/delete", params=params) return None + @route_metadata( + path="/thermostats/schedules/get", + has_required_parameters=True, + has_pagination=False, + ) def get(self, *, thermostat_schedule_id: str) -> ThermostatSchedule: """Returns a specified `thermostat schedule `_. :param thermostat_schedule_id: ID of the thermostat schedule that you want to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if thermostat_schedule_id is not None: - json_payload["thermostat_schedule_id"] = thermostat_schedule_id + params["thermostat_schedule_id"] = thermostat_schedule_id + + if not params: + raise ValueError( + "At least one parameter is required for /thermostats/schedules/get" + ) - res = self.client.post("/thermostats/schedules/get", json=json_payload) + res = self.client.get("/thermostats/schedules/get", params=params) return ThermostatSchedule.from_dict(res["thermostat_schedule"]) + @route_metadata( + path="/thermostats/schedules/list", + has_required_parameters=True, + has_pagination=False, + ) def list( self, *, device_id: str, user_identifier_key: Optional[str] = None ) -> List[ThermostatSchedule]: @@ -190,20 +240,32 @@ def list( :param user_identifier_key: User identifier key by which to filter the list of returned thermostat schedules. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if user_identifier_key is not None: - json_payload["user_identifier_key"] = user_identifier_key + params["user_identifier_key"] = user_identifier_key + + if not params: + raise ValueError( + "At least one parameter is required for /thermostats/schedules/list" + ) - res = self.client.post("/thermostats/schedules/list", json=json_payload) + res = self.client.get("/thermostats/schedules/list", params=params) return [ ThermostatSchedule.from_dict(item) for item in res["thermostat_schedules"] ] + @route_metadata( + path="/thermostats/schedules/update", + has_required_parameters=True, + has_pagination=False, + ) def update( self, *, @@ -211,9 +273,9 @@ def update( climate_preset_key: Optional[str] = None, ends_at: Optional[str] = None, is_override_allowed: Optional[bool] = None, - max_override_period_minutes: Optional[int] = None, + max_override_period_minutes: Optional[Union[int, Null]] = None, name: Optional[str] = None, - starts_at: Optional[str] = None + starts_at: Optional[str] = None, ) -> None: """Updates a specified `thermostat schedule `_. @@ -230,8 +292,9 @@ def update( :param name: Name of the thermostat schedule. :param starts_at: Date and time at which the thermostat schedule starts, in `ISO 8601 `_ format. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if thermostat_schedule_id is not None: json_payload["thermostat_schedule_id"] = thermostat_schedule_id @@ -248,6 +311,11 @@ def update( if starts_at is not None: json_payload["starts_at"] = starts_at - self.client.post("/thermostats/schedules/update", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/schedules/update" + ) + + self.client.patch("/thermostats/schedules/update", json=json_payload) return None diff --git a/seam/routes/thermostats_simulate.py b/seam/routes/thermostats_simulate.py index 94d2a1be..69dec843 100644 --- a/seam/routes/thermostats_simulate.py +++ b/seam/routes/thermostats_simulate.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata class AbstractThermostatsSimulate(abc.ABC): @@ -14,7 +15,7 @@ def hvac_mode_adjusted( cooling_set_point_celsius: Optional[float] = None, cooling_set_point_fahrenheit: Optional[float] = None, heating_set_point_celsius: Optional[float] = None, - heating_set_point_fahrenheit: Optional[float] = None + heating_set_point_fahrenheit: Optional[float] = None, ) -> None: """Simulates having adjusted the `HVAC mode `_ for a `thermostat `_. Only applicable for `sandbox devices `_. See also `Testing Your Thermostat App with Simulate Endpoints `_. @@ -29,7 +30,8 @@ def hvac_mode_adjusted( :param heating_set_point_celsius: Heating `set point `_ in °C that you want to simulate. You must set ``heating_set_point_celsius`` or ``heating_set_point_fahrenheit``. :param heating_set_point_fahrenheit: Heating `set point `_ in °F that you want to simulate. You must set ``heating_set_point_fahrenheit`` or ``heating_set_point_celsius``. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -38,7 +40,7 @@ def temperature_reached( *, device_id: str, temperature_celsius: Optional[float] = None, - temperature_fahrenheit: Optional[float] = None + temperature_fahrenheit: Optional[float] = None, ) -> None: """Simulates a `thermostat `_ reaching a specified temperature. Only applicable for `sandbox devices `_. See also `Testing Your Thermostat App with Simulate Endpoints `_. @@ -47,7 +49,8 @@ def temperature_reached( :param temperature_celsius: Temperature in °C that you want simulate the thermostat reaching. You must set ``temperature_celsius`` or ``temperature_fahrenheit``. :param temperature_fahrenheit: Temperature in °F that you want simulate the thermostat reaching. You must set ``temperature_fahrenheit`` or ``temperature_celsius``. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -56,6 +59,11 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/thermostats/simulate/hvac_mode_adjusted", + has_required_parameters=True, + has_pagination=False, + ) def hvac_mode_adjusted( self, *, @@ -64,7 +72,7 @@ def hvac_mode_adjusted( cooling_set_point_celsius: Optional[float] = None, cooling_set_point_fahrenheit: Optional[float] = None, heating_set_point_celsius: Optional[float] = None, - heating_set_point_fahrenheit: Optional[float] = None + heating_set_point_fahrenheit: Optional[float] = None, ) -> None: """Simulates having adjusted the `HVAC mode `_ for a `thermostat `_. Only applicable for `sandbox devices `_. See also `Testing Your Thermostat App with Simulate Endpoints `_. @@ -79,8 +87,9 @@ def hvac_mode_adjusted( :param heating_set_point_celsius: Heating `set point `_ in °C that you want to simulate. You must set ``heating_set_point_celsius`` or ``heating_set_point_fahrenheit``. :param heating_set_point_fahrenheit: Heating `set point `_ in °F that you want to simulate. You must set ``heating_set_point_fahrenheit`` or ``heating_set_point_celsius``. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id @@ -95,16 +104,26 @@ def hvac_mode_adjusted( if heating_set_point_fahrenheit is not None: json_payload["heating_set_point_fahrenheit"] = heating_set_point_fahrenheit + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/simulate/hvac_mode_adjusted" + ) + self.client.post("/thermostats/simulate/hvac_mode_adjusted", json=json_payload) return None + @route_metadata( + path="/thermostats/simulate/temperature_reached", + has_required_parameters=True, + has_pagination=False, + ) def temperature_reached( self, *, device_id: str, temperature_celsius: Optional[float] = None, - temperature_fahrenheit: Optional[float] = None + temperature_fahrenheit: Optional[float] = None, ) -> None: """Simulates a `thermostat `_ reaching a specified temperature. Only applicable for `sandbox devices `_. See also `Testing Your Thermostat App with Simulate Endpoints `_. @@ -113,8 +132,9 @@ def temperature_reached( :param temperature_celsius: Temperature in °C that you want simulate the thermostat reaching. You must set ``temperature_celsius`` or ``temperature_fahrenheit``. :param temperature_fahrenheit: Temperature in °F that you want simulate the thermostat reaching. You must set ``temperature_fahrenheit`` or ``temperature_celsius``. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id @@ -123,6 +143,11 @@ def temperature_reached( if temperature_fahrenheit is not None: json_payload["temperature_fahrenheit"] = temperature_fahrenheit + if not json_payload: + raise ValueError( + "At least one parameter is required for /thermostats/simulate/temperature_reached" + ) + self.client.post("/thermostats/simulate/temperature_reached", json=json_payload) return None diff --git a/seam/routes/user_identities.py b/seam/routes/user_identities.py index 819ddd91..a9107bc6 100644 --- a/seam/routes/user_identities.py +++ b/seam/routes/user_identities.py @@ -1,6 +1,8 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +from ..null import Null from ..resources import ( UserIdentity, InstantKey, @@ -28,7 +30,7 @@ def add_acs_user( *, acs_user_id: str, user_identity_id: Optional[str] = None, - user_identity_key: Optional[str] = None + user_identity_key: Optional[str] = None, ) -> None: """Adds a specified `access system user `_ to a specified `user identity `_. @@ -41,7 +43,8 @@ def add_acs_user( :param user_identity_id: ID of the user identity to which you want to add an access system user. :param user_identity_key: Key of the user identity to which you want to add an access system user. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -49,10 +52,10 @@ def create( self, *, acs_system_ids: Optional[List[str]] = None, - email_address: Optional[str] = None, - full_name: Optional[str] = None, - phone_number: Optional[str] = None, - user_identity_key: Optional[str] = None + email_address: Optional[Union[str, Null]] = None, + full_name: Optional[Union[str, Null]] = None, + phone_number: Optional[Union[str, Null]] = None, + user_identity_key: Optional[Union[str, Null]] = None, ) -> UserIdentity: """Creates a new `user identity `_. @@ -73,7 +76,9 @@ def create( def delete(self, *, user_identity_id: str) -> None: """Deletes a specified `user identity `_. This deletes the user identity and all associated resources, including any `credentials `_, `acs users `_ and `client sessions `_. - :param user_identity_id: ID of the user identity that you want to delete.""" + :param user_identity_id: ID of the user identity that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -82,7 +87,7 @@ def generate_instant_key( *, user_identity_id: str, customization_profile_id: Optional[str] = None, - max_use_count: Optional[float] = None + max_use_count: Optional[float] = None, ) -> InstantKey: """Generates a new `instant key `_ for a specified `user identity `_. @@ -92,7 +97,9 @@ def generate_instant_key( :param max_use_count: Maximum number of times the instant key can be used. Default: 1. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -100,7 +107,7 @@ def get( self, *, user_identity_id: Optional[str] = None, - user_identity_key: Optional[str] = None + user_identity_key: Optional[str] = None, ) -> UserIdentity: """Returns a specified `user identity `_. @@ -108,7 +115,9 @@ def get( :param user_identity_key: - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -118,7 +127,8 @@ def grant_access_to_device(self, *, device_id: str, user_identity_id: str) -> No :param device_id: ID of the managed device to which you want to grant access to the user identity. :param user_identity_id: ID of the user identity that you want to grant access to a device. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -128,9 +138,9 @@ def list( created_before: Optional[str] = None, credential_manager_acs_system_id: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, - user_identity_ids: Optional[List[str]] = None + user_identity_ids: Optional[List[str]] = None, ) -> List[UserIdentity]: """Returns a list of all `user identities `_. @@ -155,7 +165,9 @@ def list_accessible_devices(self, *, user_identity_id: str) -> List[Device]: :param user_identity_id: ID of the user identity for which you want to retrieve all accessible devices. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -164,7 +176,9 @@ def list_accessible_entrances(self, *, user_identity_id: str) -> List[AcsEntranc :param user_identity_id: ID of the user identity for which you want to retrieve all accessible entrances. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -173,7 +187,9 @@ def list_acs_systems(self, *, user_identity_id: str) -> List[AcsSystem]: :param user_identity_id: ID of the user identity for which you want to retrieve all access systems. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -182,7 +198,9 @@ def list_acs_users(self, *, user_identity_id: str) -> List[AcsUser]: :param user_identity_id: ID of the user identity for which you want to retrieve all access system users. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -192,7 +210,8 @@ def remove_acs_user(self, *, acs_user_id: str, user_identity_id: str) -> None: :param acs_user_id: ID of the access system user that you want to remove from the user identity.. :param user_identity_id: ID of the user identity from which you want to remove an access system user. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -202,7 +221,8 @@ def revoke_access_to_device(self, *, device_id: str, user_identity_id: str) -> N :param device_id: ID of the managed device to which you want to revoke access from the user identity. :param user_identity_id: ID of the user identity from which you want to revoke access to a device. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -210,10 +230,10 @@ def update( self, *, user_identity_id: str, - email_address: Optional[str] = None, - full_name: Optional[str] = None, - phone_number: Optional[str] = None, - user_identity_key: Optional[str] = None + email_address: Optional[Union[str, Null]] = None, + full_name: Optional[Union[str, Null]] = None, + phone_number: Optional[Union[str, Null]] = None, + user_identity_key: Optional[Union[str, Null]] = None, ) -> None: """Updates a specified `user identity `_. @@ -225,7 +245,9 @@ def update( :param phone_number: Unique phone number for the user identity. - :param user_identity_key: Unique key for the user identity.""" + :param user_identity_key: Unique key for the user identity. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -239,12 +261,17 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): def unmanaged(self) -> UserIdentitiesUnmanaged: return self._unmanaged + @route_metadata( + path="/user_identities/add_acs_user", + has_required_parameters=True, + has_pagination=False, + ) def add_acs_user( self, *, acs_user_id: str, user_identity_id: Optional[str] = None, - user_identity_key: Optional[str] = None + user_identity_key: Optional[str] = None, ) -> None: """Adds a specified `access system user `_ to a specified `user identity `_. @@ -257,8 +284,9 @@ def add_acs_user( :param user_identity_id: ID of the user identity to which you want to add an access system user. :param user_identity_key: Key of the user identity to which you want to add an access system user. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if acs_user_id is not None: json_payload["acs_user_id"] = acs_user_id @@ -267,18 +295,28 @@ def add_acs_user( if user_identity_key is not None: json_payload["user_identity_key"] = user_identity_key - self.client.post("/user_identities/add_acs_user", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /user_identities/add_acs_user" + ) + + self.client.put("/user_identities/add_acs_user", json=json_payload) return None + @route_metadata( + path="/user_identities/create", + has_required_parameters=False, + has_pagination=False, + ) def create( self, *, acs_system_ids: Optional[List[str]] = None, - email_address: Optional[str] = None, - full_name: Optional[str] = None, - phone_number: Optional[str] = None, - user_identity_key: Optional[str] = None + email_address: Optional[Union[str, Null]] = None, + full_name: Optional[Union[str, Null]] = None, + phone_number: Optional[Union[str, Null]] = None, + user_identity_key: Optional[Union[str, Null]] = None, ) -> UserIdentity: """Creates a new `user identity `_. @@ -293,7 +331,7 @@ def create( :param user_identity_key: Unique key for the new user identity. :returns: OK""" - json_payload = {} + json_payload: Dict[str, Any] = {} if acs_system_ids is not None: json_payload["acs_system_ids"] = acs_system_ids @@ -310,25 +348,42 @@ def create( return UserIdentity.from_dict(res["user_identity"]) + @route_metadata( + path="/user_identities/delete", + has_required_parameters=True, + has_pagination=False, + ) def delete(self, *, user_identity_id: str) -> None: """Deletes a specified `user identity `_. This deletes the user identity and all associated resources, including any `credentials `_, `acs users `_ and `client sessions `_. - :param user_identity_id: ID of the user identity that you want to delete.""" - json_payload = {} + :param user_identity_id: ID of the user identity that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id - self.client.post("/user_identities/delete", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /user_identities/delete" + ) + + self.client.delete("/user_identities/delete", params=params) return None + @route_metadata( + path="/user_identities/generate_instant_key", + has_required_parameters=True, + has_pagination=False, + ) def generate_instant_key( self, *, user_identity_id: str, customization_profile_id: Optional[str] = None, - max_use_count: Optional[float] = None + max_use_count: Optional[float] = None, ) -> InstantKey: """Generates a new `instant key `_ for a specified `user identity `_. @@ -338,8 +393,10 @@ def generate_instant_key( :param max_use_count: Maximum number of times the instant key can be used. Default: 1. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id @@ -348,17 +405,25 @@ def generate_instant_key( if max_use_count is not None: json_payload["max_use_count"] = max_use_count + if not json_payload: + raise ValueError( + "At least one parameter is required for /user_identities/generate_instant_key" + ) + res = self.client.post( "/user_identities/generate_instant_key", json=json_payload ) return InstantKey.from_dict(res["instant_key"]) + @route_metadata( + path="/user_identities/get", has_required_parameters=True, has_pagination=False + ) def get( self, *, user_identity_id: Optional[str] = None, - user_identity_key: Optional[str] = None + user_identity_key: Optional[str] = None, ) -> UserIdentity: """Returns a specified `user identity `_. @@ -366,45 +431,66 @@ def get( :param user_identity_key: - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id if user_identity_key is not None: - json_payload["user_identity_key"] = user_identity_key + params["user_identity_key"] = user_identity_key - res = self.client.post("/user_identities/get", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /user_identities/get" + ) + + res = self.client.get("/user_identities/get", params=params) return UserIdentity.from_dict(res["user_identity"]) + @route_metadata( + path="/user_identities/grant_access_to_device", + has_required_parameters=True, + has_pagination=False, + ) def grant_access_to_device(self, *, device_id: str, user_identity_id: str) -> None: """Grants a specified `user identity `_ access to a specified `device `_. :param device_id: ID of the managed device to which you want to grant access to the user identity. :param user_identity_id: ID of the user identity that you want to grant access to a device. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if device_id is not None: json_payload["device_id"] = device_id if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id - self.client.post("/user_identities/grant_access_to_device", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /user_identities/grant_access_to_device" + ) + + self.client.put("/user_identities/grant_access_to_device", json=json_payload) return None + @route_metadata( + path="/user_identities/list", has_required_parameters=False, has_pagination=True + ) def list( self, *, created_before: Optional[str] = None, credential_manager_acs_system_id: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = None, + page_cursor: Optional[Union[str, Null]] = None, search: Optional[str] = None, - user_identity_ids: Optional[List[str]] = None + user_identity_ids: Optional[List[str]] = None, ) -> List[UserIdentity]: """Returns a list of all `user identities `_. @@ -421,7 +507,7 @@ def list( :param user_identity_ids: Array of user identity IDs by which to filter the list of user identities. :returns: OK""" - json_payload = {} + json_payload: Dict[str, Any] = {} if created_before is not None: json_payload["created_before"] = created_before @@ -442,114 +528,187 @@ def list( return [UserIdentity.from_dict(item) for item in res["user_identities"]] + @route_metadata( + path="/user_identities/list_accessible_devices", + has_required_parameters=True, + has_pagination=False, + ) def list_accessible_devices(self, *, user_identity_id: str) -> List[Device]: """Returns a list of all `devices `_ associated with a specified `user identity `_. This includes devices derived from the access grants assigned to the user identity and devices directly linked to the user identity. :param user_identity_id: ID of the user identity for which you want to retrieve all accessible devices. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id - res = self.client.post( - "/user_identities/list_accessible_devices", json=json_payload - ) + if not params: + raise ValueError( + "At least one parameter is required for /user_identities/list_accessible_devices" + ) + + res = self.client.get("/user_identities/list_accessible_devices", params=params) return [Device.from_dict(item) for item in res["devices"]] + @route_metadata( + path="/user_identities/list_accessible_entrances", + has_required_parameters=True, + has_pagination=False, + ) def list_accessible_entrances(self, *, user_identity_id: str) -> List[AcsEntrance]: """Returns a list of all `ACS entrances `_ accessible to a specified `user identity `_. This includes entrances derived from the access grants assigned to the user identity and entrances accessible through ACS users linked to the user identity. :param user_identity_id: ID of the user identity for which you want to retrieve all accessible entrances. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id - res = self.client.post( - "/user_identities/list_accessible_entrances", json=json_payload + if not params: + raise ValueError( + "At least one parameter is required for /user_identities/list_accessible_entrances" + ) + + res = self.client.get( + "/user_identities/list_accessible_entrances", params=params ) return [AcsEntrance.from_dict(item) for item in res["acs_entrances"]] + @route_metadata( + path="/user_identities/list_acs_systems", + has_required_parameters=True, + has_pagination=False, + ) def list_acs_systems(self, *, user_identity_id: str) -> List[AcsSystem]: """Returns a list of all `access systems `_ associated with a specified `user identity `_. :param user_identity_id: ID of the user identity for which you want to retrieve all access systems. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError( + "At least one parameter is required for /user_identities/list_acs_systems" + ) - res = self.client.post("/user_identities/list_acs_systems", json=json_payload) + res = self.client.get("/user_identities/list_acs_systems", params=params) return [AcsSystem.from_dict(item) for item in res["acs_systems"]] + @route_metadata( + path="/user_identities/list_acs_users", + has_required_parameters=True, + has_pagination=False, + ) def list_acs_users(self, *, user_identity_id: str) -> List[AcsUser]: """Returns a list of all `access system users `_ assigned to a specified `user identity `_. :param user_identity_id: ID of the user identity for which you want to retrieve all access system users. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError( + "At least one parameter is required for /user_identities/list_acs_users" + ) - res = self.client.post("/user_identities/list_acs_users", json=json_payload) + res = self.client.get("/user_identities/list_acs_users", params=params) return [AcsUser.from_dict(item) for item in res["acs_users"]] + @route_metadata( + path="/user_identities/remove_acs_user", + has_required_parameters=True, + has_pagination=False, + ) def remove_acs_user(self, *, acs_user_id: str, user_identity_id: str) -> None: """Removes a specified `access system user `_ from a specified `user identity `_. :param acs_user_id: ID of the access system user that you want to remove from the user identity.. :param user_identity_id: ID of the user identity from which you want to remove an access system user. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if acs_user_id is not None: - json_payload["acs_user_id"] = acs_user_id + params["acs_user_id"] = acs_user_id if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError( + "At least one parameter is required for /user_identities/remove_acs_user" + ) - self.client.post("/user_identities/remove_acs_user", json=json_payload) + self.client.delete("/user_identities/remove_acs_user", params=params) return None + @route_metadata( + path="/user_identities/revoke_access_to_device", + has_required_parameters=True, + has_pagination=False, + ) def revoke_access_to_device(self, *, device_id: str, user_identity_id: str) -> None: """Revokes access to a specified `device `_ from a specified `user identity `_. :param device_id: ID of the managed device to which you want to revoke access from the user identity. :param user_identity_id: ID of the user identity from which you want to revoke access to a device. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if device_id is not None: - json_payload["device_id"] = device_id + params["device_id"] = device_id if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id - self.client.post("/user_identities/revoke_access_to_device", json=json_payload) + if not params: + raise ValueError( + "At least one parameter is required for /user_identities/revoke_access_to_device" + ) + + self.client.delete("/user_identities/revoke_access_to_device", params=params) return None + @route_metadata( + path="/user_identities/update", + has_required_parameters=True, + has_pagination=False, + ) def update( self, *, user_identity_id: str, - email_address: Optional[str] = None, - full_name: Optional[str] = None, - phone_number: Optional[str] = None, - user_identity_key: Optional[str] = None + email_address: Optional[Union[str, Null]] = None, + full_name: Optional[Union[str, Null]] = None, + phone_number: Optional[Union[str, Null]] = None, + user_identity_key: Optional[Union[str, Null]] = None, ) -> None: """Updates a specified `user identity `_. @@ -561,8 +720,10 @@ def update( :param phone_number: Unique phone number for the user identity. - :param user_identity_key: Unique key for the user identity.""" - json_payload = {} + :param user_identity_key: Unique key for the user identity. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if user_identity_id is not None: json_payload["user_identity_id"] = user_identity_id @@ -575,6 +736,11 @@ def update( if user_identity_key is not None: json_payload["user_identity_key"] = user_identity_key - self.client.post("/user_identities/update", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /user_identities/update" + ) + + self.client.patch("/user_identities/update", json=json_payload) return None diff --git a/seam/routes/user_identities_unmanaged.py b/seam/routes/user_identities_unmanaged.py index bc5df14a..b177177a 100644 --- a/seam/routes/user_identities_unmanaged.py +++ b/seam/routes/user_identities_unmanaged.py @@ -1,6 +1,8 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +from ..null import Null from ..resources import UnmanagedUserIdentity @@ -12,7 +14,9 @@ def get(self, *, user_identity_id: str) -> UnmanagedUserIdentity: :param user_identity_id: ID of the unmanaged user identity that you want to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -21,8 +25,8 @@ def list( *, created_before: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = None, - search: Optional[str] = None + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, ) -> List[UnmanagedUserIdentity]: """Returns a list of all unmanaged `user identities `_ (where is_managed = false). @@ -43,7 +47,7 @@ def update( *, is_managed: bool, user_identity_id: str, - user_identity_key: Optional[str] = None + user_identity_key: Optional[str] = None, ) -> None: """Updates an unmanaged `user identity `_ to make it managed. @@ -54,7 +58,8 @@ def update( :param user_identity_id: ID of the unmanaged user identity that you want to update. :param user_identity_key: Unique key for the user identity. If not provided, the existing key will be preserved. - """ + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -63,28 +68,45 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/user_identities/unmanaged/get", + has_required_parameters=True, + has_pagination=False, + ) def get(self, *, user_identity_id: str) -> UnmanagedUserIdentity: """Returns a specified unmanaged `user identity `_ (where is_managed = false). :param user_identity_id: ID of the unmanaged user identity that you want to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if user_identity_id is not None: - json_payload["user_identity_id"] = user_identity_id + params["user_identity_id"] = user_identity_id + + if not params: + raise ValueError( + "At least one parameter is required for /user_identities/unmanaged/get" + ) - res = self.client.post("/user_identities/unmanaged/get", json=json_payload) + res = self.client.get("/user_identities/unmanaged/get", params=params) return UnmanagedUserIdentity.from_dict(res["user_identity"]) + @route_metadata( + path="/user_identities/unmanaged/list", + has_required_parameters=False, + has_pagination=True, + ) def list( self, *, created_before: Optional[str] = None, limit: Optional[int] = None, - page_cursor: Optional[str] = None, - search: Optional[str] = None + page_cursor: Optional[Union[str, Null]] = None, + search: Optional[str] = None, ) -> List[UnmanagedUserIdentity]: """Returns a list of all unmanaged `user identities `_ (where is_managed = false). @@ -97,29 +119,34 @@ def list( :param search: String for which to search. Filters returned unmanaged user identities to include all records that satisfy a partial match using ``full_name``, ``phone_number``, ``email_address``, ``user_identity_id`` or ``acs_system_id``. :returns: OK""" - json_payload = {} + params: Dict[str, Any] = {} if created_before is not None: - json_payload["created_before"] = created_before + params["created_before"] = created_before if limit is not None: - json_payload["limit"] = limit + params["limit"] = limit if page_cursor is not None: - json_payload["page_cursor"] = page_cursor + params["page_cursor"] = page_cursor if search is not None: - json_payload["search"] = search + params["search"] = search - res = self.client.post("/user_identities/unmanaged/list", json=json_payload) + res = self.client.get("/user_identities/unmanaged/list", params=params) return [ UnmanagedUserIdentity.from_dict(item) for item in res["user_identities"] ] + @route_metadata( + path="/user_identities/unmanaged/update", + has_required_parameters=True, + has_pagination=False, + ) def update( self, *, is_managed: bool, user_identity_id: str, - user_identity_key: Optional[str] = None + user_identity_key: Optional[str] = None, ) -> None: """Updates an unmanaged `user identity `_ to make it managed. @@ -130,8 +157,9 @@ def update( :param user_identity_id: ID of the unmanaged user identity that you want to update. :param user_identity_key: Unique key for the user identity. If not provided, the existing key will be preserved. - """ - json_payload = {} + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if is_managed is not None: json_payload["is_managed"] = is_managed @@ -140,6 +168,11 @@ def update( if user_identity_key is not None: json_payload["user_identity_key"] = user_identity_key - self.client.post("/user_identities/unmanaged/update", json=json_payload) + if not json_payload: + raise ValueError( + "At least one parameter is required for /user_identities/unmanaged/update" + ) + + self.client.patch("/user_identities/unmanaged/update", json=json_payload) return None diff --git a/seam/routes/webhooks.py b/seam/routes/webhooks.py index 444bed45..8aa22a82 100644 --- a/seam/routes/webhooks.py +++ b/seam/routes/webhooks.py @@ -1,6 +1,7 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata from ..resources import Webhook @@ -14,14 +15,18 @@ def create(self, *, url: str, event_types: Optional[List[str]] = None) -> Webhoo :param event_types: Types of events that you want the new webhook to receive. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod def delete(self, *, webhook_id: str) -> None: """Deletes a specified `webhook `_. - :param webhook_id: ID of the webhook that you want to delete.""" + :param webhook_id: ID of the webhook that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -30,7 +35,9 @@ def get(self, *, webhook_id: str) -> Webhook: :param webhook_id: ID of the webhook that you want to get. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -46,7 +53,9 @@ def update(self, *, event_types: List[str], webhook_id: str) -> None: :param event_types: Types of events that you want the webhook to receive. - :param webhook_id: ID of the webhook that you want to update.""" + :param webhook_id: ID of the webhook that you want to update. + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @@ -55,6 +64,9 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/webhooks/create", has_required_parameters=True, has_pagination=False + ) def create(self, *, url: str, event_types: Optional[List[str]] = None) -> Webhook: """Creates a new `webhook `_. @@ -62,69 +74,101 @@ def create(self, *, url: str, event_types: Optional[List[str]] = None) -> Webhoo :param event_types: Types of events that you want the new webhook to receive. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if url is not None: json_payload["url"] = url if event_types is not None: json_payload["event_types"] = event_types + if not json_payload: + raise ValueError("At least one parameter is required for /webhooks/create") + res = self.client.post("/webhooks/create", json=json_payload) return Webhook.from_dict(res["webhook"]) + @route_metadata( + path="/webhooks/delete", has_required_parameters=True, has_pagination=False + ) def delete(self, *, webhook_id: str) -> None: """Deletes a specified `webhook `_. - :param webhook_id: ID of the webhook that you want to delete.""" - json_payload = {} + :param webhook_id: ID of the webhook that you want to delete. + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if webhook_id is not None: - json_payload["webhook_id"] = webhook_id + params["webhook_id"] = webhook_id + + if not params: + raise ValueError("At least one parameter is required for /webhooks/delete") - self.client.post("/webhooks/delete", json=json_payload) + self.client.delete("/webhooks/delete", params=params) return None + @route_metadata( + path="/webhooks/get", has_required_parameters=True, has_pagination=False + ) def get(self, *, webhook_id: str) -> Webhook: """Gets a specified `webhook `_. :param webhook_id: ID of the webhook that you want to get. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + params: Dict[str, Any] = {} if webhook_id is not None: - json_payload["webhook_id"] = webhook_id + params["webhook_id"] = webhook_id + + if not params: + raise ValueError("At least one parameter is required for /webhooks/get") - res = self.client.post("/webhooks/get", json=json_payload) + res = self.client.get("/webhooks/get", params=params) return Webhook.from_dict(res["webhook"]) + @route_metadata( + path="/webhooks/list", has_required_parameters=False, has_pagination=False + ) def list(self) -> List[Webhook]: """Returns a list of all `webhooks `_. :returns: OK""" - json_payload = {} + params: Dict[str, Any] = {} - res = self.client.post("/webhooks/list", json=json_payload) + res = self.client.get("/webhooks/list", params=params) return [Webhook.from_dict(item) for item in res["webhooks"]] + @route_metadata( + path="/webhooks/update", has_required_parameters=True, has_pagination=False + ) def update(self, *, event_types: List[str], webhook_id: str) -> None: """Updates a specified `webhook `_. :param event_types: Types of events that you want the webhook to receive. - :param webhook_id: ID of the webhook that you want to update.""" - json_payload = {} + :param webhook_id: ID of the webhook that you want to update. + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if event_types is not None: json_payload["event_types"] = event_types if webhook_id is not None: json_payload["webhook_id"] = webhook_id - self.client.post("/webhooks/update", json=json_payload) + if not json_payload: + raise ValueError("At least one parameter is required for /webhooks/update") + + self.client.put("/webhooks/update", json=json_payload) return None diff --git a/seam/routes/workspaces.py b/seam/routes/workspaces.py index 9b9b9195..01ee6655 100644 --- a/seam/routes/workspaces.py +++ b/seam/routes/workspaces.py @@ -1,6 +1,8 @@ from typing import Optional, Any, List, Dict, Union import abc from ..client import SeamHttpClient +from ..route import route_metadata +from ..null import Null from ..resources import Workspace, ActionAttempt from ..modules.action_attempts import resolve_action_attempt @@ -13,14 +15,14 @@ def create( *, name: str, company_name: Optional[str] = None, - connect_partner_name: Optional[str] = None, + connect_partner_name: Optional[Union[str, Null]] = None, connect_webview_customization: Optional[Dict[str, Any]] = None, is_sandbox: Optional[bool] = None, organization_id: Optional[str] = None, webview_logo_shape: Optional[str] = None, webview_primary_button_color: Optional[str] = None, webview_primary_button_text_color: Optional[str] = None, - webview_success_message: Optional[str] = None + webview_success_message: Optional[str] = None, ) -> Workspace: """Creates a new `workspace `_. @@ -44,7 +46,9 @@ def create( :param webview_success_message: Deprecated: Use ``connect_webview_customization.webview_success_message`` instead. - :returns: OK""" + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" raise NotImplementedError() @abc.abstractmethod @@ -81,7 +85,7 @@ def update( is_publishable_key_auth_enabled: Optional[bool] = None, is_suspended: Optional[bool] = None, name: Optional[str] = None, - organization_id: Optional[str] = None + organization_id: Optional[str] = None, ) -> None: """Updates the `workspace `_ associated with the authentication value. @@ -105,19 +109,22 @@ def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]): self.client = client self.defaults = defaults + @route_metadata( + path="/workspaces/create", has_required_parameters=True, has_pagination=False + ) def create( self, *, name: str, company_name: Optional[str] = None, - connect_partner_name: Optional[str] = None, + connect_partner_name: Optional[Union[str, Null]] = None, connect_webview_customization: Optional[Dict[str, Any]] = None, is_sandbox: Optional[bool] = None, organization_id: Optional[str] = None, webview_logo_shape: Optional[str] = None, webview_primary_button_color: Optional[str] = None, webview_primary_button_text_color: Optional[str] = None, - webview_success_message: Optional[str] = None + webview_success_message: Optional[str] = None, ) -> Workspace: """Creates a new `workspace `_. @@ -141,8 +148,10 @@ def create( :param webview_success_message: Deprecated: Use ``connect_webview_customization.webview_success_message`` instead. - :returns: OK""" - json_payload = {} + :returns: OK + + :raises ValueError: At least one parameter must be provided.""" + json_payload: Dict[str, Any] = {} if name is not None: json_payload["name"] = name @@ -169,30 +178,46 @@ def create( if webview_success_message is not None: json_payload["webview_success_message"] = webview_success_message + if not json_payload: + raise ValueError( + "At least one parameter is required for /workspaces/create" + ) + res = self.client.post("/workspaces/create", json=json_payload) return Workspace.from_dict(res["workspace"]) + @route_metadata( + path="/workspaces/get", has_required_parameters=False, has_pagination=False + ) def get(self) -> Workspace: """Returns the `workspace `_ associated with the authentication value. :returns: OK""" - json_payload = {} + params: Dict[str, Any] = {} - res = self.client.post("/workspaces/get", json=json_payload) + res = self.client.get("/workspaces/get", params=params) return Workspace.from_dict(res["workspace"]) + @route_metadata( + path="/workspaces/list", has_required_parameters=False, has_pagination=False + ) def list(self) -> List[Workspace]: """Returns a list of `workspaces `_ associated with the authentication value. :returns: OK""" - json_payload = {} + params: Dict[str, Any] = {} - res = self.client.post("/workspaces/list", json=json_payload) + res = self.client.get("/workspaces/list", params=params) return [Workspace.from_dict(item) for item in res["workspaces"]] + @route_metadata( + path="/workspaces/reset_sandbox", + has_required_parameters=False, + has_pagination=False, + ) def reset_sandbox( self, *, wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = None ) -> ActionAttempt: @@ -201,7 +226,7 @@ def reset_sandbox( :param wait_for_action_attempt: Whether, and for how long, to wait for the action attempt to finish. :returns: OK""" - json_payload = {} + json_payload: Dict[str, Any] = {} res = self.client.post("/workspaces/reset_sandbox", json=json_payload) @@ -217,6 +242,9 @@ def reset_sandbox( wait_for_action_attempt=wait_for_action_attempt, ) + @route_metadata( + path="/workspaces/update", has_required_parameters=False, has_pagination=False + ) def update( self, *, @@ -225,7 +253,7 @@ def update( is_publishable_key_auth_enabled: Optional[bool] = None, is_suspended: Optional[bool] = None, name: Optional[str] = None, - organization_id: Optional[str] = None + organization_id: Optional[str] = None, ) -> None: """Updates the `workspace `_ associated with the authentication value. @@ -241,7 +269,7 @@ def update( :param organization_id: ID of the organization to assign the workspace to. The authenticated user must be the owner of the workspace and an admin of the target organization. """ - json_payload = {} + json_payload: Dict[str, Any] = {} if connect_partner_name is not None: json_payload["connect_partner_name"] = connect_partner_name @@ -260,6 +288,6 @@ def update( if organization_id is not None: json_payload["organization_id"] = organization_id - self.client.post("/workspaces/update", json=json_payload) + self.client.patch("/workspaces/update", json=json_payload) return None diff --git a/seam/seam.py b/seam/seam.py index 97442403..7a42d895 100644 --- a/seam/seam.py +++ b/seam/seam.py @@ -1,8 +1,8 @@ from typing import Any, Optional, Union, Dict, Callable from typing_extensions import Self -from urllib3.util.retry import Retry +from httpx_retries import Retry -from .constants import DEFAULT_TIMEOUT, LTS_VERSION +from .constants import DEFAULT_TIMEOUT from .parse_options import parse_options from .routes import Routes from .models import AbstractSeam @@ -17,9 +17,6 @@ class Seam(AbstractSeam): Seam API endpoints, including devices, access codes, action_attempts, and more. It supports authentication via API key or personal access token. - :cvar lts_version: The long-term support (LTS) version of the Seam - Python SDK - :vartype lts_version: str :ivar defaults: Default settings for API requests :vartype defaults: Dict[str, Any] :ivar client: The HTTP client used for making API requests @@ -31,8 +28,6 @@ class Seam(AbstractSeam): For more information about the Seam API, visit https://docs.seam.co/ """ - lts_version: str = LTS_VERSION - def __init__( self, api_key: Optional[str] = None, @@ -43,7 +38,7 @@ def __init__( wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = True, retries: Optional[Retry] = None, timeout: Optional[float] = DEFAULT_TIMEOUT, - niquests_options: Optional[Dict[str, Any]] = None, + httpx_options: Optional[Dict[str, Any]] = None, ): """Initialize a Seam client instance. @@ -71,13 +66,13 @@ def __init__( 'timeout' and 'poll_interval' keys :type wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] :param retries: Configuration for retry behavior on failed requests - :type retries: Optional[urllib3.util.Retry] + :type retries: Optional[httpx_retries.Retry] :param timeout: The request timeout in seconds. Defaults to 30 seconds. Pass None for no timeout :type timeout: Optional[float] - :param niquests_options: Options passed through to the underlying - niquests Session, for control the other options do not cover - :type niquests_options: Optional[Dict[str, Any]] + :param httpx_options: Options passed through to the underlying + httpx Client, for control the other options do not cover + :type httpx_options: Optional[Dict[str, Any]] :raises SeamInvalidOptionsError: If neither api_key nor personal_access_token is provided, or if workspace_id is missing @@ -86,7 +81,6 @@ def __init__( access token format is invalid """ - self.lts_version = Seam.lts_version self.wait_for_action_attempt = wait_for_action_attempt auth_headers, endpoint = parse_options( api_key=api_key, @@ -101,10 +95,13 @@ def __init__( auth_headers=auth_headers, retries=retries, timeout=timeout, - niquests_options=niquests_options, + httpx_options=httpx_options, ) - Routes.__init__(self, client=self.client, defaults=self.defaults) + # Seam and Routes are siblings under AbstractRoutes rather than parent + # and child, so borrowing this initializer to attach the route + # namespaces passes a self the signature does not admit. + Routes.__init__(self, client=self.client, defaults=self.defaults) # type: ignore[arg-type] def create_paginator( self, request: Callable, params: Optional[Dict[str, Any]] = None, / @@ -129,6 +126,18 @@ def create_paginator( >>> for connected_account in connected_accounts_paginator.flatten(): >>> print(connected_account.account_type_display_name) """ + if not getattr(request, "__seam_has_pagination__", False): + raise ValueError("Cannot create a paginator for a non-paginated endpoint") + + has_required_parameters = getattr( + request, "__seam_has_required_parameters__", False + ) + if has_required_parameters and ( + not params or not any(value is not None for value in params.values()) + ): + path = getattr(request, "__seam_path__", "this endpoint") + raise ValueError(f"At least one parameter is required for {path}") + return SeamPaginator(self.client, request, params) @classmethod @@ -140,7 +149,7 @@ def from_api_key( wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = True, retries: Optional[Retry] = None, timeout: Optional[float] = DEFAULT_TIMEOUT, - niquests_options: Optional[Dict[str, Any]] = None, + httpx_options: Optional[Dict[str, Any]] = None, ) -> Self: """Create a Seam instance using an API key. @@ -170,7 +179,7 @@ def from_api_key( wait_for_action_attempt=wait_for_action_attempt, retries=retries, timeout=timeout, - niquests_options=niquests_options, + httpx_options=httpx_options, ) @classmethod @@ -183,7 +192,7 @@ def from_personal_access_token( wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = True, retries: Optional[Retry] = None, timeout: Optional[float] = DEFAULT_TIMEOUT, - niquests_options: Optional[Dict[str, Any]] = None, + httpx_options: Optional[Dict[str, Any]] = None, ) -> Self: """Create a Seam instance using a personal access token. @@ -217,5 +226,5 @@ def from_personal_access_token( wait_for_action_attempt=wait_for_action_attempt, retries=retries, timeout=timeout, - niquests_options=niquests_options, + httpx_options=httpx_options, ) diff --git a/seam/seam_without_workspace.py b/seam/seam_without_workspace.py index c413ab25..4a38a9bf 100644 --- a/seam/seam_without_workspace.py +++ b/seam/seam_without_workspace.py @@ -1,9 +1,8 @@ from typing import Any, Dict, Optional, Union -import niquests as requests from typing_extensions import Self -from urllib3.util import Retry +from httpx_retries import Retry -from .constants import DEFAULT_TIMEOUT, LTS_VERSION +from .constants import DEFAULT_TIMEOUT from .parse_options import parse_without_workspace_options from .client import SeamHttpClient from .models import AbstractSeamWithoutWorkspace @@ -30,9 +29,6 @@ class SeamWithoutWorkspace(AbstractSeamWithoutWorkspace): This class provides methods to authenticate and interact with Seam API endpoints that can operate without being tied to a specific workspace. It supports operations such as creating and listing workspaces. - :cvar lts_version: The long-term support (LTS) version of the Seam - Python SDK - :vartype lts_version: str :ivar wait_for_action_attempt: Controls whether to wait for an action attempt to complete :vartype wait_for_action_attempt: Union[bool, Dict[str, float]] @@ -42,8 +38,6 @@ class SeamWithoutWorkspace(AbstractSeamWithoutWorkspace): :vartype workspaces: WorkspacesProxy """ - lts_version: str = LTS_VERSION - def __init__( self, personal_access_token: Optional[str] = None, @@ -52,7 +46,7 @@ def __init__( wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = True, retries: Optional[Retry] = None, timeout: Optional[float] = DEFAULT_TIMEOUT, - niquests_options: Optional[Dict[str, Any]] = None, + httpx_options: Optional[Dict[str, Any]] = None, ): """ Initialize a SeamWithoutWorkspace client instance. @@ -72,20 +66,19 @@ def __init__( 'timeout' and 'poll_interval' keys :type wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] :param retries: Configuration for retry behavior on failed requests - :type retries: Optional[urllib3.util.Retry] + :type retries: Optional[httpx_retries.Retry] :param timeout: The request timeout in seconds. Defaults to 30 seconds. Pass None for no timeout :type timeout: Optional[float] - :param niquests_options: Options passed through to the underlying - niquests Session, for control the other options do not cover - :type niquests_options: Optional[Dict[str, Any]] + :param httpx_options: Options passed through to the underlying + httpx Client, for control the other options do not cover + :type httpx_options: Optional[Dict[str, Any]] :raises SeamInvalidOptionsError: If no personal_access_token is provided and the SEAM_PERSONAL_ACCESS_TOKEN environment variable is not set :raises SeamInvalidTokenError: If the provided personal access token format is invalid """ - self.lts_version = SeamWithoutWorkspace.lts_version self.wait_for_action_attempt = wait_for_action_attempt auth_headers, endpoint = parse_without_workspace_options( personal_access_token=personal_access_token, @@ -97,7 +90,7 @@ def __init__( auth_headers=auth_headers, retries=retries, timeout=timeout, - niquests_options=niquests_options, + httpx_options=httpx_options, ) defaults = {"wait_for_action_attempt": wait_for_action_attempt} @@ -114,7 +107,7 @@ def from_personal_access_token( wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] = True, retries: Optional[Retry] = None, timeout: Optional[float] = DEFAULT_TIMEOUT, - niquests_options: Optional[Dict[str, Any]] = None, + httpx_options: Optional[Dict[str, Any]] = None, ) -> Self: """ Create a SeamWithoutWorkspace instance using a personal access token. @@ -131,7 +124,7 @@ def from_personal_access_token( 'timeout' and 'poll_interval' keys :type wait_for_action_attempt: Optional[Union[bool, Dict[str, float]]] :param retries: Configuration for retry behavior on failed requests - :type retries: Optional[urllib3.util.Retry] + :type retries: Optional[httpx_retries.Retry] :return: A new instance of the SeamWithoutWorkspace class authenticated with the provided personal access token :rtype: Self @@ -147,5 +140,5 @@ def from_personal_access_token( wait_for_action_attempt=wait_for_action_attempt, retries=retries, timeout=timeout, - niquests_options=niquests_options, + httpx_options=httpx_options, ) diff --git a/seam/url_search_params_serializer.py b/seam/url_search_params_serializer.py new file mode 100644 index 00000000..973fc3df --- /dev/null +++ b/seam/url_search_params_serializer.py @@ -0,0 +1,435 @@ +"""Serializes Python objects to URL search params. + +This is a Python port of the `@seamapi/url-search-params-serializer +`_ reference +implementation, which defines the standard for how the Seam SDKs and other +Seam API consumers serialize objects to URL search params in HTTP GET requests. + +Output is byte-for-byte identical to the reference implementation: +values are encoded with the ``application/x-www-form-urlencoded`` serializer, +params are sorted by name, and numbers are formatted using the +ECMAScript ``Number::toString`` algorithm. + +Type mapping between the reference implementation and this port: + +- JavaScript ``undefined`` is ``None``, or simply an absent key. +- JavaScript ``null`` is :data:`seam.NULL `. + Python has a single absence value, so ``None`` means the safe option of + omitting the param and sending null is always explicit. +- JavaScript ``string`` is ``str``. +- JavaScript ``boolean`` is ``bool``. +- JavaScript ``number`` is ``float`` or ``int``. +- JavaScript ``bigint`` is ``int``. + Python integers are arbitrary precision, so ``int`` covers both cases + and is always serialized in full without exponent notation. +- JavaScript ``Date`` and ``Temporal.Instant`` are + :class:`datetime.datetime`. + A naive ``datetime`` is interpreted as UTC. + Since ``Date`` has millisecond precision, microseconds are truncated. +- JavaScript ``Array`` is ``list`` or ``tuple``. + Unordered collections such as ``set`` are unsupported + because they would not serialize deterministically. +- A JavaScript plain object is any ``Mapping``, e.g., a ``dict``. +""" + +import datetime +import math +import string +from collections.abc import Mapping +from decimal import Decimal +from typing import Any, Iterator, List, Optional, Sequence, Tuple, Union +from urllib.parse import parse_qsl + +from .null import is_null + +Params = Mapping[str, Any] + + +class UnserializableParamError(Exception): + """Exception raised when a param could not be serialized. + + :ivar name: Name of the param that could not be serialized + :vartype name: str + """ + + def __init__(self, name: str, message: str): + """ + :param name: Name of the param that could not be serialized + :type name: str + :param message: Description of why the param could not be serialized + :type message: str + """ + + super().__init__(f"Could not serialize parameter: '{name}' {message}") + self.name = name + + +class UrlSearchParams: + """A mutable collection of URL search params. + + Implements the parts of the `URLSearchParams + `_ + interface needed to serialize params to a query string. + Unlike a ``dict``, a name may appear more than once, + which is how arrays are serialized. + """ + + def __init__( + self, + init: Optional[Union[str, Params, Sequence[Tuple[str, str]]]] = None, + ): + """ + :param init: A query string, a mapping of names to values, + or a sequence of name-value pairs + :type init: Optional[Union[str, Mapping[str, Any], Sequence[Tuple[str, str]]]] + """ + + self._pairs: List[Tuple[str, str]] = [] + + if init is None: + return + + if isinstance(init, str): + query = init[1:] if init.startswith("?") else init + self._pairs = list(parse_qsl(query, keep_blank_values=True)) + return + + items = init.items() if isinstance(init, Mapping) else init + self._pairs = [(str(name), str(value)) for name, value in items] + + def append(self, name: str, value: str) -> None: + """Appends a name-value pair, keeping any existing pairs with this name. + + :param name: Name of the param + :type name: str + :param value: Value of the param + :type value: str + """ + + self._pairs.append((name, value)) + + def set(self, name: str, value: str) -> None: + """Sets the value associated with a name. + + Replaces the first pair with this name and removes any others. + Appends a new pair if no pair with this name exists. + + :param name: Name of the param + :type name: str + :param value: Value of the param + :type value: str + """ + + if not self.has(name): + self.append(name, value) + return + + pairs: List[Tuple[str, str]] = [] + is_set = False + + for pair in self._pairs: + if pair[0] != name: + pairs.append(pair) + elif not is_set: + pairs.append((name, value)) + is_set = True + + self._pairs = pairs + + def get(self, name: str) -> Optional[str]: + """Returns the value of the first pair with this name. + + :param name: Name of the param + :type name: str + + :returns: The value, or ``None`` if no pair with this name exists + """ + + for existing_name, value in self._pairs: + if existing_name == name: + return value + + return None + + def get_all(self, name: str) -> List[str]: + """Returns the values of all pairs with this name, in insertion order. + + :param name: Name of the param + :type name: str + + :returns: The values""" + + return [value for existing_name, value in self._pairs if existing_name == name] + + def has(self, name: str) -> bool: + """Returns whether a pair with this name exists. + + :param name: Name of the param + :type name: str + + :returns: Whether a pair with this name exists""" + + return any(existing_name == name for existing_name, _ in self._pairs) + + def delete(self, name: str) -> None: + """Removes all pairs with this name. + + :param name: Name of the param + :type name: str + """ + + self._pairs = [pair for pair in self._pairs if pair[0] != name] + + def sort(self) -> None: + """Sorts all pairs by name. + + Sorting is stable, so the relative order of pairs + with the same name is preserved. + Names are compared by UTF-16 code units to match the + `URLSearchParams.sort() + `_ + specification. + """ + + self._pairs.sort(key=lambda pair: pair[0].encode("utf-16-be")) + + def to_string(self) -> str: + """Serializes all pairs to a query string. + + :returns: The query string, without a leading ``?``""" + + return "&".join( + f"{_encode_form_component(name)}={_encode_form_component(value)}" + for name, value in self._pairs + ) + + def __str__(self) -> str: + return self.to_string() + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.to_string()!r})" + + def __len__(self) -> int: + return len(self._pairs) + + def __iter__(self) -> Iterator[Tuple[str, str]]: + return iter(self._pairs) + + +def serialize_url_search_params(params: Params) -> str: + """Serializes params to a URL search param query string. + + :param params: The params to serialize + :type params: Mapping[str, Any] + + :returns: The query string, without a leading ``?`` + + :raises UnserializableParamError: If any param could not be serialized + """ + + search_params = UrlSearchParams() + update_url_search_params(search_params, params) + + return search_params.to_string() + + +def update_url_search_params(search_params: UrlSearchParams, params: Params) -> None: + """Updates existing URL search params with serialized params. + + Existing params are preserved unless overwritten by a serialized param. + All params are sorted by name. + + :param search_params: The URL search params to update + :type search_params: UrlSearchParams + :param params: The params to serialize + :type params: Mapping[str, Any] + + :raises UnserializableParamError: If any param could not be serialized + """ + + _nested_update_url_search_params(search_params, params, []) + search_params.sort() + + +def _nested_update_url_search_params( + search_params: UrlSearchParams, params: Params, path: List[str] +) -> None: + for key, value in params.items(): + if not isinstance(key, str): + raise UnserializableParamError( + repr(key), + f"is a {type(key).__name__} which is unsupported as a parameter name", + ) + + if "." in key: + raise UnserializableParamError( + key, + 'contains one or more dots "." in its name which is unsupported', + ) + + current_path = [*path, key] + + if isinstance(value, Mapping): + _nested_update_url_search_params(search_params, value, current_path) + continue + + name = ".".join(current_path) + + if value is None: + continue + + if isinstance(value, str) and len(value) == 0: + continue + + if isinstance(value, (list, tuple)): + _update_url_search_params_from_array(search_params, name, value) + continue + + search_params.set(name, _serialize(name, value)) + + +def _update_url_search_params_from_array( + search_params: UrlSearchParams, name: str, values: Sequence[Any] +) -> None: + if len(values) == 0: + search_params.set(name, "") + return + + if len(values) == 1 and _is_empty_string(values[0]): + raise UnserializableParamError( + name, + "is a single element array containing the empty string which is unsupported", + ) + + if any(_is_empty_string(value) for value in values): + raise UnserializableParamError( + name, + "is an array containing the empty string which is unsupported", + ) + + if any(value is None or is_null(value) for value in values): + raise UnserializableParamError( + name, + "is an array containing null or undefined values which is unsupported", + ) + + for value in values: + search_params.append(name, _serialize(name, value)) + + +def _serialize(name: str, value: Any) -> str: + if is_null(value): + return "" + + if isinstance(value, str): + return value + + if isinstance(value, bool): + return "true" if value else "false" + + if isinstance(value, int): + return str(value) + + if isinstance(value, float): + return _format_number(name, value) + + if isinstance(value, datetime.datetime): + return _format_datetime(value) + + raise UnserializableParamError(name, f"is a {type(value).__name__}") + + +def _is_empty_string(value: Any) -> bool: + return isinstance(value, str) and len(value) == 0 + + +def _format_datetime(value: datetime.datetime) -> str: + if value.tzinfo is None: + value = value.replace(tzinfo=datetime.timezone.utc) + + utc_value = value.astimezone(datetime.timezone.utc) + milliseconds = utc_value.microsecond // 1000 + + return ( + f"{utc_value.year:04d}-{utc_value.month:02d}-{utc_value.day:02d}" + f"T{utc_value.hour:02d}:{utc_value.minute:02d}:{utc_value.second:02d}" + f".{milliseconds:03d}Z" + ) + + +def _format_number(name: str, value: float) -> str: + if math.isnan(value): + raise UnserializableParamError(name, "is NaN") + + if math.isinf(value): + raise UnserializableParamError( + name, "is Infinity" if value > 0 else "is -Infinity" + ) + + if value == 0: + return "0" + + sign = "-" if value < 0 else "" + _, digit_tuple, exponent = Decimal(repr(abs(value))).as_tuple() + + # The shortest digit string that round-trips, and the position of the + # decimal point relative to it, as required by the ECMAScript + # Number::toString algorithm. + digits = "".join(str(digit) for digit in digit_tuple) + point = int(exponent) + len(digits) + digits = digits.rstrip("0") + + return sign + _format_digits(digits, point) + + +def _format_digits(digits: str, point: int) -> str: + """Formats digits and a decimal point position per ECMAScript Number::toString. + + :param digits: Significant digits, without trailing zeros + :type digits: str + :param point: Position of the decimal point relative to the digits + :type point: int + + :returns: The formatted number""" + + count = len(digits) + + if count <= point <= 21: + return digits + "0" * (point - count) + + if 0 < point <= 21: + return f"{digits[:point]}.{digits[point:]}" + + if -6 < point <= 0: + return f"0.{'0' * -point}{digits}" + + exponent = point - 1 + exponent_sign = "+" if exponent >= 0 else "-" + mantissa = digits if count == 1 else f"{digits[0]}.{digits[1:]}" + + return f"{mantissa}e{exponent_sign}{abs(exponent)}" + + +_FORM_SAFE_CHARACTERS = frozenset(f"{string.ascii_letters}{string.digits}*-._") + + +def _encode_form_component(value: str) -> str: + """Percent-encodes a string using the ``application/x-www-form-urlencoded`` serializer. + + :param value: The string to encode + :type value: str + + :returns: The encoded string""" + + encoded = [] + + for byte in value.encode("utf-8"): + character = chr(byte) + if character in _FORM_SAFE_CHARACTERS: + encoded.append(character) + elif character == " ": + encoded.append("+") + else: + encoded.append(f"%{byte:02X}") + + return "".join(encoded) diff --git a/test/conftest.py b/test/conftest.py index bacc479f..0af77f8d 100755 --- a/test/conftest.py +++ b/test/conftest.py @@ -62,6 +62,8 @@ def recording_server(responses): content type inferred from the body, e.g. to serve malformed JSON. Yields the endpoint along with the list of requests received so far. + Each request records the ``method``, the raw request ``target``, the + ``path`` and ``query`` it splits into, the ``headers``, and the ``body``. """ requests = [] @@ -70,14 +72,17 @@ def recording_server(responses): class Handler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" - # pylint: disable-next=invalid-name - def do_POST(self): # BaseHTTPRequestHandler dispatches on this name. + def _handle_request(self): content_length = int(self.headers.get("content-length", 0)) raw_body = self.rfile.read(content_length) + path, _, query = self.path.partition("?") requests.append( { - "path": self.path, + "method": self.command, + "target": self.path, + "path": path, + "query": query, "headers": {k.lower(): v for k, v in self.headers.items()}, "body": json.loads(raw_body) if raw_body else None, } @@ -105,6 +110,14 @@ def do_POST(self): # BaseHTTPRequestHandler dispatches on this name. def log_message(self, *args): pass + # Every verb the SDK sends is recorded and answered the same way. + # pylint: disable=invalid-name + do_GET = _handle_request + do_POST = _handle_request + do_PUT = _handle_request + do_PATCH = _handle_request + do_DELETE = _handle_request + server = ThreadingHTTPServer(("localhost", 0), Handler) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() diff --git a/test/headers_test.py b/test/headers_test.py index a9f47e8e..ffdf34e6 100644 --- a/test/headers_test.py +++ b/test/headers_test.py @@ -2,7 +2,6 @@ from importlib.metadata import version from seam import Seam -from seam.constants import LTS_VERSION def test_seam_sends_default_headers(recording_server): @@ -19,15 +18,14 @@ def test_seam_sends_default_headers(recording_server): [request] = requests assert request["path"] == "/devices/get" - assert request["body"] == {"device_id": device_id} + assert request["query"] == f"device_id={device_id}" + assert request["body"] is None assert request["headers"]["seam-sdk-name"] == "seamapi/python" assert request["headers"]["seam-sdk-version"] == version("seam") - assert request["headers"]["seam-lts-version"] == LTS_VERSION + assert "seam-lts-version" not in request["headers"] assert request["headers"]["authorization"] == "Bearer seam_apikey_token" - assert Seam.lts_version == seam.lts_version - def test_seam_sends_workspace_header_with_personal_access_token(recording_server): device_id = str(uuid.uuid4()) diff --git a/test/http_error_test.py b/test/http_error_test.py index d737cfcc..dd30329b 100644 --- a/test/http_error_test.py +++ b/test/http_error_test.py @@ -1,5 +1,5 @@ import pytest -import niquests +from httpx import HTTPStatusError from seam import Seam from seam.exceptions import ( SeamHttpApiError, @@ -40,7 +40,7 @@ def test_seam_http_throws_invalid_input_error(server): seam = Seam(api_key=seed["seam_apikey1_token"], endpoint=endpoint) with pytest.raises(SeamHttpInvalidInputError) as exc_info: - seam.devices.get(device_id=4242) + seam.devices.list(device_ids=4242) err = exc_info.value assert err.status_code == 400 assert err.code == "invalid_input" @@ -56,20 +56,17 @@ def test_seam_http_throws_http_error_on_non_standard_response(server): json={"workspace_id": seed["seed_workspace_1"], "routes": ["/devices/list"]}, ) - with pytest.raises(niquests.HTTPError) as exc_info: + with pytest.raises(HTTPStatusError) as exc_info: seam.devices.list() assert exc_info.value.response.status_code == 503 -# The fake cannot produce malformed error responses, so the recording server -# drives the bodies that must fall through is_api_error_response and raise a -# plain HTTPError rather than being parsed into a SeamHttpApiError. def test_seam_http_raises_http_error_on_non_json_response(recording_server): with recording_server([(500, "Internal Server Error")]) as (endpoint, _): seam = Seam.from_api_key("seam_apikey_token", endpoint=endpoint) - with pytest.raises(niquests.HTTPError) as exc_info: + with pytest.raises(HTTPStatusError) as exc_info: seam.devices.list() assert exc_info.value.response.status_code == 500 @@ -81,7 +78,7 @@ def test_seam_http_raises_http_error_on_malformed_json(recording_server): with recording_server(responses) as (endpoint, _): seam = Seam.from_api_key("seam_apikey_token", endpoint=endpoint) - with pytest.raises(niquests.HTTPError) as exc_info: + with pytest.raises(HTTPStatusError) as exc_info: seam.devices.list() assert exc_info.value.response.status_code == 500 @@ -91,7 +88,7 @@ def test_seam_http_raises_http_error_on_json_without_error_object(recording_serv with recording_server([(500, {"message": "Some error"})]) as (endpoint, _): seam = Seam.from_api_key("seam_apikey_token", endpoint=endpoint) - with pytest.raises(niquests.HTTPError) as exc_info: + with pytest.raises(HTTPStatusError) as exc_info: seam.devices.list() assert exc_info.value.response.status_code == 500 @@ -103,7 +100,7 @@ def test_seam_http_raises_http_error_on_error_object_without_type_and_message( with recording_server([(500, {"error": {"code": 500}})]) as (endpoint, _): seam = Seam.from_api_key("seam_apikey_token", endpoint=endpoint) - with pytest.raises(niquests.HTTPError) as exc_info: + with pytest.raises(HTTPStatusError) as exc_info: seam.devices.list() assert exc_info.value.response.status_code == 500 diff --git a/test/nested_resource_test.py b/test/nested_resource_test.py index 5e84488a..74fb7823 100644 --- a/test/nested_resource_test.py +++ b/test/nested_resource_test.py @@ -65,7 +65,6 @@ def test_action_attempt_union_hydrates_nested_result_and_error(): def test_merged_variants_keep_every_variant_field(): result_fields = {f.name for f in dataclasses.fields(ActionAttempt.Result)} - # One field from each of several action attempt variants. assert "was_confirmed_by_device" in result_fields assert "acs_credential_on_encoder" in result_fields assert "instant_key_url" in result_fields @@ -94,7 +93,6 @@ def test_merged_variants_keep_every_variant_field(): def test_merged_variants_recurse_into_nested_objects(): from_fields = {f.name for f in dataclasses.fields(AcsUser.PendingMutations.From)} - # Each of these arrives from a different pending mutation variant. assert "full_name" in from_fields assert "starts_at" in from_fields assert "is_suspended" in from_fields @@ -129,5 +127,4 @@ def test_nested_classes_are_scoped_to_their_owner(): assert "climate_ref" in preset_metadata.__dataclass_fields__ assert "ecobee_device_id" in device_metadata.__dataclass_fields__ - # Nested shapes stay off the module namespace. assert not hasattr(device_module, "DeviceProperties") diff --git a/test/null_test.py b/test/null_test.py new file mode 100644 index 00000000..e329bb3a --- /dev/null +++ b/test/null_test.py @@ -0,0 +1,56 @@ +from seam.null import NULL, Null, is_null, replace_null + + +def test_null_is_a_singleton(): + assert Null() is NULL + assert is_null(NULL) + assert is_null(Null()) + + +def test_null_is_not_none(): + assert NULL is not None + assert not is_null(None) + assert not is_null("") + assert not is_null(0) + + +def test_null_is_falsy(): + assert not NULL + + +def test_null_repr(): + assert repr(NULL) == "NULL" + + +def test_replace_null_replaces_the_sentinel_with_none(): + assert replace_null(NULL) is None + + +def test_replace_null_recurses_into_dicts_and_lists(): + assert replace_null( + { + "name": NULL, + "properties": {"code": NULL, "kind": "lock"}, + "codes": [NULL, "1234", [NULL]], + "pairs": (NULL, "1234"), + } + ) == { + "name": None, + "properties": {"code": None, "kind": "lock"}, + "codes": [None, "1234", [None]], + "pairs": [None, "1234"], + } + + +def test_replace_null_leaves_other_values_alone(): + values = [None, "", 0, False, "NULL", {"a": 1}, ["b"]] + + assert replace_null(values) == values + + +def test_replace_null_does_not_mutate_its_argument(): + body = {"name": NULL, "codes": [NULL]} + + replace_null(body) + + assert body == {"name": NULL, "codes": [NULL]} diff --git a/test/nullable_param_test.py b/test/nullable_param_test.py new file mode 100644 index 00000000..9557020d --- /dev/null +++ b/test/nullable_param_test.py @@ -0,0 +1,52 @@ +"""Tests that generated params accept the NULL sentinel where the API allows it. + +These assertions are about types as much as behavior: the SDK is type checked, +so a nullable param losing its ``Null`` type, or a param that is merely +optional gaining one, fails the type check rather than any assertion here. +""" + +from seam import NULL, Seam + +DEVICE = {"device": {"device_id": "device1"}} + + +def test_a_nullable_param_is_sent_as_null(recording_server): + with recording_server([(200, DEVICE)]) as (endpoint, requests): + seam = Seam.from_api_key("seam_apikey_token", endpoint=endpoint) + + # The API documents name as nullable, so it may be unset. + seam.devices.update(device_id="device1", name=NULL) + + [request] = requests + + assert request["body"] == {"device_id": "device1", "name": None} + + +def test_a_nullable_number_param_is_sent_as_null(recording_server): + with recording_server([(200, {})]) as (endpoint, requests): + seam = Seam.from_api_key("seam_apikey_token", endpoint=endpoint) + + seam.thermostats.set_temperature_threshold( + device_id="device1", + lower_limit_celsius=NULL, + upper_limit_celsius=20.5, + ) + + [request] = requests + + assert request["body"] == { + "device_id": "device1", + "lower_limit_celsius": None, + "upper_limit_celsius": 20.5, + } + + +def test_an_omitted_param_is_not_sent(recording_server): + with recording_server([(200, DEVICE)]) as (endpoint, requests): + seam = Seam.from_api_key("seam_apikey_token", endpoint=endpoint) + + seam.devices.update(device_id="device1", name=None) + + [request] = requests + + assert request["body"] == {"device_id": "device1"} diff --git a/test/paginator_test.py b/test/paginator_test.py index 800cf78e..65af163b 100644 --- a/test/paginator_test.py +++ b/test/paginator_test.py @@ -14,7 +14,7 @@ def test_paginator_next_page_requires_a_cursor(seam: Seam): paginator = seam.create_paginator(seam.connected_accounts.list, {"limit": 2}) with pytest.raises(ValueError, match=r"next_page_cursor"): - paginator.next_page(None) + paginator.next_page(None) # type: ignore[arg-type] with pytest.raises(ValueError, match=r"next_page_cursor"): paginator.next_page("") @@ -24,8 +24,12 @@ def test_paginator_last_page_has_no_next_page(seam: Seam): paginator = seam.create_paginator(seam.connected_accounts.list, {"limit": 2}) _, first_pagination = paginator.first_page() + assert first_pagination is not None + assert first_pagination.next_page_cursor is not None + _, next_pagination = paginator.next_page(first_pagination.next_page_cursor) + assert next_pagination is not None assert next_pagination.has_next_page is False assert next_pagination.next_page_cursor is None @@ -47,7 +51,9 @@ def test_paginator_next_page(seam: Seam): first_page_accounts, first_pagination = paginator.first_page() assert len(first_page_accounts) == 2 + assert first_pagination is not None assert first_pagination.has_next_page is True + assert first_pagination.next_page_cursor is not None next_page_accounts, _ = paginator.next_page(first_pagination.next_page_cursor) diff --git a/test/retry_test.py b/test/retry_test.py index d1575ac9..a9ebdd0c 100644 --- a/test/retry_test.py +++ b/test/retry_test.py @@ -1,13 +1,42 @@ -import niquests import pytest -from urllib3.util import Retry +from httpx import HTTPStatusError -from seam import Seam +from seam import Retry, Seam +from seam.client import DEFAULT_RETRIES SERVICE_UNAVAILABLE = (503, "Service Unavailable") DEVICES = (200, {"devices": [{"device_id": "august_device_1"}]}) +def test_default_retry_policy_matches_the_standard_policy(): + assert DEFAULT_RETRIES.total == 2 + assert DEFAULT_RETRIES.allowed_methods == { + "GET", + "HEAD", + "OPTIONS", + "PUT", + "DELETE", + } + assert DEFAULT_RETRIES.status_forcelist == {429, *range(500, 600)} + assert DEFAULT_RETRIES.backoff_factor == 0.12 + assert DEFAULT_RETRIES.backoff_jitter == pytest.approx(1 / 6) + + +def test_default_retry_backoff_ranges(monkeypatch): + monkeypatch.setattr("httpx_retries.retry.random.uniform", lambda low, high: low) + + first_retry = DEFAULT_RETRIES.increment() + second_retry = first_retry.increment() + + assert first_retry.backoff_strategy() == pytest.approx(0.2) + assert second_retry.backoff_strategy() == pytest.approx(0.4) + + monkeypatch.setattr("httpx_retries.retry.random.uniform", lambda low, high: high) + + assert first_retry.backoff_strategy() == pytest.approx(0.24) + assert second_retry.backoff_strategy() == pytest.approx(0.48) + + def test_seam_retries_service_unavailable_responses(recording_server): expected_retry_count = 2 responses = [SERVICE_UNAVAILABLE, SERVICE_UNAVAILABLE, DEVICES] @@ -18,7 +47,8 @@ def test_seam_retries_service_unavailable_responses(recording_server): endpoint=endpoint, retries=retry_policy(total=expected_retry_count), ) - devices = seam.devices.list() + # TODO: Use seam.devices.list() once the generated SDK route uses GET. + devices = seam.client.get("/devices/list")["devices"] assert len(devices) == 1 assert len(requests) == expected_retry_count + 1 @@ -34,8 +64,9 @@ def test_seam_stops_retrying_once_retries_are_exhausted(recording_server): retries=retry_policy(total=expected_retry_count), ) - with pytest.raises(niquests.HTTPError) as exc_info: - seam.devices.list() + with pytest.raises(HTTPStatusError) as exc_info: + # TODO: Use seam.devices.list() once the generated SDK route uses GET. + seam.client.get("/devices/list") assert exc_info.value.response.status_code == 503 assert len(requests) == expected_retry_count + 1 @@ -47,8 +78,9 @@ def test_seam_does_not_retry_when_retries_are_disabled(recording_server): "seam_apikey_token", endpoint=endpoint, retries=retry_policy(total=0) ) - with pytest.raises(niquests.HTTPError) as exc_info: - seam.devices.list() + with pytest.raises(HTTPStatusError) as exc_info: + # TODO: Use seam.devices.list() once the generated SDK route uses GET. + seam.client.get("/devices/list") assert exc_info.value.response.status_code == 503 assert len(requests) == 1 @@ -68,7 +100,7 @@ def test_seam_surfaces_service_unavailable_from_a_workspace_outage(server): }, ) - with pytest.raises(niquests.HTTPError) as exc_info: + with pytest.raises(HTTPStatusError) as exc_info: seam.devices.list() assert exc_info.value.response.status_code == 503 @@ -78,7 +110,5 @@ def retry_policy(*, total): return Retry( total=total, status_forcelist=[503], - allowed_methods=["POST"], backoff_factor=0, - raise_on_status=False, ) diff --git a/test/search_params_test.py b/test/search_params_test.py new file mode 100644 index 00000000..79545daf --- /dev/null +++ b/test/search_params_test.py @@ -0,0 +1,176 @@ +from typing import Any, Dict, List + +import pytest + +from seam import NULL, Seam, UnserializableParamError + +DEVICE = {"device": {"device_id": "device1"}} +DEVICES: Dict[str, List[Any]] = {"devices": []} + + +def test_client_serializes_search_params(recording_server): + with recording_server([(200, DEVICES)]) as (endpoint, requests): + seam = Seam.from_api_key("seam_apikey_token", endpoint=endpoint) + + seam.client.get( + "/devices/list", + params={ + "device_ids": ["device1", "device2"], + "custom_metadata_has": {"tag": "front", "floor": 2}, + "limit": 20, + }, + ) + + [request] = requests + + assert request["method"] == "GET" + assert request["path"] == "/devices/list" + assert request["query"] == ( + "custom_metadata_has.floor=2" + "&custom_metadata_has.tag=front" + "&device_ids=device1" + "&device_ids=device2" + "&limit=20" + ) + + +def test_client_does_not_reencode_the_serialized_search_params(recording_server): + """The serializer and httpx disagree on exactly two characters. + + httpx escapes ``*`` and leaves ``~`` alone, so a query it encodes is not + the one the serializer produced. Setting the query on the url keeps ours. + """ + + with recording_server([(200, DEVICES)]) as (endpoint, requests): + seam = Seam.from_api_key("seam_apikey_token", endpoint=endpoint) + + seam.client.get("/devices/list", params={"search": "a *~ b"}) + + [request] = requests + + assert request["query"] == "search=a+*%7E+b" + + +def test_client_omits_search_params_set_to_none(recording_server): + with recording_server([(200, DEVICES)]) as (endpoint, requests): + seam = Seam.from_api_key("seam_apikey_token", endpoint=endpoint) + + seam.client.get("/devices/list", params={"search": None, "limit": 20}) + + [request] = requests + + assert request["query"] == "limit=20" + + +def test_client_serializes_search_params_set_to_null(recording_server): + with recording_server([(200, DEVICES)]) as (endpoint, requests): + seam = Seam.from_api_key("seam_apikey_token", endpoint=endpoint) + + seam.client.get("/devices/list", params={"search": NULL, "limit": 20}) + + [request] = requests + + assert request["query"] == "limit=20&search=" + + +def test_client_sends_no_query_string_without_search_params(recording_server): + with recording_server([(200, DEVICES)]) as (endpoint, requests): + seam = Seam.from_api_key("seam_apikey_token", endpoint=endpoint) + + seam.client.get("/devices/list", params={}) + seam.client.get("/devices/list", params={"search": None}) + seam.client.get("/devices/list") + + for request in requests: + assert request["target"] == "/devices/list" + + +def test_client_serializes_search_params_of_every_verb(recording_server): + with recording_server([(200, DEVICES)]) as (endpoint, requests): + seam = Seam.from_api_key("seam_apikey_token", endpoint=endpoint) + + seam.client.get("/devices/list", params={"device_ids": ["device1"]}) + seam.client.delete("/access_codes/delete", params={"sync": True}) + + assert [(request["method"], request["query"]) for request in requests] == [ + ("GET", "device_ids=device1"), + ("DELETE", "sync=true"), + ] + + +def test_client_passes_search_params_it_did_not_serialize_to_httpx(recording_server): + """Params that are not a mapping are left for httpx to encode. + + A caller who serialized the params themselves, e.g. to the pairs of a + ``UrlSearchParams``, has already chosen how they are represented. + """ + + with recording_server([(200, DEVICES)]) as (endpoint, requests): + seam = Seam.from_api_key("seam_apikey_token", endpoint=endpoint) + + seam.client.get("/devices/list", params=[("device_ids", "device1")]) + + [request] = requests + + assert request["query"] == "device_ids=device1" + + +def test_client_rejects_a_search_param_it_cannot_serialize(recording_server): + with recording_server([(200, DEVICES)]) as (endpoint, requests): + seam = Seam.from_api_key("seam_apikey_token", endpoint=endpoint) + + with pytest.raises(UnserializableParamError): + seam.client.get("/devices/list", params={"search": object()}) + + assert requests == [] + + +def test_client_serializes_null_in_a_json_body_to_null(recording_server): + with recording_server([(200, DEVICE)]) as (endpoint, requests): + seam = Seam.from_api_key("seam_apikey_token", endpoint=endpoint) + + seam.client.post( + "/devices/update", + json={ + "device_id": "device1", + "name": NULL, + "properties": {"code": NULL}, + "codes": [NULL, "1234"], + }, + ) + + [request] = requests + + assert request["method"] == "POST" + assert request["body"] == { + "device_id": "device1", + "name": None, + "properties": {"code": None}, + "codes": [None, "1234"], + } + + +def test_client_leaves_a_json_body_without_null_unchanged(recording_server): + body = {"device_id": "device1", "name": "Front Door", "limit": 20, "sync": True} + + with recording_server([(200, DEVICE)]) as (endpoint, requests): + seam = Seam.from_api_key("seam_apikey_token", endpoint=endpoint) + + seam.client.post("/devices/update", json=body) + + [request] = requests + + assert request["body"] == body + + +def test_client_serializes_the_search_params_of_a_generated_route(recording_server): + with recording_server([(200, DEVICE)]) as (endpoint, requests): + seam = Seam.from_api_key("seam_apikey_token", endpoint=endpoint) + + seam.devices.get(name="Front Door") + + [request] = requests + + assert request["method"] == "GET" + assert request["path"] == "/devices/get" + assert request["query"] == "name=Front+Door" diff --git a/test/timeout_test.py b/test/timeout_test.py index 5e3cebcd..a7aff192 100644 --- a/test/timeout_test.py +++ b/test/timeout_test.py @@ -1,13 +1,14 @@ +import ssl import threading import time from contextlib import contextmanager from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -import niquests import pytest -from urllib3.util import Retry +from httpx import HTTPTransport, Limits, Timeout, TimeoutException +from httpx_retries import RetryTransport -from seam import Seam +from seam import Retry, Seam from seam.constants import DEFAULT_TIMEOUT @@ -15,24 +16,24 @@ def test_timeout_defaults_to_30_seconds(): seam = Seam.from_api_key("seam_apikey_token") assert DEFAULT_TIMEOUT == 30 - assert seam.client.timeout == 30 + assert seam.client.timeout == Timeout(30) def test_timeout_can_be_overridden(): seam = Seam.from_api_key("seam_apikey_token", timeout=60) - assert seam.client.timeout == 60 + assert seam.client.timeout == Timeout(60) def test_timeout_can_be_disabled_with_none(): seam = Seam.from_api_key("seam_apikey_token", timeout=None) - assert seam.client.timeout is None + assert seam.client.timeout == Timeout(None) -def test_niquests_options_are_passed_to_the_session(): +def test_httpx_options_are_passed_to_the_client(): seam = Seam.from_api_key( - "seam_apikey_token", niquests_options={"headers": {"Custom-Header": "Test"}} + "seam_apikey_token", httpx_options={"headers": {"Custom-Header": "Test"}} ) assert seam.client.headers["Custom-Header"] == "Test" @@ -40,10 +41,49 @@ def test_niquests_options_are_passed_to_the_session(): assert seam.client.headers["Authorization"] == "Bearer seam_apikey_token" -def test_niquests_options_take_precedence(): - seam = Seam.from_api_key("seam_apikey_token", niquests_options={"pool_maxsize": 25}) +def test_httpx_options_take_precedence(): + seam = Seam.from_api_key("seam_apikey_token", httpx_options={"timeout": 15}) - assert seam.client.timeout == 30 + assert seam.client.timeout == Timeout(15) + + +def test_transport_httpx_options_are_applied(): + seam = Seam.from_api_key( + "seam_apikey_token", + httpx_options={ + "limits": Limits(max_connections=25, max_keepalive_connections=20), + "verify": False, + }, + ) + + transport = vars(seam.client)["_transport"] + assert isinstance(transport, RetryTransport) + inner_transport = vars(transport)["_sync_transport"] + assert isinstance(inner_transport, HTTPTransport) + pool = vars(inner_transport)["_pool"] + assert vars(pool)["_max_connections"] == 25 + assert vars(pool)["_ssl_context"].verify_mode == ssl.CERT_NONE + + +def test_environment_proxy_transport_is_used(monkeypatch): + for variable in ( + "ALL_PROXY", + "HTTP_PROXY", + "NO_PROXY", + "all_proxy", + "http_proxy", + "https_proxy", + "no_proxy", + ): + monkeypatch.delenv(variable, raising=False) + monkeypatch.setenv("HTTPS_PROXY", "http://localhost:8080") + + seam = Seam.from_api_key("seam_apikey_token") + + mounts = vars(seam.client)["_mounts"].values() + proxy_transports = [transport for transport in mounts if transport is not None] + assert proxy_transports + assert all(isinstance(transport, RetryTransport) for transport in proxy_transports) def test_per_request_timeout_overrides_the_client_timeout(recording_server): @@ -64,19 +104,17 @@ def test_seam_times_out_a_slow_request(): retries=Retry(total=0), ) - with pytest.raises(niquests.exceptions.Timeout): + with pytest.raises(TimeoutException): seam.devices.list() @contextmanager def slow_server(): - """Serve a response too slowly for the client timeout to tolerate.""" - class Handler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" # pylint: disable-next=invalid-name - def do_POST(self): # BaseHTTPRequestHandler dispatches on this name. + def do_POST(self): time.sleep(5) self.send_response(200) self.send_header("content-length", "0") diff --git a/test/url_search_params_serializer_test.py b/test/url_search_params_serializer_test.py new file mode 100644 index 00000000..48c94e04 --- /dev/null +++ b/test/url_search_params_serializer_test.py @@ -0,0 +1,454 @@ +from collections import OrderedDict +from datetime import date, datetime, timedelta, timezone + +import pytest + +from seam.null import NULL +from seam.url_search_params_serializer import ( + UnserializableParamError, + UrlSearchParams, + serialize_url_search_params, + update_url_search_params, +) + + +def test_serializes_empty_object(): + assert serialize_url_search_params({}) == "" + + +def test_serializes_string(): + assert serialize_url_search_params({"foo": "d"}) == "foo=d" + assert serialize_url_search_params({"foo": "null"}) == "foo=null" + assert serialize_url_search_params({"foo": "None"}) == "foo=None" + assert serialize_url_search_params({"foo": "undefined"}) == "foo=undefined" + assert serialize_url_search_params({"foo": "0"}) == "foo=0" + + +def test_removes_the_empty_string(): + # Serializing the empty string would conflict with NULL. + assert serialize_url_search_params({"foo": ""}) == "" + assert serialize_url_search_params({"foo": "d", "bar": ""}) == "foo=d" + + +def test_serializes_int(): + assert serialize_url_search_params({"foo": 1}) == "foo=1" + assert serialize_url_search_params({"foo": 0}) == "foo=0" + assert serialize_url_search_params({"foo": -42}) == "foo=-42" + + +def test_serializes_arbitrary_precision_int(): + assert ( + serialize_url_search_params({"foo": 9007199254740993}) == "foo=9007199254740993" + ) + assert ( + serialize_url_search_params({"foo": 123456789012345678901234567890}) + == "foo=123456789012345678901234567890" + ) + + +def test_serializes_float(): + assert serialize_url_search_params({"foo": 23.8}) == "foo=23.8" + assert serialize_url_search_params({"foo": -23.8}) == "foo=-23.8" + assert serialize_url_search_params({"foo": 0.30000000000000004}) == ( + "foo=0.30000000000000004" + ) + + +def test_serializes_float_using_the_ecmascript_number_format(): + # A float is serialized exactly as JavaScript would serialize the number, + # which is not always the same as the Python repr. + assert serialize_url_search_params({"foo": 1.0}) == "foo=1" + assert serialize_url_search_params({"foo": -0.0}) == "foo=0" + assert serialize_url_search_params({"foo": 100.0}) == "foo=100" + assert serialize_url_search_params({"foo": 1e16}) == "foo=10000000000000000" + assert serialize_url_search_params({"foo": 1e20}) == "foo=100000000000000000000" + assert serialize_url_search_params({"foo": 1e21}) == "foo=1e%2B21" + assert serialize_url_search_params({"foo": 0.0001}) == "foo=0.0001" + assert serialize_url_search_params({"foo": 1e-6}) == "foo=0.000001" + assert serialize_url_search_params({"foo": 1e-7}) == "foo=1e-7" + assert serialize_url_search_params({"foo": 5e-324}) == "foo=5e-324" + assert serialize_url_search_params({"foo": 1.7976931348623157e308}) == ( + "foo=1.7976931348623157e%2B308" + ) + + +def test_serializes_bool(): + assert serialize_url_search_params({"foo": True}) == "foo=true" + assert serialize_url_search_params({"foo": False}) == "foo=false" + assert serialize_url_search_params({"foo": True, "bar": False}) == ( + "bar=false&foo=true" + ) + + +def test_removes_none_params(): + assert serialize_url_search_params({"bar": None}) == "" + assert serialize_url_search_params({"foo": 1, "bar": None}) == "foo=1" + + +def test_serializes_null_params(): + assert serialize_url_search_params({"bar": NULL}) == "bar=" + assert serialize_url_search_params({"foo": 1, "bar": NULL}) == "bar=&foo=1" + + +def test_removes_none_params_at_any_depth(): + assert serialize_url_search_params({"foo": {"bar": None, "baz": 1}}) == "foo.baz=1" + assert serialize_url_search_params({"foo": {"bar": None}}) == "" + + +def test_serializes_empty_array_params(): + assert serialize_url_search_params({"bar": []}) == "bar=" + assert serialize_url_search_params({"foo": 1, "bar": []}) == "bar=&foo=1" + assert serialize_url_search_params({"bar": ()}) == "bar=" + + +def test_serializes_array_params_with_one_value(): + assert serialize_url_search_params({"bar": ["a"]}) == "bar=a" + assert serialize_url_search_params({"foo": 1, "bar": ["a"]}) == "bar=a&foo=1" + + +def test_serializes_array_params_with_many_values(): + assert serialize_url_search_params({"foo": 1, "bar": ["a", "2"]}) == ( + "bar=a&bar=2&foo=1" + ) + assert serialize_url_search_params( + {"foo": 1, "bar": ["null", "2", "undefined"]} + ) == ("bar=null&bar=2&bar=undefined&foo=1") + + +def test_serializes_tuple_params(): + assert serialize_url_search_params({"bar": ("a", "2")}) == "bar=a&bar=2" + + +def test_serializes_array_params_with_mixed_values(): + assert serialize_url_search_params( + {"bar": [1, "a", True, datetime(1970, 1, 1, tzinfo=timezone.utc)]} + ) == ("bar=1&bar=a&bar=true&bar=1970-01-01T00%3A00%3A00.000Z") + + +def test_serializes_datetime(): + assert serialize_url_search_params( + {"foo": 1, "now": datetime(2025, 2, 24, 18, 44, 39, tzinfo=timezone.utc)} + ) == ("foo=1&now=2025-02-24T18%3A44%3A39.000Z") + + +def test_serializes_datetime_with_milliseconds(): + assert serialize_url_search_params( + { + "now": datetime( + 2025, 2, 24, 18, 44, 39, microsecond=123000, tzinfo=timezone.utc + ) + } + ) == ("now=2025-02-24T18%3A44%3A39.123Z") + + +def test_truncates_datetime_microseconds(): + assert serialize_url_search_params( + { + "now": datetime( + 2025, 2, 24, 18, 44, 39, microsecond=123999, tzinfo=timezone.utc + ) + } + ) == ("now=2025-02-24T18%3A44%3A39.123Z") + + +def test_serializes_datetime_as_utc(): + assert serialize_url_search_params( + {"now": datetime(2025, 2, 24, 13, 44, 39, tzinfo=timezone(timedelta(hours=-5)))} + ) == ("now=2025-02-24T18%3A44%3A39.000Z") + + +def test_serializes_naive_datetime_as_utc(): + assert serialize_url_search_params({"now": datetime(2025, 2, 24, 18, 44, 39)}) == ( + "now=2025-02-24T18%3A44%3A39.000Z" + ) + + +def test_serializes_datetime_before_the_epoch(): + assert serialize_url_search_params( + {"then": datetime(1969, 12, 31, 23, 59, 59, tzinfo=timezone.utc)} + ) == ("then=1969-12-31T23%3A59%3A59.000Z") + + +def test_serializes_dicts(): + assert serialize_url_search_params({"foo": 1, "bar": {"baz": "a"}}) == ( + "bar.baz=a&foo=1" + ) + + assert serialize_url_search_params({"foo": 1, "bar": {"baz": {"x": {"z": 1}}}}) == ( + "bar.baz.x.z=1&foo=1" + ) + + assert serialize_url_search_params( + {"foo": 1, "bar": {"baz": {"x": {"z": NULL}}}} + ) == ("bar.baz.x.z=&foo=1") + + assert serialize_url_search_params({"foo": 1, "bar": {"baz": [1, "a"]}}) == ( + "bar.baz=1&bar.baz=a&foo=1" + ) + + assert serialize_url_search_params({"foo": {}, "bar": 2}) == "bar=2" + + assert serialize_url_search_params({"foo": {"x": {}}, "bar": 2}) == "bar=2" + + assert serialize_url_search_params( + {"foo": {}, "bar": {"baz": {"x": {"z": NULL, "t": {}}, "q": {}}}} + ) == ("bar.baz.x.z=") + + +def test_serializes_dict_subclasses(): + assert serialize_url_search_params( + {"foo": OrderedDict([("bar", 1), ("baz", 2)])} + ) == ("foo.bar=1&foo.baz=2") + + +def test_sorts_params_by_name(): + assert serialize_url_search_params({"b": 1, "a": 2, "c": 3}) == "a=2&b=1&c=3" + assert serialize_url_search_params({"b": 1, "A": 2, "a": 3, "B": 4}) == ( + "A=2&B=4&a=3&b=1" + ) + assert serialize_url_search_params({"a10": 1, "a2": 2, "a1": 3}) == ( + "a1=3&a10=1&a2=2" + ) + assert serialize_url_search_params({"zz": 1, "a": {"z": 2, "b": 3}}) == ( + "a.b=3&a.z=2&zz=1" + ) + assert serialize_url_search_params({"ab": 1, "a": {"b": 2}}) == "a.b=2&ab=1" + + +def test_sorts_params_by_utf_16_code_unit(): + assert serialize_url_search_params({"￿": 1, "\U0001f600": 2}) == ( + "%F0%9F%98%80=2&%EF%BF%BF=1" + ) + + +def test_sorting_preserves_array_order(): + assert serialize_url_search_params({"b": ["3", "1", "2"], "a": 1}) == ( + "a=1&b=3&b=1&b=2" + ) + + +def test_encodes_params_as_form_urlencoded(): + assert serialize_url_search_params({"foo": "a b"}) == "foo=a+b" + assert serialize_url_search_params({"foo": "a+b"}) == "foo=a%2Bb" + assert serialize_url_search_params({"foo": "a~b"}) == "foo=a%7Eb" + assert serialize_url_search_params({"foo": "a*b"}) == "foo=a*b" + assert serialize_url_search_params({"foo": "abcXYZ019*-._"}) == "foo=abcXYZ019*-._" + assert serialize_url_search_params({"foo": "a&b=c?d#e/f"}) == ( + "foo=a%26b%3Dc%3Fd%23e%2Ff" + ) + assert serialize_url_search_params({"foo": "100%"}) == "foo=100%25" + assert serialize_url_search_params({"foo": "a\nb"}) == "foo=a%0Ab" + + +def test_encodes_unicode_params(): + assert serialize_url_search_params({"foo": "héllo wörld"}) == ( + "foo=h%C3%A9llo+w%C3%B6rld" + ) + assert serialize_url_search_params({"foo": "日本語"}) == ( + "foo=%E6%97%A5%E6%9C%AC%E8%AA%9E" + ) + assert serialize_url_search_params({"🔒": "a"}) == "%F0%9F%94%92=a" + assert serialize_url_search_params({"a b": 1}) == "a+b=1" + + +def test_cannot_serialize_keys_containing_a_dot(): + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo.bar": 1}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": {"bar.baz": 1}}) + + +def test_cannot_serialize_non_string_keys(): + with pytest.raises(UnserializableParamError): + serialize_url_search_params({1: "a"}) + + +def test_cannot_serialize_functions(): + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": lambda: None}) + + +def test_cannot_serialize_number_pointers(): + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": float("inf")}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": float("-inf")}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": float("nan")}) + + +def test_cannot_serialize_arbitrary_objects(): + class Device: + def __init__(self): + self.device_id = "a" + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": Device()}) + + +def test_cannot_serialize_date(): + # A date is not an instant, so it has no unambiguous serialization. + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": date(2025, 2, 24)}) + + +def test_cannot_serialize_sets(): + # A set would not serialize deterministically. + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": {"a", "b"}}) + + +def test_cannot_serialize_array_params_with_unserializable_values(): + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": [""]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"bar": ["a", None]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"bar": ["a", NULL]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"bar": ["a", ["s"]]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"bar": ["a", []]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"bar": ["a", [""]]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"bar": ["a", {}]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"bar": ["a", {"x": 2}]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"bar": ["a", lambda: None]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": 1, "bar": ["", "a", ""]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": 1, "bar": ["", "a", "2"]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": 1, "bar": ["", "", ""]}) + + with pytest.raises(UnserializableParamError): + serialize_url_search_params({"foo": [1, float("nan")]}) + + +def test_unserializable_param_error_message(): + with pytest.raises(UnserializableParamError) as error: + serialize_url_search_params({"foo": {"bar.baz": 1}}) + + assert str(error.value) == ( + "Could not serialize parameter: 'bar.baz' contains one or more dots" + ' "." in its name which is unsupported' + ) + assert error.value.name == "bar.baz" + + +def test_unserializable_param_error_message_uses_the_full_path(): + with pytest.raises(UnserializableParamError) as error: + serialize_url_search_params({"foo": {"bar": float("nan")}}) + + assert str(error.value) == "Could not serialize parameter: 'foo.bar' is NaN" + + +def test_update_url_search_params(): + search_params = UrlSearchParams() + update_url_search_params(search_params, {"foo": "d", "bar": 2}) + + assert search_params.to_string() == "bar=2&foo=d" + + +def test_update_url_search_params_preserves_existing_params(): + search_params = UrlSearchParams([("foo", "bar")]) + update_url_search_params( + search_params, + {"name": "Dax", "age": 27, "is_admin": True, "tags": ["cars", "planes"]}, + ) + + assert search_params.to_string() == ( + "age=27&foo=bar&is_admin=true&name=Dax&tags=cars&tags=planes" + ) + + +def test_update_url_search_params_overwrites_existing_params(): + search_params = UrlSearchParams([("foo", "a"), ("bar", "x"), ("foo", "b")]) + update_url_search_params(search_params, {"foo": "new"}) + + assert search_params.to_string() == "bar=x&foo=new" + + +def test_update_url_search_params_appends_array_params(): + search_params = UrlSearchParams([("foo", "old")]) + update_url_search_params(search_params, {"foo": [1, 2]}) + + assert search_params.to_string() == "foo=old&foo=1&foo=2" + + +def test_update_url_search_params_keeps_existing_params_for_absent_values(): + for value in [None, "", {}]: + search_params = UrlSearchParams([("foo", "a")]) + update_url_search_params(search_params, {"foo": value}) + + assert search_params.to_string() == "foo=a" + + +def test_url_search_params_from_query_string(): + search_params = UrlSearchParams("?a=1&b=hello+world&c=%F0%9F%94%92&d") + + assert search_params.get("a") == "1" + assert search_params.get("b") == "hello world" + assert search_params.get("c") == "🔒" + assert search_params.get("d") == "" + assert search_params.to_string() == "a=1&b=hello+world&c=%F0%9F%94%92&d=" + + +def test_url_search_params_from_dict(): + assert UrlSearchParams({"a": "1", "b": "2"}).to_string() == "a=1&b=2" + + +def test_url_search_params_append_and_get(): + search_params = UrlSearchParams() + search_params.append("foo", "a") + search_params.append("foo", "b") + + assert search_params.get("foo") == "a" + assert search_params.get_all("foo") == ["a", "b"] + assert search_params.get("bar") is None + assert search_params.get_all("bar") == [] + assert len(search_params) == 2 + assert list(search_params) == [("foo", "a"), ("foo", "b")] + + +def test_url_search_params_set(): + search_params = UrlSearchParams([("foo", "a"), ("bar", "x"), ("foo", "b")]) + search_params.set("foo", "c") + + assert list(search_params) == [("foo", "c"), ("bar", "x")] + + search_params.set("baz", "y") + + assert search_params.get("baz") == "y" + + +def test_url_search_params_has_and_delete(): + search_params = UrlSearchParams([("foo", "a"), ("foo", "b")]) + + assert search_params.has("foo") + + search_params.delete("foo") + + assert not search_params.has("foo") + assert len(search_params) == 0 + + +def test_url_search_params_str(): + assert str(UrlSearchParams([("foo", "a b")])) == "foo=a+b" diff --git a/test/wait_for_action_attempt_test.py b/test/wait_for_action_attempt_test.py index 369178a3..c2ea9c4b 100644 --- a/test/wait_for_action_attempt_test.py +++ b/test/wait_for_action_attempt_test.py @@ -77,7 +77,6 @@ def update_action_attempt(): }, ) - # Use Timer to schedule the update after 1 second t = Timer(1.0, update_action_attempt) t.start() diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..523558be --- /dev/null +++ b/uv.lock @@ -0,0 +1,1228 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version >= '3.12' and python_full_version < '3.15'", + "python_full_version < '3.12'", +] + +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "ast-serialize" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/a9/11851c3e02a3fea2ddc9932d1fdc7d2edaeecc0d2e11bc5f2a7fde2b0934/ast_serialize-0.8.0.tar.gz", hash = "sha256:6c37c43e4004dfb42d321ddedc569dc17ff4259296f3af577c9ea46a809bc010", size = 845638, upload-time = "2026-08-07T11:29:02.152Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/16/6e520b57cd8c75914b38c670ad4593d13c22911e4306cc7165dab8b0789b/ast_serialize-0.8.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3d822605fa7bb326ef868d25fafced7fc660fa46d9b90c02ea86d5e2f5d325f7", size = 863924, upload-time = "2026-08-07T11:27:34.579Z" }, + { url = "https://files.pythonhosted.org/packages/03/e1/48802de9b22a2bcad42ec80601a17e3f69172fe4f590e6311bcc2b323aeb/ast_serialize-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:2efa40b068197d5efb62655b43baadb842ed71c4958cccd3e8b86a35726f0119", size = 1177662, upload-time = "2026-08-07T11:27:36.196Z" }, + { url = "https://files.pythonhosted.org/packages/38/d4/323438db76bded3a1f3523a3167b8325916b2ddceb2107a330c6ec9fcf4d/ast_serialize-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:db1b957291bca08c7e72f43a12357b2948e20775d970e3fc3dac0aa3160ab725", size = 1167072, upload-time = "2026-08-07T11:27:37.646Z" }, + { url = "https://files.pythonhosted.org/packages/77/82/53c5400b54144b56de8ed7f957fd1ccd97e42482009292ab46121d15f8dd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdc0d5b18ff8fb364e87923e47c0a91d0d69dbcaeaa274591f7fd26892cc3a3a", size = 1225497, upload-time = "2026-08-07T11:27:39.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/5f/36c07327a8b91303fbf1382c7c3e8a2902072dbe1b9546138a5288e75ff0/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9da7330f3e235bf7da89b8d39205c6350fc0c08a85379743f2df9fff87d6d980", size = 1227101, upload-time = "2026-08-07T11:27:40.799Z" }, + { url = "https://files.pythonhosted.org/packages/9d/48/5adf5c67addc7ddb328122208c6d375a84cf154984f412b4087330a157bd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3186969ee66a9863b00acc6523ace44c56974eecb348a7ea4b228d9f0b80e19", size = 1424001, upload-time = "2026-08-07T11:27:42.708Z" }, + { url = "https://files.pythonhosted.org/packages/38/a1/70074dd3869d2b0e934f91891d8d6b734361cd3b80f85ca7ece2e668ecdd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40a57b73731be45da4fa41430c4d5dc94a24b3a4faba7b9e069978c0402064ea", size = 1245545, upload-time = "2026-08-07T11:27:44.4Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/53b9c0a8a6399950c2e3546bdfab96d2b299d5b114b47eb94fd3c49c4054/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b9da3ef807eda752502446dfecea3b381c4900b7e27a5d5f4f899eb39951", size = 1248961, upload-time = "2026-08-07T11:27:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/eb/13/3651d3812548a2bda15e26e5dd51aadb48cf682d0865370255fcf0e367dd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:293cc1c5bfa741f8e3fbe8175b9c07beee487c9a6fdbb25a5acad9f1df2d30a9", size = 1243877, upload-time = "2026-08-07T11:27:47.325Z" }, + { url = "https://files.pythonhosted.org/packages/21/a0/521f0bf000f675e9312a4aae2c8ba7a992405d072a85c485e08fd59433b9/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0910c3442a75216dde0f102d854ba2aaa71d2482e0ee213630b9bf29584fba3", size = 1293903, upload-time = "2026-08-07T11:27:49.264Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7e/402fc902568aa2ee65865a3e151f000db0153da8ce6b1be4c9c349025f8d/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43dd6d596879bb1cb8a12cc9dae7bb10090a39a35883026c24f82488a195619a", size = 1401070, upload-time = "2026-08-07T11:27:50.947Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7c/97d4b66c057f1706fc8be6dd532cc77c988794357c8f4ffdb6adabb39562/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8c9d537f59e936392cfd3597789d1390304dd659efc3c486ce7f40fb6b8a9f53", size = 1502602, upload-time = "2026-08-07T11:27:52.364Z" }, + { url = "https://files.pythonhosted.org/packages/89/6f/72cc3b71562001bba46e898ccfbf1844f7939b3e28912736206102f2e5a8/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f0190a33d7f97c65e9069f7a7f40499eea6b5cbe260c558378109caf20ce934b", size = 1495848, upload-time = "2026-08-07T11:27:53.803Z" }, + { url = "https://files.pythonhosted.org/packages/a0/53/d6f629d1e49308b2f363dae028baa213ec222c9106fa1f7f0d1f7b41499a/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:77308ae6c5cf5264cc0f01a7c556ec77a9e68eb1f61b093534d698139fdc3b14", size = 1556556, upload-time = "2026-08-07T11:27:55.342Z" }, + { url = "https://files.pythonhosted.org/packages/ee/22/340f35dd8dfc6d412d53dc20699ca014b8d228db923e8ed4759c512b162c/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8d53a23f27e1ed3a36b2d26fd2a1a6228c8e85a1ed62ff7cdb44bd610769f20a", size = 1417822, upload-time = "2026-08-07T11:27:56.712Z" }, + { url = "https://files.pythonhosted.org/packages/11/29/6dde5c13fbebc051d3a6df4ec0a6fd1d5359333cc1193f7f609f3410b4d8/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa5e7cb08f96fed9121f77b224151e41caf88feab9d652bb46c78202b6fbeda", size = 1445153, upload-time = "2026-08-07T11:27:58.275Z" }, + { url = "https://files.pythonhosted.org/packages/62/c5/f473a8ed030f7a0ca24b9849cca184677a50c053867a7b808c2e1289bbd3/ast_serialize-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:fa70ed4dea0bb18b30a1789c77baa701d0ef30c474f2ccabdea61e25623a8827", size = 1063711, upload-time = "2026-08-07T11:27:59.793Z" }, + { url = "https://files.pythonhosted.org/packages/23/63/39e171fcd38ca057c2e1979d5ee81ac7a3502784abe3d83df7454f7a0978/ast_serialize-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d8b3c8eee4c1baef9d4e84d2a59a805501617127be42615cb48970b15b0892b6", size = 1103740, upload-time = "2026-08-07T11:28:01.405Z" }, + { url = "https://files.pythonhosted.org/packages/21/1c/d00762b399e7726d68d0a088cc946e3a4c60f1c6176f557608f672f627f3/ast_serialize-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:ac4f0a83c55a9b782f79ad55a5247b7db123c1db405959791c2ef886e9710c9f", size = 1076021, upload-time = "2026-08-07T11:28:02.947Z" }, + { url = "https://files.pythonhosted.org/packages/4c/11/911210c3c78923273a9211a2b6cfc4c8aa723b30dab3e1c8d19afb983b40/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:86b8a1e6d90467345356098b040150e82fbc26d24a7a202224b13dc1f6264ca0", size = 1177715, upload-time = "2026-08-07T11:28:04.654Z" }, + { url = "https://files.pythonhosted.org/packages/77/89/6282881c8587606638db153cbe21e1e0c4d1f3970dee1aa0610a1c62a026/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:39e92ff8e8cb45947fe9007174b2950e1fb098e6abd00266a13cd3bcf6675068", size = 1169347, upload-time = "2026-08-07T11:28:06.1Z" }, + { url = "https://files.pythonhosted.org/packages/97/78/a9f846a03a340ff3728c915f23338ca742742f3292700559cdb3ad999b1e/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85d8d18db5b2dfcb3b7e38a4d600ca35504c0ed8a6f75cd1c811e4ffe248a15", size = 1225916, upload-time = "2026-08-07T11:28:07.654Z" }, + { url = "https://files.pythonhosted.org/packages/c0/15/aba6ef8a988a6eceb6f0359589aac509e29ae2dba67fd9bfd5af0c3f13e7/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9830ff7e764f74d9eefb01170c61a9f0fd2c027dac5fcb72e064decd57d56371", size = 1227135, upload-time = "2026-08-07T11:28:09.504Z" }, + { url = "https://files.pythonhosted.org/packages/94/29/3f63d696ea7c5b8abadcecc3505be51bd900daaccc522ed8322fa5b05a93/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6479d9722a4cd21b578f5478074c41e6169f04811996ec881655560f703a5bba", size = 1425040, upload-time = "2026-08-07T11:28:11.044Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5d/0aac338604ff59df5774d4304307898982252f325ff7cafe31d52fedcb65/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a63bed264e818cd83eec11feed0f50aa162542b91132ef58afebc857182763a5", size = 1246278, upload-time = "2026-08-07T11:28:12.519Z" }, + { url = "https://files.pythonhosted.org/packages/23/ca/9f1ef795bb724719532bd86dbec11e5b66857d3fbe9b6772baec0191a6ed/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d187197d234aa45d6cfa2b096be5f666e8cc2e7eb3722d0ab8926293cf5720c", size = 1250029, upload-time = "2026-08-07T11:28:13.896Z" }, + { url = "https://files.pythonhosted.org/packages/dc/25/5e061372d2ed953b9ba3b9c4f73de3b8e9234cda3f6c088db4686801d0e1/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:2d39a56282cfcc0d8eeea37267c754be59c98d48505c23b1dae5c6011f3813dd", size = 1243575, upload-time = "2026-08-07T11:28:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c1/ae7da218053120635a4ca802366c69f707203641af95372eeb83f70dfd52/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7cc5f10386994c0f4844f1e6d6a97127e9b478660eb6dec2b257644f0acab64", size = 1294396, upload-time = "2026-08-07T11:28:16.813Z" }, + { url = "https://files.pythonhosted.org/packages/2e/89/271d1f49c5269fcddcc789ea3f25be401f6723fc1138aeda539f4d05516d/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6102f2f985c2e542be85cd857678ec9356fefa792b93cadfadd31139f5696f27", size = 1401987, upload-time = "2026-08-07T11:28:18.333Z" }, + { url = "https://files.pythonhosted.org/packages/55/be/4e7d77fcf571ac7cb5cf7115a20c36642bd7d29473b45dfaaefeb9618f90/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:3a8660fe66667b76a6e9dccd1d33e66b229fde3b308db991c041609226c005b6", size = 1502904, upload-time = "2026-08-07T11:28:20.039Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ae/ed1de2db7e019d4236fbc164ffa5ef9a6022a300a342bbf142d21b7c141e/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:e7266307e5fba39836edb79def8608887af48820508bff3c5f2941e1e04d1534", size = 1496967, upload-time = "2026-08-07T11:28:21.734Z" }, + { url = "https://files.pythonhosted.org/packages/92/89/5fea507fae5c5f18b7dc7f95e5c00956574b8c717b8fd2049c504fab0b18/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca7e6fd1ad845d1cc649dc2ecd499db2f8f46af5bf8da7b70dd858774cc038b", size = 1559041, upload-time = "2026-08-07T11:28:23.194Z" }, + { url = "https://files.pythonhosted.org/packages/42/71/478d69df21b64e064554a68134c94be304270316ca676a94e63c389a636a/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2880350b13d3eae69a0d70bc1fb6c9bfaca4dbd0e20ba8cd1aa483080b56ff06", size = 1417367, upload-time = "2026-08-07T11:28:24.601Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2d/8962dc8d5b3a9dc27b36f9db199afa25264c741505469d9ec10ffbfd2ba7/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:ab0f9a59f7d63d0d441b56b9a818b273705264352d5115cfee12e940e816d958", size = 1446178, upload-time = "2026-08-07T11:28:26.152Z" }, + { url = "https://files.pythonhosted.org/packages/4f/22/14d2ad4fd1d1bcd0dc687ca268e0630069f45162496260c0efb70ee0ea72/ast_serialize-0.8.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:0485a25ef519c62e749ee3c1ad8070e591b380d67226349eb5a70b228dc1ac4a", size = 1063811, upload-time = "2026-08-07T11:28:27.864Z" }, + { url = "https://files.pythonhosted.org/packages/18/1d/84a327c0202a41aa5fdba3ade33904d6d8f3b9e6806fa83568d835395850/ast_serialize-0.8.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:bd84d60bca7079e741be4ac5dbe237751a59d7f6f9f0126b11880d63822cbe16", size = 1105518, upload-time = "2026-08-07T11:28:29.691Z" }, + { url = "https://files.pythonhosted.org/packages/8c/92/74556dec52fde85a2ad84ed159991b916241043788609c15d8b77e14570b/ast_serialize-0.8.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:057769b5921336eb2d9124f2a731b42ed05ffdac559b840dbdf6f3937cf153dc", size = 1076319, upload-time = "2026-08-07T11:28:31.282Z" }, + { url = "https://files.pythonhosted.org/packages/d1/5d/c650b1f2cc1e75193358da95a080261422e8cd10b66d7370b1688c9915c5/ast_serialize-0.8.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:a02cbed7d8bfdcdee88edaac12bd50d53d9953aaa2e1852ef078625be5f1c0b5", size = 852914, upload-time = "2026-08-07T11:28:32.929Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e3/6142e920fec6ef7bccabd8c24ed8ed99f8bdc6cb8b065e1df7c6a3b2d667/ast_serialize-0.8.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e1bd223df0f6c96b396975fa604cb33bce53d9b4a0185490be4c4a289f7c9c87", size = 1184007, upload-time = "2026-08-07T11:28:34.654Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e9/6e8be8df02b35d85e2b8809f7f1cfa290bdf5882b55127a539d049482db0/ast_serialize-0.8.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ddd3b61f45c132da66c5476b281891e08c1fd87fbdabe8a6973e1622efc85f06", size = 1177588, upload-time = "2026-08-07T11:28:36.318Z" }, + { url = "https://files.pythonhosted.org/packages/8c/80/7e0fd2e2e2aba257820db4a8657c4c356844d36b914b20a4af294bcfb902/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9caa63fad8241257ae401b5ff0a64026c6adb36b8e86cbe8782d9ea505daf6", size = 1234575, upload-time = "2026-08-07T11:28:37.772Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/3bae0af06f9b1bae3001c44d64215f5b567877e7aae9ffd45db11c3a7647/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3926fa117b5e65019853a2969966d11c7175af377a3425991f3fe73784412405", size = 1236015, upload-time = "2026-08-07T11:28:39.14Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c4/ce2d41a1bc22508e82618901f7e10f2a5e2f9556553fea90624daf9875e2/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:485f1113af805e9e170b95ef993ca3fbd4f89c04bab25c58b4fc632d854801ab", size = 1432808, upload-time = "2026-08-07T11:28:40.664Z" }, + { url = "https://files.pythonhosted.org/packages/1a/90/f5058f209756dd70e958b7538aaa82d25d24944baf9ec8ae6f27b06fcacc/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccebbed24f1281062d5852353c72c47502955926cfcb8345ffb3a44d87ff3d3", size = 1256251, upload-time = "2026-08-07T11:28:42.223Z" }, + { url = "https://files.pythonhosted.org/packages/bf/32/7f77ea87fa0836daab706ed5cb7f903bb25fa26a77439011aee626af11d8/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:252f883290d1cdb728eb7fe1d9a7221b88af5a329aae0bc91ddee4dafb820331", size = 1258574, upload-time = "2026-08-07T11:28:43.751Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5a/75b82ad2725b5e8e8c742732f9e76c6738a292d0709e1f60d10a973730b4/ast_serialize-0.8.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:96abc072ad29db8d02194afd47d68987322622787daceae82398d7b69f3ba2e6", size = 1254075, upload-time = "2026-08-07T11:28:45.28Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/8c20ed4eea805516a3fd23dd4a721ce28c64f50f0e4b359969f60a8c97a6/ast_serialize-0.8.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9118ad3e369727060b2696fc4078f250ecffca4248ba87f537f55cea9f9dce06", size = 1301018, upload-time = "2026-08-07T11:28:46.851Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5b/9f14430f12fe830b656fb38f8e2e05ee13b02a88967660bef46af0ab22a8/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f359df4bd921918af8bebd142a376c77511d7151cc8ba852760b587b5a4a54f3", size = 1409951, upload-time = "2026-08-07T11:28:48.312Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3d/084882eca93c842bd4262591a071ec7f825340644035e51501208cc5a8d4/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e94f9121d13fa36cbf21314783c77d05ae3a0868decd18cf5233fdcc6de49ac8", size = 1509544, upload-time = "2026-08-07T11:28:49.847Z" }, + { url = "https://files.pythonhosted.org/packages/ce/73/ea84852096c2036c61cc0b2f97b90242207419f534dc671060ee1c8e05cb/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:54f95b486018d262bcb387a9afd96f0da74508b442762b80c769454a6fbb3ee3", size = 1505671, upload-time = "2026-08-07T11:28:51.239Z" }, + { url = "https://files.pythonhosted.org/packages/cb/88/287b9a5300c1f2f651d259f670931b63110adc265b7613c885b44c5bc53d/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c38b915511e32bc718c49dbce98ff9af36bac0ad6a604f58000cd5e3aecdba7", size = 1563685, upload-time = "2026-08-07T11:28:53.112Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/1bc3a79afcf0c2a8d2c37182d0d659d1545a9d7f7f6dc9cf3e63d6c17135/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a2ef9cf12f2de4f1028c42c1dd7d775255e0fb3e5bb48896c97e35ef52366fe", size = 1427977, upload-time = "2026-08-07T11:28:54.418Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cd/440c798957e14e31776bfeb024d8fafe0bb1d5b89c51c2f067e69938f7b0/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f18048fe9f6dd266bd577cdec48bdcecb74faaa01fe941324435483b013ed2a", size = 1454335, upload-time = "2026-08-07T11:28:55.968Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4a/587eb36dcc240a54c8660f599464516b469ecad96f0dbdb6bccbedb50745/ast_serialize-0.8.0-cp39-abi3-win32.whl", hash = "sha256:31883542dd6c94d178f5db3d32fbd69c5eb88b3a7c018e7ac8cc0c45195ddbed", size = 1068858, upload-time = "2026-08-07T11:28:57.541Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a4/3e887bbd92164e183cb6e412c6a3e9198ddd446d7fe405958293ef5ef49c/ast_serialize-0.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:861794565b06337005c1447ef23103a3d5a627d08bdc827870d00d0b28ef5f51", size = 1111839, upload-time = "2026-08-07T11:28:59Z" }, + { url = "https://files.pythonhosted.org/packages/25/6c/b400476d3ceba681ab929787edc9554f6d88fcc69435eb681b00fc0457a5/ast_serialize-0.8.0-cp39-abi3-win_arm64.whl", hash = "sha256:b2a5978662fd4db463dfb4b974d2b10ac6430b98f5333aabc7051909df3561d0", size = 1083655, upload-time = "2026-08-07T11:29:00.349Z" }, +] + +[[package]] +name = "astroid" +version = "4.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/63/0adf26577da5eff6eb7a177876c1cfa213856be9926a000f65c4add9692b/astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0", size = 406358, upload-time = "2026-02-07T23:35:07.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/cf/1c5f42b110e57bc5502eb80dbc3b03d256926062519224835ef08134f1f9/astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753", size = 276445, upload-time = "2026-02-07T23:35:05.344Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "black" +version = "26.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/96/3c3e09f09f44a37aac36b178a279cd19aa7001bd796187a7b162a294c81f/black-26.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96ae2c733b2aabdd9986e2c5df628ff3473676cd1c5faded1ff496cf6d74083c", size = 1970639, upload-time = "2026-05-18T17:05:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/83/ea/5ad117b9ee3ecd933c712bcbae610006e5b7cc9f41c526cd7ed3b6c4124c/black-26.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e48b87e03bf109288e55cfceadcfa15ff5470aca2851a851950ed2926f450d7", size = 1792130, upload-time = "2026-05-18T17:05:12.983Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/7c448bc623fcdfa96672531beb5a616ea5e64f6975955254d7731ffb0ad9/black-26.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5119fa92ae61f786e8c3662fd60aece1d0a2dd5cca5d0c79417a95e7a4272a59", size = 1846134, upload-time = "2026-05-18T17:05:14.506Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5b/0b39b3a5917f0657ac014ad2edb58c139553a478adfe7f817abf1622ff6e/black-26.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:30d3c14661f2792e9142cce3eeeb1cbc175b3eb5f733be0c8eeb99651e52b0c3", size = 1478883, upload-time = "2026-05-18T17:05:16.542Z" }, + { url = "https://files.pythonhosted.org/packages/4c/48/dc222692e0f95030db1bbfb6c857e76858bad09058221ea7aae815255327/black-26.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:1ef92b76f7733f282fd096ea406200b5a286c42947412b0eaff3a74e3616cefe", size = 1277776, upload-time = "2026-05-18T17:05:18.029Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/7744b906703228264ef73bdd534df88ec1ef3de45c4e78f6d31b9e32d0c9/black-26.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4ad6fa01f941920f54f2bbb35f3df7673428a0ef98a0b0840c2eaef3b110efa8", size = 2012518, upload-time = "2026-05-18T17:05:20.108Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c0/c5a3b1636dfd09c42534f2b3cf33506814f6d3e066fb0879ffa16c1ae860/black-26.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3915f256e75a2d7cf88d8953d37f780455dc586cc72dee059c528fe77f581217", size = 1816016, upload-time = "2026-05-18T17:05:21.84Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0e/36044316b65ca471d3bb6d3703fd06fb50c6b727c3562f6a5a3153634f88/black-26.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d98d4137277c75dfb898ec8d846c4fd68ba1e9cf77f95e2865c203dc18f4c3d", size = 1884150, upload-time = "2026-05-18T17:05:23.546Z" }, + { url = "https://files.pythonhosted.org/packages/b3/33/dafc5808c2af43672912111d7c3354af1615f7e2be3bed7a878461abbe4d/black-26.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:a1dca32d9f1784af512a13410ec204c6f7f0aa9797a111c42e1c03449821c264", size = 1486825, upload-time = "2026-05-18T17:05:25.004Z" }, + { url = "https://files.pythonhosted.org/packages/82/14/b965ee6ad2a311f28bdbf692def3ee9848d2ae289dab28b27657fcee3e78/black-26.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1037d5ac7b7b310b2632ad867ec8d0e4c4819dcdb0b820f63135da746a24e418", size = 1288646, upload-time = "2026-05-18T17:05:26.477Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5c/c384363980e11e25ca6b93205949bb331fbf35f4e0dbec376dfa6326cec8/black-26.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3", size = 2009020, upload-time = "2026-05-18T17:05:28.132Z" }, + { url = "https://files.pythonhosted.org/packages/0b/df/9f31c5e0babbfed77d505fc5d120beb98b21b33feaeded3924ea941fe360/black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0", size = 1813335, upload-time = "2026-05-18T17:05:31.266Z" }, + { url = "https://files.pythonhosted.org/packages/fb/24/8e7b9a2fa61b0afd82209efe937557d180a1fa055bd7f6161eb9defc3719/black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294", size = 1881614, upload-time = "2026-05-18T17:05:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/49/ad/b4e0d9365ba8ac34f6bbab62a4b1b2dd5d618fac3fa1b8db968c844201b5/black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a", size = 1488925, upload-time = "2026-05-18T17:05:34.259Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4b/652b859bf5df88a751c30451b09338f7fd26a77d1271c666992f836b7711/black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52", size = 1289883, upload-time = "2026-05-18T17:05:36.019Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a8da8eb208c51c7f4ce74609a45d0dcc6d8a2141e45e81ee5289d1bb0d59/black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168", size = 2004800, upload-time = "2026-05-18T17:05:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/11/8a/a479296a19e383b70a725882a6cf3d786540601ff03cabbaaf1cce864c5a/black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3", size = 1815576, upload-time = "2026-05-18T17:05:40.309Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/cfaf3d39f25132c156a068f6b805576c9103a84086019507c70e1911ee7d/black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18", size = 1877927, upload-time = "2026-05-18T17:05:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/66/76/302e313964bcff7e28df329d39f84f5270095730d85ff0acc260610a0d82/black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50", size = 1511860, upload-time = "2026-05-18T17:05:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/27/4e/a3827e35e0e567f9f9ee59e2a0ab979267dca98718f25547ca8c6733afd4/black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae", size = 1316632, upload-time = "2026-05-18T17:05:45.521Z" }, + { url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.15.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00", size = 936952, upload-time = "2026-08-06T13:50:24.442Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/66/edcec7d7a0b524aa8923e22925fde6fe50ce005a113dca13ae1581455c4c/coverage-7.15.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bbac5abad70df71019988f83f26ac7092ff2642975def4429e98dc7585ef3490", size = 222367, upload-time = "2026-08-06T13:47:15.578Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c6/ab8de429e2e8548faf58ec7e1674a4ce00414b4113942d3fe87109cf0f68/coverage-7.15.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:357a173465c7ce028d07a95cc2b63b5bf59f50ecdd5ad75c5cbb78ada984048e", size = 222874, upload-time = "2026-08-06T13:47:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/be/c4/3b7b49587e8a6b9af79b3eb468d443d6042b6d65b47aa26586846a0d6566/coverage-7.15.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:21b803935e2efc3acebe9697197a294fccf5dc4e5382bd6369542ff7a7d2a1d7", size = 253287, upload-time = "2026-08-06T13:47:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/fb/65/ec03b743a2a229c72cc1eff3e57be9d3564e9c6b4d5aba2d70744a3fc0d8/coverage-7.15.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7a2b580774a4786c1053157c0165e04476e03ff293993d7c148eee784a94bae6", size = 255199, upload-time = "2026-08-06T13:47:19.765Z" }, + { url = "https://files.pythonhosted.org/packages/41/4b/5163729e4b6582d61975cfd3ccab45b4ec53e21cf156d9941cb025188468/coverage-7.15.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9464451c4efffe8d47ace5a540b10b0dc10e879066290f8600872b7f54a419d", size = 257308, upload-time = "2026-08-06T13:47:21.206Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/2167a0f08fb87d702fa423a48578a32865464b7c9e1db3911ad7812ab414/coverage-7.15.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de602f34123c2f4af1c1869c6dbbbd60da6d5983bf01937367295d135cccbfce", size = 259268, upload-time = "2026-08-06T13:47:22.503Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e5/68eebae3053dbd48508edea559c21b23fbdf3460784f91370c83a86a6acd/coverage-7.15.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6879ded16a27f3eeca19b900c147e81616e7054db451471a611b2755ee5249f7", size = 253392, upload-time = "2026-08-06T13:47:23.88Z" }, + { url = "https://files.pythonhosted.org/packages/1a/46/fd4ced40a2b691c774e515c9b69500bfa64c7960b67fcee4b2f6fad97fc3/coverage-7.15.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:986be58c3ab54aae8d3496a6225eea74f760fdbe739b38bd442c7e8d133aa53b", size = 255001, upload-time = "2026-08-06T13:47:25.469Z" }, + { url = "https://files.pythonhosted.org/packages/53/25/ae2e5fa710bb6957a9aadeb9e3598d3b3e4af6587ce857ad42e8639a3f30/coverage-7.15.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c6103639613fe6c1e989082948419bc77a2d26b6c825c99d7fad25f7d3d87afc", size = 253061, upload-time = "2026-08-06T13:47:26.845Z" }, + { url = "https://files.pythonhosted.org/packages/d7/31/67ddc0365db2c6e93ac8580bc4bbc50f65273262f973f63ebcdbc15c0495/coverage-7.15.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3af93dddb5659276c63bc16ac6466ac2033a70ca816097bbc06345b8ccdf571", size = 256831, upload-time = "2026-08-06T13:47:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/f6/78/82b8fd18f57fb13f12d98fe874995bb2c4f9f17be8aff762c426323fdb96/coverage-7.15.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b10075e5421d04265766a6d1dac809bbeb8a946fbb23c8f82c227409b2190719", size = 252781, upload-time = "2026-08-06T13:47:29.712Z" }, + { url = "https://files.pythonhosted.org/packages/0a/eb/6c74ef4dd12b252e573c49bdef9e2ac265bf3dbb79b8d7feb3266e084e9e/coverage-7.15.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a67a9f78b2942d87ba8ce3059c642164d2aedd65337377fb52fe9803656bc5c7", size = 253692, upload-time = "2026-08-06T13:47:31.192Z" }, + { url = "https://files.pythonhosted.org/packages/5a/66/eb9aed1c3fd2d36ee00eb173f434b14fa607fc056739c9a89ff4244010ea/coverage-7.15.4-cp311-cp311-win32.whl", hash = "sha256:69484d1aca26e322e1c3ce03f09341e84524ababad2d7202161738d83cc9f82e", size = 224461, upload-time = "2026-08-06T13:47:32.572Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6d/81fa4161dfb3ed9d74e40d58647eff83a56b7612e78352581280fce2f477/coverage-7.15.4-cp311-cp311-win_amd64.whl", hash = "sha256:63fd6fcd1dd6e158f7eb78606e72933b3f6d01e7b747f99c6c12d764307a0fdc", size = 224937, upload-time = "2026-08-06T13:47:34.205Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c1/d8dacf683c6cad3cf85ce68fd3774a6774ec402128822fdfaed920f11e6a/coverage-7.15.4-cp311-cp311-win_arm64.whl", hash = "sha256:ea82116c9893fa89e929b7f197ee5a1950a76e91cc5c85ba503fc02379d04890", size = 224479, upload-time = "2026-08-06T13:47:36.118Z" }, + { url = "https://files.pythonhosted.org/packages/1d/48/bc8d4ba7b37551a767bd863f15b3f80182b271c2f55975356f5f7dbe94c2/coverage-7.15.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4fedd1f7f428f9fe83b1ead5e7cc87a43427be31aadafbac3ac0636dc7abb22", size = 222543, upload-time = "2026-08-06T13:47:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/20/dd/88d6f83f1fffc974a3691a34a97951c5b12df7512a6782c5963883cbc058/coverage-7.15.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37e2f0cdf58e2e1fed4e4d5a8f8786ae2f7eb80b478016876667dc4a01d60a97", size = 222905, upload-time = "2026-08-06T13:47:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5c/54ee0d4748585bb0acab9891cd8d92f2d3593165b4e59fc9de113bfb3140/coverage-7.15.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb55d0e70bb15f2e81477613627286581414693d74ac7963c93a790dd453ca9d", size = 254407, upload-time = "2026-08-06T13:47:40.488Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3f/f0642a372f494bd0d7dad3b497083b910194a5f1c88be2c94fef707c3b59/coverage-7.15.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2", size = 257145, upload-time = "2026-08-06T13:47:41.931Z" }, + { url = "https://files.pythonhosted.org/packages/71/17/8b46d0ed68251016002ec972c8fc0119961a765d0984cafb8bf317c43758/coverage-7.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931", size = 258257, upload-time = "2026-08-06T13:47:43.527Z" }, + { url = "https://files.pythonhosted.org/packages/30/b8/8498a0e72d0adbe15477dd07463d2b3bb2c9f6a4815e8589e50939e2c3ae/coverage-7.15.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8", size = 260517, upload-time = "2026-08-06T13:47:45.121Z" }, + { url = "https://files.pythonhosted.org/packages/41/e1/7dce19c3bdb1e3dd63e769508216500edad81bd5f69a26d724e32aceaf78/coverage-7.15.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4193a04b518f7968f3099755f5509ee7cccc6dc2b92a6b14841934d22e222c9", size = 254785, upload-time = "2026-08-06T13:47:46.541Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b1/e1494703c675a2561723cd9b89f45c9168782c31280c611b1f767851e57c/coverage-7.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e98dcc55d572b38e69d117da7e8e8efb8500f1f5eaf81ecd460a63220790b839", size = 256176, upload-time = "2026-08-06T13:47:48.155Z" }, + { url = "https://files.pythonhosted.org/packages/73/76/a5629d270fb638a43a4b10466f51e2f49d532c1aa4da2913cbbb150bbe0a/coverage-7.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72", size = 254321, upload-time = "2026-08-06T13:47:49.757Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/9c44447218435d5766b911534f9d798144a5560f85e9a54ebe5f3f5d19f9/coverage-7.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d10025d96ea89fc2f73714dbc4cbd433fe012c1ac9e23f895d7728b238b6e52", size = 258390, upload-time = "2026-08-06T13:47:51.248Z" }, + { url = "https://files.pythonhosted.org/packages/de/36/c1e127616fb3fa18a9ff71e76c417f2fd7424332a4870015ac224ef4c039/coverage-7.15.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d802e1947603162ded419bff83ac7489820355d2b856dfb09206574e3a37ac0c", size = 253894, upload-time = "2026-08-06T13:47:52.816Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b9/fdb92c8ae7a8bb9b850cc253b7b3b9c8526f68130002048b5671cd510d09/coverage-7.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4", size = 255763, upload-time = "2026-08-06T13:47:54.296Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/a7d51b2587c7bdb76e71b0896d2565bf7d60436b5122fc83e511adb1f7cd/coverage-7.15.4-cp312-cp312-win32.whl", hash = "sha256:5c3431b2161279b7db5c2a1aa58ae02e5cb8c3c42d93a5094be3f5537bd5b11b", size = 224597, upload-time = "2026-08-06T13:47:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/49/b9/5c5f80cc55f5acaaca6dee677626bfcec8c87204a7809b438b08e84f4571/coverage-7.15.4-cp312-cp312-win_amd64.whl", hash = "sha256:6befeab5fb2b51c958ca4ac6c5d141a1e8240f4f76e46350f1911963deda49cd", size = 225135, upload-time = "2026-08-06T13:47:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/47/e4/2a4561f89ff6bf7c925c287d0f2cce8bdf139c3a33735c87e3203401cf94/coverage-7.15.4-cp312-cp312-win_arm64.whl", hash = "sha256:67bc345491ab55b837277d76f5775d057e8c7f1ac44d890d8c2c82adde258c6f", size = 224515, upload-time = "2026-08-06T13:47:58.977Z" }, + { url = "https://files.pythonhosted.org/packages/f1/84/651a9310859673aaa3b3203f1aa1641ca60fcf2494683e1c9474c7172780/coverage-7.15.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c705b28feb2775dc82a25f1d473a370bc37ff93f5177f4e29ce2425f560f6921", size = 222565, upload-time = "2026-08-06T13:48:00.796Z" }, + { url = "https://files.pythonhosted.org/packages/82/f9/4dcf700137e8af550670f4d74d1b63828ce93e1e2b05e5f10710eb2ea987/coverage-7.15.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ff205ab5e3ecc670f6a4dd19d9cbf12ede53dd41cfc1e15716ec961ea6d314e", size = 222936, upload-time = "2026-08-06T13:48:02.391Z" }, + { url = "https://files.pythonhosted.org/packages/07/4a/612ff1e780b3fbfd637486f542f84adc5503873d8b5d279dec1ffeef9414/coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36", size = 253926, upload-time = "2026-08-06T13:48:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/b0/04/d1cff1c2ead4708a6a79c01d3736b6a25bd38a36678398f72a8dd33dfad9/coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4", size = 256523, upload-time = "2026-08-06T13:48:05.996Z" }, + { url = "https://files.pythonhosted.org/packages/b9/80/d34e13fb4b293cbdb9665838cf5522077b8ad14ef947550631a4bced36a5/coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c", size = 257759, upload-time = "2026-08-06T13:48:08.036Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e7/2c5fe7636fdb0732fe0f09f308a5b066864078b7fc61f6678e8478554f2e/coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7", size = 259890, upload-time = "2026-08-06T13:48:09.834Z" }, + { url = "https://files.pythonhosted.org/packages/92/28/9689f0858dfff59c2ea688938ab9fa2925631235df67126a42b6c5c70ae1/coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25", size = 254121, upload-time = "2026-08-06T13:48:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/785077c230c157243eb5aa9a26c3be260ecd02001bead54a3cada3df8e03/coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b", size = 255891, upload-time = "2026-08-06T13:48:13.209Z" }, + { url = "https://files.pythonhosted.org/packages/d4/90/e20371b17b40f912f21305c2db2f30efa3de306f7320fc916804872c85a4/coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78", size = 253859, upload-time = "2026-08-06T13:48:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/05/49/25371987ee459a5f67c0427fb75c74f9358e65f2c71fe75bf41c1b6c5fcb/coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f", size = 258011, upload-time = "2026-08-06T13:48:16.464Z" }, + { url = "https://files.pythonhosted.org/packages/30/6e/32e67467f6154bf4f1c4f63b05acc5097cba4237d45bbeeea446b52e8ac1/coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d", size = 253676, upload-time = "2026-08-06T13:48:18.493Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/8b24192e89286399765155251f99ee9f070a9d637109018ac23d99b99f6f/coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff", size = 255453, upload-time = "2026-08-06T13:48:20.057Z" }, + { url = "https://files.pythonhosted.org/packages/16/6f/8b41ebdf67c87854e17c035336a90f1cfbad0c14c2a584301be6ff148718/coverage-7.15.4-cp313-cp313-win32.whl", hash = "sha256:c65a9e0dfc6143491879da4e13b5e30f8be192055de508d737fb14601edbd22c", size = 224605, upload-time = "2026-08-06T13:48:21.655Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e2/2946c7f0b42b152ecb21ff1bdad72e3d301e790c0c487e4a86e8c9f69347/coverage-7.15.4-cp313-cp313-win_amd64.whl", hash = "sha256:2ff8f5e9b8f7a94f0c11c45631eee103dbcb7d63274edd12c56efe1be690b3b4", size = 225148, upload-time = "2026-08-06T13:48:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/9e/83/3f4a69957f48ae7a0aba76c34743f88963d607b19e03f3f8e66f91cae0f9/coverage-7.15.4-cp313-cp313-win_arm64.whl", hash = "sha256:6e0a8a5083b096487d6cfced94cdd514d8f5db6f113610fb36c0620edb1028cf", size = 224536, upload-time = "2026-08-06T13:48:25.117Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ac/748cf29eeb2d6be34a3176ce26a4f49e38085ee08e8935f05f6f26ed7e0f/coverage-7.15.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:770e9325ab5ea6d56f77e59b29ecfe0ac20b57a82a601876f90494a4dda0386f", size = 222608, upload-time = "2026-08-06T13:48:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/0b/02/1abbf5c984677b0aa439cdacaccbf38d248939d8ef8fe1cc7a50d73edb77/coverage-7.15.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d12b33a3a50a1676b7784dc8d00a0c6d66a9f2add4b85a041c19b6a7e53ef23c", size = 222940, upload-time = "2026-08-06T13:48:28.432Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e1/ff8f9f53d9fcf586125b55d0b1f04ec1c14955fee41e83d5814bee141bb5/coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082", size = 253985, upload-time = "2026-08-06T13:48:29.995Z" }, + { url = "https://files.pythonhosted.org/packages/a1/26/595759762e514e81be1d7d01ed03444303bcd152226a6529998d253f9201/coverage-7.15.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff97a14362eef486483ed44042ca2027ea257df6ff768e62358ee0c9776925ac", size = 256492, upload-time = "2026-08-06T13:48:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/b79aabac54d482be23b5fcdd4f4662bff24a78edc4ee29201726929936d5/coverage-7.15.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a325e815318638aed1655d9c06e6d7c2d3d46c09231ce988070428a8762d734", size = 257837, upload-time = "2026-08-06T13:48:33.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/0f/bf7f297885a5bf6fd71e5782404e0ff059ca09e8711ceb3a08544abde45a/coverage-7.15.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d", size = 260152, upload-time = "2026-08-06T13:48:34.75Z" }, + { url = "https://files.pythonhosted.org/packages/fd/f1/296744e854ff8368542343457414380465e9ceefb9192342feb9d3bc461d/coverage-7.15.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf", size = 253978, upload-time = "2026-08-06T13:48:36.434Z" }, + { url = "https://files.pythonhosted.org/packages/55/b0/bbdb2e9057493e66220a2e149ca2d301ba0e3a58a83bd6b90de9826d16f3/coverage-7.15.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39ece820e29e0a2ba34b3ecb3be83c27e997eed8926f2ba6fe7ce7a0bda5843b", size = 255846, upload-time = "2026-08-06T13:48:38.317Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/38015b2b6d21258713bd17e76b59d033b191efb5703589cffd037dfbca20/coverage-7.15.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f21b56dcace11dfe013014201f577dcd592b2a9b72182d930361b47cf6f73f25", size = 253808, upload-time = "2026-08-06T13:48:39.993Z" }, + { url = "https://files.pythonhosted.org/packages/0b/64/0d515c1e60ee6fbfd1a0e79c07cd87d388a233b7adc37758735677203808/coverage-7.15.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303", size = 258081, upload-time = "2026-08-06T13:48:41.971Z" }, + { url = "https://files.pythonhosted.org/packages/91/71/04d9e7a3642146c6351338aef4ef85ab11dbbb54744c13245caba1aad1c0/coverage-7.15.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:141fae2cabf5569b782c10afc4c850ce10f618c13f8db54765cba99cc839da1f", size = 253624, upload-time = "2026-08-06T13:48:43.731Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a7/6c28b74c81ebff66987b0e2522ba5cffa3e90b0c33cb6a2eb264d4ee8cf1/coverage-7.15.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5", size = 255280, upload-time = "2026-08-06T13:48:45.58Z" }, + { url = "https://files.pythonhosted.org/packages/52/af/bc19996a7014b98d7bbb0f0939453c67074af65784a3aa16a789a07381fa/coverage-7.15.4-cp314-cp314-win32.whl", hash = "sha256:7bbd7d6418e0dab31a206af5203bd43ae36edb8e7fba1940b055d3e9249290d7", size = 224768, upload-time = "2026-08-06T13:48:47.525Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/219484e476d6e101ba0a444852579e05f5b75c37c611a42ed1190f73ef62/coverage-7.15.4-cp314-cp314-win_amd64.whl", hash = "sha256:f0204ed122758782970526057093f448051a39db9d810d4e344bb87a3546f425", size = 225259, upload-time = "2026-08-06T13:48:49.513Z" }, + { url = "https://files.pythonhosted.org/packages/b7/66/fa77daf4e383e5f776dac62c2409b6af81910ae6fe326bd5170dba74cc63/coverage-7.15.4-cp314-cp314-win_arm64.whl", hash = "sha256:9e71e7bc71c686a123347ae47a0de33a175e797a85bb57b791492adf4eec8ed8", size = 224684, upload-time = "2026-08-06T13:48:51.235Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/f03bf0ce362bbf3f785fa5219620d00778d4ac6fc9e407734828e9c672f6/coverage-7.15.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7c922735321eef3f87c280a3d39afff6b646723a2880b862cda4ac7a093b8aa8", size = 223338, upload-time = "2026-08-06T13:48:52.896Z" }, + { url = "https://files.pythonhosted.org/packages/0f/76/e77d0ae22501831cc9f92193e8a957a5caa1dd177f90a6d1d9b106242d92/coverage-7.15.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f41c17c4668a655ce96d090d8d5ffdc24ef64b5a02f9753884d08483e8a4a41a", size = 223609, upload-time = "2026-08-06T13:48:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/82/1a/b1f089da8d38ac612fa2dd6dc7f4a1a7657d12f3e261d2996edd3a838d0b/coverage-7.15.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b", size = 264970, upload-time = "2026-08-06T13:48:56.403Z" }, + { url = "https://files.pythonhosted.org/packages/bf/31/e66d98d6e9c7fcc88470f1e234eaf6b1950dc0dfbf797f7282c1c861da24/coverage-7.15.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5", size = 267088, upload-time = "2026-08-06T13:48:58.41Z" }, + { url = "https://files.pythonhosted.org/packages/59/a1/ae94eb2c541add426378408379f233591e069040b1e2cdb33df9498a0682/coverage-7.15.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba", size = 269508, upload-time = "2026-08-06T13:49:00.42Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c7/88a10694a1c6a213569766aba9f25847b28155d4ac731b13226db216356d/coverage-7.15.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05e491d4f3165d62d4f5c8fd48dfeabf2ae8f42cbbd484319af33ea851b78982", size = 270629, upload-time = "2026-08-06T13:49:02.234Z" }, + { url = "https://files.pythonhosted.org/packages/b3/34/d8b8232e5e55169933b59aabcef2fedfa4b9d8897361bb80fcbda146505f/coverage-7.15.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:226c66e80ec0598d3b9b4874123df167ccca342aca8714f77cac6829688ee09c", size = 264043, upload-time = "2026-08-06T13:49:04.102Z" }, + { url = "https://files.pythonhosted.org/packages/7e/35/58b009dbf8c471c7224716478b9fed4a7e1af15320e1ed41660978504663/coverage-7.15.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57", size = 266963, upload-time = "2026-08-06T13:49:05.821Z" }, + { url = "https://files.pythonhosted.org/packages/62/aa/57fbda1b42c892968273c56b6ee9dc0f1310850859230a507bc7873b1f65/coverage-7.15.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26", size = 264569, upload-time = "2026-08-06T13:49:07.706Z" }, + { url = "https://files.pythonhosted.org/packages/98/8a/360e6e7f24d477b7e889703af0afa878d15b6d4d8d2a822b2835c169a879/coverage-7.15.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07545711d4f0f32852a18f18ad11f76f0109909d09e78b9008b4cfc67e829429", size = 268299, upload-time = "2026-08-06T13:49:09.587Z" }, + { url = "https://files.pythonhosted.org/packages/4e/89/6f701261aee21b6b5fa8f7872229406dc917e125069448292223bf213606/coverage-7.15.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017", size = 263413, upload-time = "2026-08-06T13:49:11.604Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0f/6f04036edc260ed425af83e834f627fad48941ce97b50bfe6edd8b6fa623/coverage-7.15.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839", size = 265725, upload-time = "2026-08-06T13:49:13.38Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ce/d19b5d4d5c49a7bfb925fd74310fee7d28bc99520ac3367ccbc54e662518/coverage-7.15.4-cp314-cp314t-win32.whl", hash = "sha256:cbde877ef9dd7baf272b9bfef2b8a25edd45d9170fc326951dd20eb480335e85", size = 225079, upload-time = "2026-08-06T13:49:15.265Z" }, + { url = "https://files.pythonhosted.org/packages/26/bb/7aa1b3b173faee0679037ca950bbbe1247273656697994d8d13f80f8d4b4/coverage-7.15.4-cp314-cp314t-win_amd64.whl", hash = "sha256:3da9e92d1c551fd7563833e9ade686efb0c4b7363ab7681a94283958c950bf5e", size = 225911, upload-time = "2026-08-06T13:49:17.279Z" }, + { url = "https://files.pythonhosted.org/packages/81/1c/4ea9e47426d80038d9222db3c4534cb6021a74b237d3ff97ffd33b6600dd/coverage-7.15.4-cp314-cp314t-win_arm64.whl", hash = "sha256:3a54f5a0d85050c73a38f6793090ee83974531e67fe5e57a1da9bee11398aa5e", size = 225219, upload-time = "2026-08-06T13:49:19.293Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/dc5d2ac8f9142e7ec7de66e7bf0591db29d78955a040bd915870d9c0e657/coverage-7.15.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c9872e4d9dc5d3cf616bf4b382f5a00359305a5be666a3dd0b5cdb4e49597f9", size = 222604, upload-time = "2026-08-06T13:49:21.279Z" }, + { url = "https://files.pythonhosted.org/packages/70/39/33e63df81fe2ee100897451841c821467635923e58e37c6bd4b46dd8106c/coverage-7.15.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:e101dbb4b9b72f0cddd8cdc8c9c5b47f456766f5e0ac82dbfb75e5c55409b78a", size = 222944, upload-time = "2026-08-06T13:49:23.187Z" }, + { url = "https://files.pythonhosted.org/packages/99/1f/ef3ffb5557febc75a0d97aa459d0266d7d741110265121cc6d8539343d44/coverage-7.15.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54", size = 254050, upload-time = "2026-08-06T13:49:25.008Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f5/1f0f6f77698c3601ca0ae7431e34b24c62ca2f06fecb23b73ed1f651d2be/coverage-7.15.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb", size = 256967, upload-time = "2026-08-06T13:49:26.896Z" }, + { url = "https://files.pythonhosted.org/packages/03/7a/2ed9bed79925f4367c83c77f66a89e5ca7229c288d2d19ad5f36d1ca0070/coverage-7.15.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb3c2aacea411cc7e1d27712490c11108e2de1d39019ae32915493a59a8b9ed", size = 258587, upload-time = "2026-08-06T13:49:28.692Z" }, + { url = "https://files.pythonhosted.org/packages/45/8c/fa34044f71b7cc4ecb6da9c2408770959b0591fa9b5fb6fb6bca38f94298/coverage-7.15.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce", size = 260785, upload-time = "2026-08-06T13:49:30.472Z" }, + { url = "https://files.pythonhosted.org/packages/4f/54/d5727ce36b4524a7394ab9f5f1df378e1f23affcdab01037dc8655185cc7/coverage-7.15.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c", size = 254545, upload-time = "2026-08-06T13:49:32.271Z" }, + { url = "https://files.pythonhosted.org/packages/dc/e6/6e3783e576719590194bdffb6dd6d85490801785b7c331e35a245d8cb8b5/coverage-7.15.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced", size = 256682, upload-time = "2026-08-06T13:49:34.089Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/bacdbde18b69ed2de424fcf64d9fb0a4913753d4f0eca8bae9daad69f4bd/coverage-7.15.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c38efe30fd74e5c19e9433f11fb1f5dc9c6522770971b7c6145bbaa413dc8800", size = 254560, upload-time = "2026-08-06T13:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a3/1fb927196e3477c1b48831169ab58ba08f451ba87ae311ff1de68b26a616/coverage-7.15.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1f4f826d70f772ab8b0c052329580d7fe8b8abd191e4ce0c8f81aec6614665d3", size = 258792, upload-time = "2026-08-06T13:49:38.01Z" }, + { url = "https://files.pythonhosted.org/packages/41/58/30d4c149c69053de0edfe325614c1d28d508f62b1783e0e4a234d2e49136/coverage-7.15.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768", size = 253968, upload-time = "2026-08-06T13:49:39.934Z" }, + { url = "https://files.pythonhosted.org/packages/89/e4/77f639371b918aad30dda4051f95404b43578f7f2e2f87ba73e02ed1ff37/coverage-7.15.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1c9bf40ebef178a45192c75c4964760bb261b0e6ad725da5fc4c93f674f19753", size = 255893, upload-time = "2026-08-06T13:49:41.825Z" }, + { url = "https://files.pythonhosted.org/packages/5c/62/13be29b3ddab35f14c87967a4820a05106d2a3eccb4fa4ff550bf30b75e0/coverage-7.15.4-cp315-cp315-win32.whl", hash = "sha256:43619d04c3671792d2c4706ae8bf45e265dc87bbd4078189ef8b847ea1e74be2", size = 224768, upload-time = "2026-08-06T13:49:44.08Z" }, + { url = "https://files.pythonhosted.org/packages/a1/70/af0c6be0f964af6954f6b74bc109b0dbca02824696d2520fb17fe1ab06e3/coverage-7.15.4-cp315-cp315-win_amd64.whl", hash = "sha256:be619439dbcd31a2eab10b32de9fff62c26ed4bab69dc32b8363fdaaa0882809", size = 225242, upload-time = "2026-08-06T13:49:45.899Z" }, + { url = "https://files.pythonhosted.org/packages/4f/2d/f3bd3aab899fc9efc18b53133ee68f5f98574ef480649b23e12962226387/coverage-7.15.4-cp315-cp315-win_arm64.whl", hash = "sha256:def597967dafc2e8d97c9097ea453c464e0bb8ed38f193a43070f10dc623bb6d", size = 224674, upload-time = "2026-08-06T13:49:48.322Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ca/f69251cd63eabc6438321aea22148754cce758a26bde07dd490e3fe7cfc5/coverage-7.15.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c7dbc748ac8a1e3e59a2b28bea47675e6e778081dbbf081bde0d75def2fcbe1d", size = 223333, upload-time = "2026-08-06T13:49:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a7/037b53b2885b0d8447064432491a4d5a1014cd9f97a594d53acd0c04541a/coverage-7.15.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:2413074a5ecbb61a01a7888fc72db0ca324d13588c5b38bc0dd8564cdcdfea26", size = 223630, upload-time = "2026-08-06T13:49:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/80/4f/152b8a4779ae90da11bb24f7467df8a59f0be48a5c52acb856325ca48289/coverage-7.15.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645", size = 264489, upload-time = "2026-08-06T13:49:54.52Z" }, + { url = "https://files.pythonhosted.org/packages/10/2d/84b4b9e0e1dd6528a51920ff7031f35b789382e467a28ec6a5a578cb8812/coverage-7.15.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a", size = 267567, upload-time = "2026-08-06T13:49:56.721Z" }, + { url = "https://files.pythonhosted.org/packages/53/fc/ba01cc25299f9f8a2c8b02d3b28c53f3543d9fbfbe4e74fa2760b48f163e/coverage-7.15.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624", size = 270123, upload-time = "2026-08-06T13:49:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d0/db2647cbf40b14f8c308f94ff7bf89c06d564e59f396906edf50086ec788/coverage-7.15.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1587fb771d1ccceef708fdde1e5af8c7ed24b486b61d13a321acb7d8145390aa", size = 271107, upload-time = "2026-08-06T13:50:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4d2d17924552c458bb4f77dd631f0e3bc92fbbdf2d2d916cd4b33bbfd5b1/coverage-7.15.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f", size = 264955, upload-time = "2026-08-06T13:50:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/ee/de/dc010c7a3691f396d93bbc26bfcafa1c2a3a351cd520470f15faf5795bd5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ffb58d7eff5b7f6ecc6fa21d6288ab7f968a212cb67d682c269c09b9eba3b66f", size = 267949, upload-time = "2026-08-06T13:50:05.557Z" }, + { url = "https://files.pythonhosted.org/packages/78/ea/dc96a11375e83c045c2f7c61fb6918277cfe9401db7c0f7b1d111a84b2e5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d9df165544774574ee004b953023d1bebada1894a80b1052a43d798b0f676e67", size = 264421, upload-time = "2026-08-06T13:50:07.612Z" }, + { url = "https://files.pythonhosted.org/packages/c8/86/b77131a0f9503ce461cd577076147d7a9040f0c5dda772686f729e2cc9cb/coverage-7.15.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f9de0a24a4079b53e523b5c5e2c5945ec251ab486652659955187cf255a259bc", size = 269121, upload-time = "2026-08-06T13:50:09.58Z" }, + { url = "https://files.pythonhosted.org/packages/24/24/944bc35007862955e7ebf05754e645419dcf5d7526c52735cfa2715e8ebf/coverage-7.15.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:150089274bdc9f940628552cb92844e0223c987f1902ab8efe9f45a2ec758d88", size = 264565, upload-time = "2026-08-06T13:50:11.722Z" }, + { url = "https://files.pythonhosted.org/packages/c7/cc/a3bb9f93e7e740659163e2ea584f8196ddcd2c456a5dbe15f6c50105fec1/coverage-7.15.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc", size = 266522, upload-time = "2026-08-06T13:50:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/49/dd/e0e40f3560d878d888c580698ff5ad1179f5e1c3ac949684ef66b41a3817/coverage-7.15.4-cp315-cp315t-win32.whl", hash = "sha256:ebd5a6d8466ff30836572f3ba2cae8a5e8f85029b1c6d5e2ed338dc472a5166a", size = 225068, upload-time = "2026-08-06T13:50:15.825Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/37732ea80eebc30e976e4cdab15c190bc42d96959a42e38ddf6f8c60468f/coverage-7.15.4-cp315-cp315t-win_amd64.whl", hash = "sha256:288bde2a2d7ab6b6c2d7252fcde8b524387f2d970bdba9658fc6f8bbcaef0f9b", size = 225895, upload-time = "2026-08-06T13:50:17.928Z" }, + { url = "https://files.pythonhosted.org/packages/c6/08/1e00f7923eaaba45fb3d51dd794125fc766304b1df264f3a9c6557bfb30e/coverage-7.15.4-cp315-cp315t-win_arm64.whl", hash = "sha256:68be5e1de60ff13c9095bbec0e5a7fa45b33b101752215b91345ea1f61c4a278", size = 225213, upload-time = "2026-08-06T13:50:19.981Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84", size = 214332, upload-time = "2026-08-06T13:50:22.192Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "docopt" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901, upload-time = "2014-06-16T11:18:57.406Z" } + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-retries" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/d3/b7a8bb09543af40009717a08a2ceba90b6d4c6f0cdf171404217d8f4c37d/httpx_retries-0.6.0.tar.gz", hash = "sha256:3e0b404969a564829d368417964fd21e6b400a10d17c92d29b8bc247ce8186e3", size = 21122, upload-time = "2026-07-06T00:52:30.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/c6/7f3d6ab3549267a1959161b38df4c0fb435eceaf2d531d8addfac01abaca/httpx_retries-0.6.0-py3-none-any.whl", hash = "sha256:d1e52a8f68a5df42de75ab89049d5020b2d0ab2f5f8bceacda008d12aa1257a3", size = 11776, upload-time = "2026-07-06T00:52:31.033Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "isort" +version = "5.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/f9/c1eb8635a24e87ade2efce21e3ce8cd6b8630bb685ddc9cdaca1349b2eb5/isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", size = 175303, upload-time = "2023-12-13T20:37:26.124Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/b3/8def84f539e7d2289a02f0524b944b15d7c75dab7628bedf1c4f0992029c/isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6", size = 92310, upload-time = "2023-12-13T20:37:23.244Z" }, +] + +[[package]] +name = "librt" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/9b/356320fbae2ac8467e21c5e73e1389c80468e4998c62cc7d3536cc51b614/librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162", size = 214338, upload-time = "2026-08-07T10:49:42.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/52/06790ced2ac7117f890c21bda43c39c958ec82aa665c0718e821d33ff939/librt-0.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22", size = 148039, upload-time = "2026-08-07T10:46:41.165Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1d/8e150b7fc449a1f33c8a760965cc1f43b14fc1577d9d0b50ab2701420e74/librt-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63", size = 153067, upload-time = "2026-08-07T10:46:42.418Z" }, + { url = "https://files.pythonhosted.org/packages/51/87/a162bc5a66a35599dc619ecb215145f4de7d68e886b479b6d12593139f7c/librt-0.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef", size = 493087, upload-time = "2026-08-07T10:46:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3a/aeea1fc620cf48060d3065b37614edbf97043c099d0f50782bc8ca61d897/librt-0.15.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f", size = 485608, upload-time = "2026-08-07T10:46:45.038Z" }, + { url = "https://files.pythonhosted.org/packages/52/ff/fe571ad416f0856fd0d5578ffc2e6dc531891e586e36b647bcf50569cab8/librt-0.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8", size = 498723, upload-time = "2026-08-07T10:46:46.35Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e1/7a65eb5dedb1f00aebd948cdd8e17add48bf066cab3514e9daf84ab45a6c/librt-0.15.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a", size = 516002, upload-time = "2026-08-07T10:46:47.599Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/59832b0ebfbd08c2742e6ece372ceb53f18bf1faef5d33c8daf3abebf749/librt-0.15.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18", size = 508607, upload-time = "2026-08-07T10:46:48.873Z" }, + { url = "https://files.pythonhosted.org/packages/ea/0d/37fa73f3b43ebd8259f91ae9102a15e5a54e65d581e48dea72df3e81d7a4/librt-0.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c", size = 530422, upload-time = "2026-08-07T10:46:50.45Z" }, + { url = "https://files.pythonhosted.org/packages/26/02/e046c6fe7a5881ac34623242192f484426ba8a75595fd18f22c53a3f530f/librt-0.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a", size = 534303, upload-time = "2026-08-07T10:46:51.693Z" }, + { url = "https://files.pythonhosted.org/packages/95/32/d5e6d861ab0366f3edf74f887ab0c9eb9f535aaf01d32b80b4f734daa179/librt-0.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091", size = 536084, upload-time = "2026-08-07T10:46:52.951Z" }, + { url = "https://files.pythonhosted.org/packages/2a/de/d69d725513fe53fc90c6d7a1f86e4428939bad2fb905b17fe4c18d413dde/librt-0.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40", size = 514307, upload-time = "2026-08-07T10:46:54.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/93/f8aded0d6682b4f25820fa86e0690f87f01df9fd7bd09ddb04d9167ad021/librt-0.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0", size = 557686, upload-time = "2026-08-07T10:46:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/ffeb6bdeb6cd862b4272fddc8ad05f938dd25d020ed517e631813917d80a/librt-0.15.0-cp311-cp311-win32.whl", hash = "sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb", size = 104917, upload-time = "2026-08-07T10:46:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/96/28/7e2313a3ffbf0b4de7ba3da58a09e488507b4bd1ea2b5e69378354a23415/librt-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db", size = 125886, upload-time = "2026-08-07T10:46:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/39/9e/04b8c3cde014ef255ee785730425268354543acc38902093a40afa0dc164/librt-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56", size = 111885, upload-time = "2026-08-07T10:46:58.787Z" }, + { url = "https://files.pythonhosted.org/packages/ba/39/99c25030e782bdfb7a21be8c05254806a2e4bbb05c8d50c2a2130acbfa05/librt-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e", size = 151021, upload-time = "2026-08-07T10:47:00.057Z" }, + { url = "https://files.pythonhosted.org/packages/14/43/f4b1bd1b2888798a1409808889a25ea1ba49eaabce7d681ed27734c2df9d/librt-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d", size = 155267, upload-time = "2026-08-07T10:47:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/0c/db/3ad9c965c72f1e1d6beeec44ec10a54e17be8ae042fbb4baade16cbadced/librt-0.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1", size = 503136, upload-time = "2026-08-07T10:47:02.45Z" }, + { url = "https://files.pythonhosted.org/packages/4b/07/5888a6d76acd62ebce66c61b74d94e9370b9c32929f111e487bb6546f8ed/librt-0.15.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa", size = 496670, upload-time = "2026-08-07T10:47:03.675Z" }, + { url = "https://files.pythonhosted.org/packages/29/39/ab57cc2f5b276156da02bb7f5a8921bada1cb1993ffec99acf811c602c23/librt-0.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd", size = 513688, upload-time = "2026-08-07T10:47:04.981Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/bdbb0b648b5c2befb031f4c6f3b1dd857415e8fb492a25a3c764a6681e6c/librt-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa", size = 531904, upload-time = "2026-08-07T10:47:06.211Z" }, + { url = "https://files.pythonhosted.org/packages/93/26/473c2e4b6c104e9e58e27ce95fc8005c8bd4fc36cae4f254371125a92db8/librt-0.15.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d", size = 524427, upload-time = "2026-08-07T10:47:07.592Z" }, + { url = "https://files.pythonhosted.org/packages/26/60/03b3abb82b41714671b907bf6989b228e31e6a8af52dec82b5b0728dc250/librt-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656", size = 543155, upload-time = "2026-08-07T10:47:08.866Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0e/9bb1f0a4affbd0a1888f4f79dc03ed2a299d9a2c26c59ab2a97dcbf11903/librt-0.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81", size = 546890, upload-time = "2026-08-07T10:47:10.327Z" }, + { url = "https://files.pythonhosted.org/packages/dc/84/6937a280d461f7de6e031ffb02edc2b7c3c90d49d630565ce8ff27cbc5f2/librt-0.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d", size = 555163, upload-time = "2026-08-07T10:47:11.798Z" }, + { url = "https://files.pythonhosted.org/packages/bc/95/2a2853c1ee014bf102116e7f897a04beeaeb2461b45b79af98bdfb95f1ef/librt-0.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0", size = 535812, upload-time = "2026-08-07T10:47:13.279Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4c/cf9601c1b4c5f09280acd5d83abdb2e68527a2be8257136eb42304218622/librt-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2", size = 573688, upload-time = "2026-08-07T10:47:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/47/6d/9ac7cbec46189a7625af4b5acbd25f10d827f4141b2002181848c8418923/librt-0.15.0-cp312-cp312-win32.whl", hash = "sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1", size = 106138, upload-time = "2026-08-07T10:47:15.973Z" }, + { url = "https://files.pythonhosted.org/packages/38/d0/2ae99c83be86ce23f925ac1aeeedc777e97f427c4a8d190c70d0a16e9a87/librt-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022", size = 126974, upload-time = "2026-08-07T10:47:17.049Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ef/dd24f9635c730b86b87587967dda7516b1845e8b17684603d31607fed598/librt-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570", size = 112292, upload-time = "2026-08-07T10:47:18.222Z" }, + { url = "https://files.pythonhosted.org/packages/e7/42/467b53a601b406ccd7b97c1fd54b59cb34f9185ad5ce7e9d5c3c4e8961c8/librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26", size = 151029, upload-time = "2026-08-07T10:47:19.312Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e6/36c2299b7a94b84fdd01220d8a777a71be5be0925bb0dbdf71c0a06a34d9/librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801", size = 155194, upload-time = "2026-08-07T10:47:20.398Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/ed5071f9325845e670bd36012757419767fbf56af77ed483077b9e4db541/librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc", size = 502568, upload-time = "2026-08-07T10:47:21.652Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/6450c67c3615d87704bcbc21323fafc69c799b06a044c447529f725d4b01/librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95", size = 496153, upload-time = "2026-08-07T10:47:22.925Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d6/5f52b722bc75076954b3bfd49be15ea362df4d580c6fb315d0f617100d30/librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b", size = 513336, upload-time = "2026-08-07T10:47:24.213Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e2/c08fd1d36ce63ea5a12b85c5d37f4550b5f86a692167e41e5a74222607ae/librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2", size = 531661, upload-time = "2026-08-07T10:47:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d8/d9482fcbeb177b9eb87bb3899eeb3b42be690313c652f9e146b1d0681fb2/librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3", size = 524487, upload-time = "2026-08-07T10:47:26.79Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/075171517b41f861753034fbb151b42cfc83bcc853849f24f5e66fd60ccf/librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785", size = 543201, upload-time = "2026-08-07T10:47:27.999Z" }, + { url = "https://files.pythonhosted.org/packages/b0/03/42c2330f37eeb475b6affeedd06518f60035f323af3a839335e3fc9fef2d/librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6", size = 546467, upload-time = "2026-08-07T10:47:29.396Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/1ad4c5638f7e64d8560328bd25c54b409a661bdb6ff254b38ff90744288d/librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101", size = 555139, upload-time = "2026-08-07T10:47:30.815Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/39fa7d15db1204cd1cbe6514680fbdc243adf754a0885061308f43afc013/librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218", size = 536050, upload-time = "2026-08-07T10:47:32.222Z" }, + { url = "https://files.pythonhosted.org/packages/1e/88/c6dcf0dd8e26dc0c9a499a2abab8646c86dcaf9ecea9524cb46d3686331a/librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b", size = 573700, upload-time = "2026-08-07T10:47:33.527Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9b/ab54c71a7918a7c34fa5327fb61390a77446a07a146fbfb1165250a61035/librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab", size = 82194, upload-time = "2026-08-07T10:47:34.835Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b2/4f9a243bb892395f3becb80789ade13771701091f9f07ab8230247953ba8/librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890", size = 106231, upload-time = "2026-08-07T10:47:36.251Z" }, + { url = "https://files.pythonhosted.org/packages/bf/af/64aff4885a40b93132382f2c314647d722574605416504379184ef3045ea/librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8", size = 126996, upload-time = "2026-08-07T10:47:37.453Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/335bccf6c7cb9028cb0b54aead27d9ece3f01f83bc6baa2abace5da655c1/librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad", size = 112188, upload-time = "2026-08-07T10:47:38.729Z" }, + { url = "https://files.pythonhosted.org/packages/a8/93/949053fb462eecc4a9a5ee770a81f4b40be7b79538b245545d4aebc6b58b/librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993", size = 149833, upload-time = "2026-08-07T10:47:39.86Z" }, + { url = "https://files.pythonhosted.org/packages/61/ca/8281aa6cd560a3420e4497729f6b704b53be3eeaaef82d5aeadddaf7441f/librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8", size = 154088, upload-time = "2026-08-07T10:47:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/dd/02/1a1662dceaba6a086360891448d5ce9a7d3555976cae59a31a39d744b9c7/librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21", size = 494215, upload-time = "2026-08-07T10:47:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/99211619dc656370a3740c33d2b0b6d5a3fb1e73689314f6ed477a397dc4/librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953", size = 491173, upload-time = "2026-08-07T10:47:43.683Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/5448d0b05f4579b635d3899176817ebf561af0e57bacd425b5b1887264c1/librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa", size = 505512, upload-time = "2026-08-07T10:47:45.314Z" }, + { url = "https://files.pythonhosted.org/packages/95/82/01940e40b83c43a546c4a3c896cf34ca272a9690899d55914e4827b3dcce/librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879", size = 523073, upload-time = "2026-08-07T10:47:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/88/fa/759c0030f3ee371439eb26de34fc745807caf0abb878af7af4b8b7c3dd3d/librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae", size = 515080, upload-time = "2026-08-07T10:47:48.319Z" }, + { url = "https://files.pythonhosted.org/packages/0b/27/894e072228fcb159703c655da69f8cd10dbed489c36e3df7dd032a2483be/librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd", size = 534164, upload-time = "2026-08-07T10:47:49.875Z" }, + { url = "https://files.pythonhosted.org/packages/98/a3/0078e91c1f36f8815db17827de15650b9a3fe56c55fbf998c854b34e40d3/librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285", size = 540616, upload-time = "2026-08-07T10:47:51.408Z" }, + { url = "https://files.pythonhosted.org/packages/86/33/81a29b796dd52a45e9ef7974c7732926e8f10f15b8d2be505665979f896d/librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239", size = 545890, upload-time = "2026-08-07T10:47:52.818Z" }, + { url = "https://files.pythonhosted.org/packages/05/82/8be1baa1350e5d30cfd70ae79d0a6f4dc5862ef47f7bb2808aabc9bb86e5/librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60", size = 523287, upload-time = "2026-08-07T10:47:54.165Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4f/d1be6a01a35c20ef734e0e44113f87d4af756a9354a89dcfbe3b4f8af5e1/librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65", size = 565868, upload-time = "2026-08-07T10:47:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/67/88/649cfa33f5825927b160610f670bdab012a64d627eddb94fa795ea4292fd/librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622", size = 81619, upload-time = "2026-08-07T10:47:56.886Z" }, + { url = "https://files.pythonhosted.org/packages/22/31/8e88a8d5e48fc8d1a817787fb6811dfff6499acd6c8683dd83934aa6ede0/librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15", size = 100138, upload-time = "2026-08-07T10:47:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/80/92/20fd6c4b6a1b1a564b076d55cd3d427d8428217d7638dc25a654cc4791d4/librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28", size = 121258, upload-time = "2026-08-07T10:47:59.564Z" }, + { url = "https://files.pythonhosted.org/packages/fc/28/6af430b44d9ebb897b865a3c363b6dcace51357be2347cc0f8f869656a86/librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95", size = 106467, upload-time = "2026-08-07T10:48:01.097Z" }, + { url = "https://files.pythonhosted.org/packages/7e/aa/b42bb798942ced219f6d63b27e07f91237887a8d0bd0921666db79a13790/librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714", size = 159523, upload-time = "2026-08-07T10:48:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/75/03/1b53cd4ef904e73b1d828a5f90143bf94a2967d7cfff0b9ccf93e12aa9b4/librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3", size = 161638, upload-time = "2026-08-07T10:48:03.725Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/9f9c9fba097d49e9e694c2b4dc331df31884645ecbc58a93b4b5fc69d2c5/librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d", size = 701795, upload-time = "2026-08-07T10:48:05.135Z" }, + { url = "https://files.pythonhosted.org/packages/4c/05/0966840bda0380c8ae167b9043c6230202941cc90ea29c48e096964c765e/librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38", size = 682147, upload-time = "2026-08-07T10:48:06.555Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1c47ca573c30ea47d195aec26133af522fea1104afaace028d7b32247ea8/librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19", size = 696397, upload-time = "2026-08-07T10:48:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0f/1aed6223d4f9f9d1171a8596ff100ea4c3f7699fea7a4ba657c3e60daa6c/librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab", size = 722542, upload-time = "2026-08-07T10:48:09.569Z" }, + { url = "https://files.pythonhosted.org/packages/c6/22/9e3a929aea456c97d69e6ef3884efea56d4807f97399471cc946baebd8af/librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2", size = 729709, upload-time = "2026-08-07T10:48:11.129Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1b/c327ef6018e3a9ca0b8e7c5eddeeb331ba8f9b76c24e126d37d0f6d62faf/librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108", size = 752891, upload-time = "2026-08-07T10:48:12.558Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d1/d5f1ea02c56930087009e39db9b70660a663e76c730b27b925d786718457/librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08", size = 745301, upload-time = "2026-08-07T10:48:14.55Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3c/5f7c585d15ebb2250c73e7c0ee4e9e47be72c65d520c07ddbcdc62037674/librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47", size = 747921, upload-time = "2026-08-07T10:48:16.453Z" }, + { url = "https://files.pythonhosted.org/packages/7f/52/1443a446486eba966bcbca1696b472e4f210320ec42f490a47f48fbf0fdc/librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81", size = 727561, upload-time = "2026-08-07T10:48:18.089Z" }, + { url = "https://files.pythonhosted.org/packages/79/91/2270a9380f11725cf83ce1925a5e32dd1dde2be9bba597f25c10a38644e7/librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc", size = 774417, upload-time = "2026-08-07T10:48:19.611Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/f4b1548d4f5b99186737fe27aec238e9823e8d5d23bf4df007c030689dc5/librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf", size = 104381, upload-time = "2026-08-07T10:48:21.048Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/134afad262def1de04c0843c376d02135f1168af43f22e09a52bd8394727/librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915", size = 127034, upload-time = "2026-08-07T10:48:22.561Z" }, + { url = "https://files.pythonhosted.org/packages/99/5f/1b6846b20572bd699c9e9ec321a5f781845bee477df2aa2a43b28bc40119/librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605", size = 110827, upload-time = "2026-08-07T10:48:23.804Z" }, + { url = "https://files.pythonhosted.org/packages/c6/44/4de9f4ddadb009a55c7758eb5736d62534a7daaf27bd71bc50e64b606b06/librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca", size = 149843, upload-time = "2026-08-07T10:48:25.148Z" }, + { url = "https://files.pythonhosted.org/packages/1f/eb/5d9ab71e30119c44094e0275f38b47dd327aea0f843a080396677029d508/librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965", size = 154510, upload-time = "2026-08-07T10:48:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/8505d1b8f5e8c19587bd03f7429993b3e9ce5c06819d856bfb11d919374c/librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad", size = 497543, upload-time = "2026-08-07T10:48:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/1d/9a/3a8390775cb095765aded027ac9c63e7c8ea74e731498607544c6505de0e/librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9", size = 480452, upload-time = "2026-08-07T10:48:29.531Z" }, + { url = "https://files.pythonhosted.org/packages/e7/40/258a4a7117ee915d66de5cd9b8ade65a440993161107ce3a686f1859955c/librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328", size = 507768, upload-time = "2026-08-07T10:48:31.007Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c6/2f4dd296c97a0b85b98894519b279408ec9dd602d4f692b1ea0e25dee670/librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0", size = 525122, upload-time = "2026-08-07T10:48:32.7Z" }, + { url = "https://files.pythonhosted.org/packages/49/dd/29eab42be13b2bf0ea8cb227135a45d44693e30a7e8b92871981ff56b82b/librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9", size = 520371, upload-time = "2026-08-07T10:48:34.294Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/4bad71adeca8fe208b775c2a35417fa5a2584c8f4791daaf89a89450fea1/librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659", size = 537258, upload-time = "2026-08-07T10:48:35.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/63/59dba6143fdcc7240c54458b629f3250000a61b8945890fc9efd451b19c5/librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0", size = 527432, upload-time = "2026-08-07T10:48:37.466Z" }, + { url = "https://files.pythonhosted.org/packages/ec/21/21a24c6a2327d8362580efebe77286bf47b0f4062ec5ea41766e609d3c7d/librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d", size = 548108, upload-time = "2026-08-07T10:48:39.384Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6d/fc68c89a7971418b41f9a873623ff935cb864097544c6a2f8ce491c8ef5d/librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c", size = 529681, upload-time = "2026-08-07T10:48:41.392Z" }, + { url = "https://files.pythonhosted.org/packages/65/7e/c2d98766124400d722063a630b0fde38a9fc768705d37eecca15c47dc192/librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d", size = 567736, upload-time = "2026-08-07T10:48:43.124Z" }, + { url = "https://files.pythonhosted.org/packages/55/6c/f8c34a95e3a515c6e1c192b89511e7253c89a7760c6b500d57ffdb8d2dc8/librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374", size = 81673, upload-time = "2026-08-07T10:48:44.645Z" }, + { url = "https://files.pythonhosted.org/packages/c9/9e/e23fa8e78679ec45728188650b39e8ff476c83b691c96f749217df3b1b7c/librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9", size = 100081, upload-time = "2026-08-07T10:48:46.171Z" }, + { url = "https://files.pythonhosted.org/packages/e1/dc/3eb4c5e297343f0620a55532cd7c8d764d3001fa2159212dadf480464827/librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8", size = 121228, upload-time = "2026-08-07T10:48:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/97/70/43abce19f04e49762f8ec834c8fafee13cc40fd6b94a72a24e534febfcd0/librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b", size = 106487, upload-time = "2026-08-07T10:48:49.095Z" }, + { url = "https://files.pythonhosted.org/packages/de/15/83f2deddb9368b8951ec8c9477269b5b9b8bd9bbf15e57402d0f38817dca/librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54", size = 159448, upload-time = "2026-08-07T10:48:50.649Z" }, + { url = "https://files.pythonhosted.org/packages/06/bf/043097353f9b3c73b583d07f6b8e552795463f4bfc8caf85e42eee50c26a/librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6", size = 161686, upload-time = "2026-08-07T10:48:52.174Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2a/8ae77f9719d42ce71cd708560a3557b38ac3c17a0383e57f87084de45bbe/librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988", size = 710668, upload-time = "2026-08-07T10:48:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/61/34/c0436ea134deb9a0d6da80a396a2739a81cb31e0418f7227239e23140898/librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13", size = 679396, upload-time = "2026-08-07T10:48:55.645Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/001e0d99aa9250d5cd5715a9081291a20656083459f9019cda15255329e1/librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416", size = 704313, upload-time = "2026-08-07T10:48:57.46Z" }, + { url = "https://files.pythonhosted.org/packages/2d/53/b34fa9d0ff00f136f4d58ebb4c411ff634baed1eb412bb602a2bc8dcafcb/librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a", size = 729847, upload-time = "2026-08-07T10:48:59.231Z" }, + { url = "https://files.pythonhosted.org/packages/86/ac/fa4d7a424665040e95baf480a6d523446057684b6758624c85338e8a23b2/librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c", size = 742736, upload-time = "2026-08-07T10:49:01.151Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/e17a9bb5de6fb8c3186ed1a7d68d21618b027ac2d3633e03d3b6109c67ae/librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302", size = 763454, upload-time = "2026-08-07T10:49:03.039Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ec/ecd02cd30935b931b9cdbfed6ab5a099c51b280b4e7baa274da80978ed27/librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d", size = 743296, upload-time = "2026-08-07T10:49:04.941Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b5/b3c2b8353ce820a4854f78d19321344242f89fa71c975b71132ba9bf242a/librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9", size = 756217, upload-time = "2026-08-07T10:49:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/3c/52/6cc22542ba59146b05cca2a656f9ff8bb67e38e63d12c3b0cc183d837bf1/librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab", size = 741934, upload-time = "2026-08-07T10:49:08.839Z" }, + { url = "https://files.pythonhosted.org/packages/40/32/a04b72b1aa86e3be23b2ecff8c1aad2dcc955bd3956d6d26e7e34267e57a/librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b", size = 783763, upload-time = "2026-08-07T10:49:10.661Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f0/89eb11dffbe9279ff37144dec786927314502ae0b114f1449dc78c458aab/librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162", size = 104313, upload-time = "2026-08-07T10:49:12.305Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4a/1f1978c200f563beda63c36adff2d65bbecb81e365e8e69e572f5f70fbc6/librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1", size = 126889, upload-time = "2026-08-07T10:49:13.881Z" }, + { url = "https://files.pythonhosted.org/packages/38/a6/800800bfed7b1fb10fc3f3d557785c3854e80d3f7a9800d784b176a1fc2d/librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc", size = 110700, upload-time = "2026-08-07T10:49:15.499Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mypy" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/af/4e516a05d3ca2eb9283e9ec45b2c02225c1514dd6da49fd3c9eaa6639370/mypy-2.3.0.tar.gz", hash = "sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e", size = 3988104, upload-time = "2026-07-13T11:34:53.387Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/b9/d75b3082b05f1b3028828aeb18e74ae5ab0a0936051bbf1f32f59f654747/mypy-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3419d00717afbc5265b50dd14b1278f29ea4884dd398ab67873489ac093fd329", size = 14838725, upload-time = "2026-07-13T11:32:44.655Z" }, + { url = "https://files.pythonhosted.org/packages/a9/50/79a65c6ea6e115bc73296038a4543b2d5c91f07912b918a2c616a2514bba/mypy-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cfca8ee88544090f86b6dcce05ec55d66eb48a762412ac2507810ba4bd793b6f", size = 13911128, upload-time = "2026-07-13T11:32:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/90/48/e11ed7716c26953ca321f726e452e374dbf81a6f2b8b212ec02af29b6b8f/mypy-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75cbb4b9ef04a0c84a957f07abc4504fbf64b8dcc145675101f2d3a78a4b1d6a", size = 14146742, upload-time = "2026-07-13T11:33:03.313Z" }, + { url = "https://files.pythonhosted.org/packages/06/72/6807565b1c4861ef66f7fdd98b51c61556356eab80235717b46c53bb8627/mypy-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:982e3d53dd23d0a4cef67dd66791fdbede0cf38f9eb617bf47663554c51e1e36", size = 15081418, upload-time = "2026-07-13T11:31:13.899Z" }, + { url = "https://files.pythonhosted.org/packages/00/80/1ea14c5d80e589e415973db3e47c78c2219a305b808b2b506395342c1d79/mypy-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85c5385b93012ffa3b31479ab579aef5415f4f3a32c6cf1ae07a984d2a0ff461", size = 15328164, upload-time = "2026-07-13T11:31:35.723Z" }, + { url = "https://files.pythonhosted.org/packages/37/28/8223157404a3d51920078459c37f80fbdc590e1d8ea049dc5ce48643022a/mypy-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:13b1b16e2fa39f3b2e33fb1c468abc7a69369fa2e886b4b87b5afc81472325cd", size = 11136472, upload-time = "2026-07-13T11:27:37.018Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cc/ea27e5959c5f258585a756b252031f3b313583d81b5064b2bebc41d3706b/mypy-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:b5cd2f027a972a4a5f2278a11fac9747f5f81a53a30b714d74950b6807e55568", size = 10135800, upload-time = "2026-07-13T11:30:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/0e7e592619e2133596a47cdd642534b0456545c218430bd3b9d8fefdd1b1/mypy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5", size = 15026523, upload-time = "2026-07-13T11:34:49.206Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/1e1731df090a857df2807177a4626863e5ac0f0256513c35780efe53986f/mypy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c", size = 14032189, upload-time = "2026-07-13T11:33:57.168Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/cab921f4a806e171f34113e6181dd23c55358ccf6a80741269ef594a410e/mypy-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491", size = 14198696, upload-time = "2026-07-13T11:32:12.767Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/e6d008bb19fe446e3662d85e0e2717bf9f2d611a2164fb29d6e067dbf46c/mypy-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7", size = 15286904, upload-time = "2026-07-13T11:34:27.594Z" }, + { url = "https://files.pythonhosted.org/packages/db/83/94397c9293608a364aa03e8084fb34ede4ae976a260384b9b52929308135/mypy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3", size = 15528342, upload-time = "2026-07-13T11:34:07.819Z" }, + { url = "https://files.pythonhosted.org/packages/cf/96/d8b37d819adec6cfccfb1fd3afc1735d94717ddeafb45536db9c6943e09b/mypy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c", size = 11218346, upload-time = "2026-07-13T11:28:27.745Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/cd9f725b19b19e5b530a154cf9bcf9e94279c5d55b3c34fb42b3aa48ea1b/mypy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595", size = 10204525, upload-time = "2026-07-13T11:31:02.552Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/f7d056eb0294586a572d0d0d89580ec633c064db520f11d37d5a2fb833bd/mypy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97", size = 14947298, upload-time = "2026-07-13T11:27:47.734Z" }, + { url = "https://files.pythonhosted.org/packages/32/d5/db3e7af01e7844d21662c6ddc1f7825ec7cb4053f0391ac02faf3638396f/mypy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac", size = 13950768, upload-time = "2026-07-13T11:27:57.726Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fb/43c031f0190513d1ec248ed037eceb742ddd2a4d74bbf406658a28173837/mypy-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6", size = 14151586, upload-time = "2026-07-13T11:29:18.615Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c3/f8b2ffc60883084da91be51af58e88a7ffd4ff9795acb7d902ff88d31eb1/mypy-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b", size = 15227411, upload-time = "2026-07-13T11:30:29.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/16b917fc7adcf03f1aadddfc93aab804ffb234b1ab09c0ffd6d92a5d34a2/mypy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2", size = 15478790, upload-time = "2026-07-13T11:33:14.686Z" }, + { url = "https://files.pythonhosted.org/packages/c0/88/aaa65a93c73d0cdae7e42f8adb302bf6885bb281302084f99d0290a35347/mypy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757", size = 11234919, upload-time = "2026-07-13T11:33:39.28Z" }, + { url = "https://files.pythonhosted.org/packages/35/19/b40de63f1a80e63bc2d40f0679a6a8dbd34e95176c8122119bdf406aa552/mypy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1", size = 10201510, upload-time = "2026-07-13T11:31:52.619Z" }, + { url = "https://files.pythonhosted.org/packages/a4/58/fa0ae047da911f540284009b4f44b96fe09d83c076d7c103e9d645f46303/mypy-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db", size = 14941909, upload-time = "2026-07-13T11:32:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/15/14/2ba1d61452d7c2a7fe12741e8d374e52b183476b07aa7f9e2a0d02b0720a/mypy-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762", size = 13967581, upload-time = "2026-07-13T11:30:00.587Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5a/483fb9e5ffbbb1a28dccc7b0a13d141b17ac769b6c9f488c0a0c63698962/mypy-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef", size = 14168807, upload-time = "2026-07-13T11:28:48.6Z" }, + { url = "https://files.pythonhosted.org/packages/ae/77/70d7a10732063beb74ad713682cf871e88f5c5fa39bfc8beff8a524bf9cb/mypy-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b", size = 15200144, upload-time = "2026-07-13T11:31:25.283Z" }, + { url = "https://files.pythonhosted.org/packages/56/72/766218ac783be4fdfcd699b90037b63017348a3e86fb2c1fbfb18302637d/mypy-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff", size = 15460389, upload-time = "2026-07-13T11:29:29.077Z" }, + { url = "https://files.pythonhosted.org/packages/38/4e/8a9db7411ecb8ec0cb1fd05dba432f28bafffcd38b4e887714a4a0506689/mypy-2.3.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4", size = 7753664, upload-time = "2026-07-13T11:29:08.147Z" }, + { url = "https://files.pythonhosted.org/packages/65/4c/c3f8bfd6ed0e5e38b5a244403b27f821d433443df5a15a278417c10a3a3c/mypy-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f", size = 11417237, upload-time = "2026-07-13T11:33:47.467Z" }, + { url = "https://files.pythonhosted.org/packages/3c/00/89a32eaf5ccf174bc4f90db0eaea5d70636c01b8d49f384bdab2e8834390/mypy-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7", size = 10389252, upload-time = "2026-07-13T11:31:43.81Z" }, + { url = "https://files.pythonhosted.org/packages/31/56/104f93d69aa9f339b6b9d3b0a7faa699b8b466c942cf3ae86cc2a2ec0915/mypy-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373", size = 16385495, upload-time = "2026-07-13T11:29:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/d2/03/f1d2123313f55efafdd27706960f43a771c62f1b68426c76043f3ab9ebf3/mypy-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556", size = 15098155, upload-time = "2026-07-13T11:30:40.301Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5d/d5f9200399b445e81726c4f23becee33f233aee81c72680b1ef3a258b641/mypy-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88", size = 15514155, upload-time = "2026-07-13T11:34:38.569Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ce/69977c555f08faa3190cfde44189b89dbd56861b1ab97aa18fc5f3a2e4a3/mypy-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe", size = 16766351, upload-time = "2026-07-13T11:33:29.195Z" }, + { url = "https://files.pythonhosted.org/packages/bc/92/6648b6caa3ab9e00f9ac0c2a78307805f873dd48139b24a6f6f7c3667bbf/mypy-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60", size = 17043490, upload-time = "2026-07-13T11:30:53.927Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ab/0dc91d80f3f016634c68d451f294a97320fe903a9b6f90b9e57b3f7f1717/mypy-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654", size = 12146869, upload-time = "2026-07-13T11:29:38.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/b5/4c964d02634ba81f4d1c84838e5c5b18ab06d13ed568960f5d6318495ccc/mypy-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5", size = 10965113, upload-time = "2026-07-13T11:28:07.056Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/fdc54fe583ba3cafbcedfb70eeeaf03849f75b1827a07096c7bd996f582d/mypy-2.3.0-py3-none-any.whl", hash = "sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88", size = 2753292, upload-time = "2026-07-13T11:33:18.48Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.11.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/98/0bf930c4f97d0266b58a89e36c015f56232c52b5d2f207215d48cca9e8f7/platformdirs-4.11.2.tar.gz", hash = "sha256:3a2ae5fca3520a01ab1be8b45613537f52ddf5b5f6f53d88233892dfbf0cd82d", size = 32716, upload-time = "2026-08-10T15:48:06.092Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/e2/4e6eee633809c376c024821b91ade709cbfd040ec53939ffbcc292aa7eee/platformdirs-4.11.2-py3-none-any.whl", hash = "sha256:7f89089b6ea71bda7962953edcf784b2e2d9d285b40ad88be2bb75c6e9d82ab4", size = 23361, upload-time = "2026-08-10T15:48:04.855Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pylint" +version = "4.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "dill" }, + { name = "isort" }, + { name = "mccabe" }, + { name = "platformdirs" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/92/98dace02f2d11b88160354c53944f77ea7327aa78bce1c75971e7aaa4347/pylint-4.0.7.tar.gz", hash = "sha256:9b2d1d15791c84b77a4fe2aafe8f0d9570717e2dea06d53b19c105cf60275a52", size = 1594770, upload-time = "2026-08-09T19:13:23.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/b0/3a8040e53df6c5c1e04b0e23ed53fdbeb64f333723a334d313fba2f581ce/pylint-4.0.7-py3-none-any.whl", hash = "sha256:be4a3111557a614411ed1fc89347ce4a8e1013a59e1f33d11485227a02e3304d", size = 539710, upload-time = "2026-08-09T19:13:21.228Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytest-runner" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/7d/60976d532519c3a0b41e06a59ad60949e2be1af937cf02738fec91bfd808/pytest-runner-6.0.1.tar.gz", hash = "sha256:70d4739585a7008f37bf4933c013fdb327b8878a5a69fcbb3316c88882f0f49b", size = 16056, upload-time = "2023-12-04T01:03:30.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/2b/73982c02d28538b6a1182c0a2faf764ca6a76a6dbe89a69288184051a67b/pytest_runner-6.0.1-py3-none-any.whl", hash = "sha256:ea326ed6f6613992746062362efab70212089a4209c08d67177b3df1c52cd9f2", size = 7186, upload-time = "2023-12-04T01:03:28.706Z" }, +] + +[[package]] +name = "pytest-watch" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "docopt" }, + { name = "pytest" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/47/ab65fc1d682befc318c439940f81a0de1026048479f732e84fe714cd69c0/pytest-watch-4.2.0.tar.gz", hash = "sha256:06136f03d5b361718b8d0d234042f7b2f203910d8568f63df2f866b547b3d4b9", size = 16340, upload-time = "2018-05-20T19:52:16.194Z" } + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rstcheck" +version = "6.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rstcheck-core" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/3e/54182f5a310e0e2eb287170eb590bb93011161e28e51aa499120ab5c9318/rstcheck-6.3.0.tar.gz", hash = "sha256:583072b43939627b1b0f4492e8af2587092efc1fb5e9f0228fa2df9ea3b44873", size = 30887, upload-time = "2026-07-28T19:49:07.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/13/8bdf4e9f028e147ebabd8e093d30b0bc6e70ebbcfa9dbd9b93861e7315cd/rstcheck-6.3.0-py3-none-any.whl", hash = "sha256:3c1d8753b5dc01c33d114711552b2c4855fde00e0713f5a8341d7aaab2777058", size = 8999, upload-time = "2026-07-28T19:49:05.664Z" }, +] + +[[package]] +name = "rstcheck-core" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/91/1d6dc8b0f4482b5c1d96aa35127963c55f5724e84390db6839e2c1973f3f/rstcheck_core-1.3.1.tar.gz", hash = "sha256:3fce3e9f9d250659db021b158e63a373298140dab9aa37ea917373c962bfa04e", size = 61224, upload-time = "2026-07-28T19:25:32.438Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/4c/b46fa11eb819094f0317622b07a560a097f751300d24c19959171a0f1398/rstcheck_core-1.3.1-py3-none-any.whl", hash = "sha256:21eb176c9cb1e6255bd4293d7d220710515cfe6f0862958fcddee8040aa30f88", size = 28703, upload-time = "2026-07-28T19:25:30.596Z" }, +] + +[[package]] +name = "seam" +version = "3.0.0b6" +source = { editable = "." } +dependencies = [ + { name = "httpx" }, + { name = "httpx-retries" }, + { name = "svix" }, +] + +[package.dev-dependencies] +dev = [ + { name = "black" }, + { name = "mypy" }, + { name = "pylint" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-runner" }, + { name = "pytest-watch" }, + { name = "rstcheck" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.23.0,<1" }, + { name = "httpx-retries", specifier = ">=0.6.0,<1" }, + { name = "svix", specifier = ">=1.24.0,<2" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "black", specifier = ">=26.5.1,<27" }, + { name = "mypy", specifier = ">=2.3.0,<3" }, + { name = "pylint", specifier = ">=4.0.7,<5" }, + { name = "pytest", specifier = ">=9.1.1,<10" }, + { name = "pytest-cov", specifier = ">=7.1.0,<8" }, + { name = "pytest-runner", specifier = ">=6.0.1,<7" }, + { name = "pytest-watch", specifier = ">=4.2.0,<5" }, + { name = "rstcheck", specifier = ">=6.3.0,<7" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "standardwebhooks" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/81/a26b812525dd9ad6cc0f6ba2f8eafd5bf595542e319d95123aa92b51bf22/standardwebhooks-1.1.0.tar.gz", hash = "sha256:e5cb66e21a6356ebb9375aeb57f1348583323015808d475a7c1baaa4b718068a", size = 3232, upload-time = "2026-07-21T15:49:51.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/dc/0cd2bd9a536e61ad0ee9de3b724492a38faa88ce1828fbc15940824664c3/standardwebhooks-1.1.0-py3-none-any.whl", hash = "sha256:9a88d48a1f198be61517fc7ad328cf58ba02b73f0fbd8941d4213e9c0b6c2a61", size = 3538, upload-time = "2026-07-21T15:49:50.519Z" }, +] + +[[package]] +name = "svix" +version = "1.99.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "deprecated" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "standardwebhooks" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/f5/f12d2a1254c9c891b4cfdf0e94bdf706057b0328df5f4ba9aeb6b2be3b78/svix-1.99.1.tar.gz", hash = "sha256:70374aeb3cc19fcff384e9e22a35fce16c57bf1c95a325b940dc0b5b765c7047", size = 55766, upload-time = "2026-07-23T16:33:52.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/fc/0d33bc99ede3fe0a029e47c947a3c2ad1b9cba1d6fcbc432765f5ac282b0/svix-1.99.1-py3-none-any.whl", hash = "sha256:c5e4af1cbcf19ed93b5a02c91f036850d10fc3d22a83092ecfcb83f637f5bb61", size = 156158, upload-time = "2026-07-23T16:33:50.79Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/96/e07752635b98536177fa1f37671c8f3cdde2e724c6bcf6034b2cfb571565/tomlkit-0.15.1.tar.gz", hash = "sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97", size = 180129, upload-time = "2026-07-17T01:48:04.562Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/bc/8c13eb66537dce1d2bd3a57132902f38d0e7f5bb46fa9f4daed9fe9d76ee/tomlkit-0.15.1-py3-none-any.whl", hash = "sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304", size = 49449, upload-time = "2026-07-17T01:48:05.728Z" }, +] + +[[package]] +name = "typer" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/40/4a3db7990d1f62a53182aa96eaef57aeb2886a27f90a195bc66713565d31/typer-0.27.1.tar.gz", hash = "sha256:a79bef8469a79c45498e7b814ecf8d603cc7644e9acbd9e19cac0334240b18df", size = 203994, upload-time = "2026-08-03T14:41:03.438Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/89/9518bc0c3929bee36b3a4a8e3daddd6e03f92f9961c66d4983b837160543/typer-0.27.1-py3-none-any.whl", hash = "sha256:53150287edd11baeb4e4722c8e394fcdf8181c0ae89485cba8d25c778d5edd56", size = 122874, upload-time = "2026-08-03T14:41:04.391Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "wrapt" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/b0/c1f5a970721f06b85c0cd5142e0ff8fe067708abd779b0c4f4be7d61d09f/wrapt-2.3.0.tar.gz", hash = "sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107", size = 131509, upload-time = "2026-07-28T06:06:14.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/b8/9182e4c618a847be0baccb68e4602b070d0fa22c782cf058f4bc66b32709/wrapt-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ab559e1b2551d23d54db2a0001c6d73bad022a254639561c5f6c382a9d6c2fe", size = 81427, upload-time = "2026-07-28T06:04:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/613cefd9c5977366b1587e61c0b428176d382e6d75b454084c5e58503042/wrapt-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bff9a671bc00709cab5a7f745c592b5671873449db0ee2a569af994f16b29a4d", size = 82360, upload-time = "2026-07-28T06:04:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/71/71/4cd2151a236f44a6e2dd4ed8011838d7ba0be3d656c8bafdfc65a2ed1917/wrapt-2.3.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc648a335d7e01adb3640b25f02fd0ea05886cf04d0af7f4ee902bc7b5e466e8", size = 161700, upload-time = "2026-07-28T06:04:22.723Z" }, + { url = "https://files.pythonhosted.org/packages/49/2c/bc508fee75eb2919ed69769800b09968e4aab16897f909a23f39c81e323f/wrapt-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0077f3d65541925fa83002f967b22ad6550d24813ac64cb905f717194128d9c", size = 162922, upload-time = "2026-07-28T06:04:24.177Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e5/04f34d38e66d857dfc2fc4088d60e70c0e422467822defa49b2b4a26e17b/wrapt-2.3.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9790ea25190a4e0fe4cdf4eeb868e9d75f8a024a70a5b6bf9c348a3a2b72e731", size = 156125, upload-time = "2026-07-28T06:04:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/23/41/c35940ea1c423f129ebe4361db853bc80d4def6326242e1206fa15bf94f4/wrapt-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:816877aa749253149f9ecfd2635d4d948ecfa338e1a0311d187b1acb1bb8a3eb", size = 162039, upload-time = "2026-07-28T06:04:27.154Z" }, + { url = "https://files.pythonhosted.org/packages/0e/60/9bda34c3d7d182aa703fe35339ae0ed4c4dad5e5c587f93890143e1f87fb/wrapt-2.3.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d1c2c1b808600d2ea808e6360910a60ed5f409a4011655e10f9164ba0a414a6", size = 155110, upload-time = "2026-07-28T06:04:28.497Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ba/60bfd9b1a751f4fcb2d603668fc272d651ccdd339a56acf8c40ad21a0293/wrapt-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5ba1e5e08ddc46130e9682b2c249f2d1dd39bda9106ed4bd401b7519f18f41bd", size = 161089, upload-time = "2026-07-28T06:04:29.959Z" }, + { url = "https://files.pythonhosted.org/packages/0f/32/2bd358c6f4f1305c813479d1e9ba746bebdd794f4a20107ab2b3ee0cbd45/wrapt-2.3.0-cp311-cp311-win32.whl", hash = "sha256:45c9279b373d15649dfa2c2077cb3408ea1a6d3125afbdab9d6b809a66f68e14", size = 78030, upload-time = "2026-07-28T06:04:31.241Z" }, + { url = "https://files.pythonhosted.org/packages/4a/62/ecc969b13b141fef89b888c9760821cb01a86ac8fc953911592c8e1e1522/wrapt-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:195b1842b4122fb54e3cd3dd5b2b4aa49302a5a61da901df0481f5c97aedde84", size = 80944, upload-time = "2026-07-28T06:04:32.655Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3d/9278ada8a2b3f24372b630361e84e9a7de7abc3784634860c26d1c37785a/wrapt-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:6db604ef0c67bdb2042ecdfd7b7f037cf09733557ca42360d1018285634f7b98", size = 80074, upload-time = "2026-07-28T06:04:33.811Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4a/d17a0fad1bf1c5f2c887ff71fef75654141b0880bff71d157d955b5bec3a/wrapt-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a45ffae742ce91a16e11cb6c7cd71e7f9994f3cbd283b962ab093f5c6dcf525", size = 82139, upload-time = "2026-07-28T06:04:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/6e/55/51b92daaf6defb57f4dc56bdcce985400f75c6984a03ca5e78ccac717028/wrapt-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:69e477046f2237ef0bc6547544ee73008dc764ca26eff44f09e976d221b34d5d", size = 82723, upload-time = "2026-07-28T06:04:36.502Z" }, + { url = "https://files.pythonhosted.org/packages/28/7f/cfd9bc4b1f5e424eeea83d0493e43f3b1b02707ce8e50c47945873982bd5/wrapt-2.3.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8", size = 172381, upload-time = "2026-07-28T06:04:37.674Z" }, + { url = "https://files.pythonhosted.org/packages/cb/89/ff7814f6eb6856b479946117d1138a2fbb46cdb6b1f379db359056c69743/wrapt-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb", size = 174120, upload-time = "2026-07-28T06:04:38.987Z" }, + { url = "https://files.pythonhosted.org/packages/12/1e/8eded8615d39e3ce81f626937a3a87b280a2a86239a2bf14a4b4bb345034/wrapt-2.3.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60", size = 163035, upload-time = "2026-07-28T06:04:40.361Z" }, + { url = "https://files.pythonhosted.org/packages/35/ea/a0af2d9da62897af2a055484920de05dade30d2ba2c0d65cbdea875d3d8b/wrapt-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02", size = 171887, upload-time = "2026-07-28T06:04:41.614Z" }, + { url = "https://files.pythonhosted.org/packages/7e/dd/63cd4c864c65ef4906df64bd2d378f4a62b54f28063f282dfb3bf93caead/wrapt-2.3.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3", size = 161113, upload-time = "2026-07-28T06:04:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ee/82f1fc9e431b5c2c5a6d201aa865dbeae3984c311c6d11a185f0c8367cf6/wrapt-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d", size = 170530, upload-time = "2026-07-28T06:04:44.212Z" }, + { url = "https://files.pythonhosted.org/packages/37/a5/5dc590e863a419930d988f8b7ca3e75a6befcfb10b6003b3a152f3d5f732/wrapt-2.3.0-cp312-cp312-win32.whl", hash = "sha256:fb8e2e6704a1e0b1b989546c69e2688371ef4a07fa5f61bde3eb6211186f5ac1", size = 78323, upload-time = "2026-07-28T06:04:45.484Z" }, + { url = "https://files.pythonhosted.org/packages/51/f9/4a6925a07951df56394f7e6ebe14f69f1c5ef9d87aa63e0839acf15aa63a/wrapt-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cdc021cb0b62471d6aac7f2bd92f3b4658073775f9ee7fcd325c511129e7bcc8", size = 81180, upload-time = "2026-07-28T06:04:47.021Z" }, + { url = "https://files.pythonhosted.org/packages/a8/4f/8b5de0395b2a72216751d41c9861df6facaeb611b619d8810ed2b3b23eb2/wrapt-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:67bfe2485f50368c3fcd2275fc1fd100e350d601e0058921a7c82678a465aeab", size = 80155, upload-time = "2026-07-28T06:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6e/0f88a072483e76b881e3fdcd6b6ffb4a5791002514fe541e72b1b73c859a/wrapt-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d3fb71e65b001adfc42684522eeccd9c21d8ba679945abc993439567b66e59f", size = 81960, upload-time = "2026-07-28T06:04:49.622Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ff/b7e2776e7c294075eb712cc9ef573d1b818f393006d09787262b8fc871c4/wrapt-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51a7a4181c1295774812271fbcd7c909df372bc25579d4ed9eb875caaf0ae86f", size = 82435, upload-time = "2026-07-28T06:04:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/d8/90/343bb5d0f1f9669bc252a6073f085b4abf862511bd5c9c9eaec754341f1d/wrapt-2.3.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9045917809c63fdf7abe3a2ceaed3d670b8ee4500ddd9291192d30aeb34467c5", size = 170350, upload-time = "2026-07-28T06:04:52.187Z" }, + { url = "https://files.pythonhosted.org/packages/59/f8/13b79a392930bd0dd6b86cbfbfe1c40944110456e1dc6d809e5c46ece904/wrapt-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54ca1d5573f69b5fe1d74f1f65799c68015e82f685efec9fd8cfa40a094c44d0", size = 170022, upload-time = "2026-07-28T06:04:53.599Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fc/4f1b6918f5290db959d6e0c07f77385d87cede29c39c9cf8f145e9c82954/wrapt-2.3.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:242b60c21e30866e6a2fa606c612b47c553fa60c0eaeeeb7797fb842ac0ce609", size = 161043, upload-time = "2026-07-28T06:04:54.936Z" }, + { url = "https://files.pythonhosted.org/packages/01/e1/45d3cf74414780bdff6d0380467e003f6eb0f028b6c9403db868dbc7209c/wrapt-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3f3d7ec0a51fbfe00d3aef047641ff2c58b25565b4717fc1f90e050be01cba8", size = 168576, upload-time = "2026-07-28T06:04:56.261Z" }, + { url = "https://files.pythonhosted.org/packages/f3/73/2fa58dd97f191c997755e2c6d569a68f0c433db4e4b36099bdd7227b6cac/wrapt-2.3.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:261f53870cd4fb2bf38f9f972c56c728fd224cb7c65721307de59d9e7e6741ae", size = 159140, upload-time = "2026-07-28T06:04:57.754Z" }, + { url = "https://files.pythonhosted.org/packages/29/a8/08a56e2000a8816d449dcbad8c8b081697acbbd490821ceca0f9d8e8d20c/wrapt-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8159ec0b0cb7608175eb150de94c19e34f4d47ac655f5ca9baf45df6b688ffd3", size = 169263, upload-time = "2026-07-28T06:04:59.161Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d4/354e1725e35a73b2af4fa70a3e024c7a5d1bf1802dfb862dcb668aae0253/wrapt-2.3.0-cp313-cp313-win32.whl", hash = "sha256:10461884b3014fbfc8eb7d09a93c5f246363e6711d9d881f95eb8c27fdef049f", size = 78241, upload-time = "2026-07-28T06:05:00.507Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7e/34c87fa2174848dfee820322aaa318bab08913998ccecc8d2f57b4ad4639/wrapt-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:ac870cc97b73bb00ac353329e9559a4bebc47c4c86792ed9b23b58c15b6ad838", size = 81113, upload-time = "2026-07-28T06:05:01.839Z" }, + { url = "https://files.pythonhosted.org/packages/11/86/fcc9a530579e008c9478bb565a6cdfbfd33536660f069c8b91a6607c5050/wrapt-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:a65e8db2b4e90c2e7ade931086351c98ef420bf7a94ee08c95ac8a3cbbc43579", size = 80182, upload-time = "2026-07-28T06:05:03.152Z" }, + { url = "https://files.pythonhosted.org/packages/96/50/3864848b95b28ef73e17551fc8dccbff2628a834f52cf26a57f9c419fb83/wrapt-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:fd1f2f557dd3491fe75905e578f4db967393d40d1a8f468edc4d40ac7f2d5944", size = 83921, upload-time = "2026-07-28T06:05:04.476Z" }, + { url = "https://files.pythonhosted.org/packages/3b/4c/3d1921a60c3e8c71c540ff136e6a47a1fbccf7f671e818394889f7871d9c/wrapt-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9f5d2aec29dfc76c37e23897dee92766a3fd4f3bff3ae7fc9c6b4bf37d8c1360", size = 84412, upload-time = "2026-07-28T06:05:05.921Z" }, + { url = "https://files.pythonhosted.org/packages/fa/1a/4a796ff7adb26ada6d4b758c94d47a38320b085e7099afc088efbbcdb006/wrapt-2.3.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:646d20d413ffcd1b0a2f700076e2d0252d872dcb7754860a73e45a59ea883614", size = 207168, upload-time = "2026-07-28T06:05:07.256Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3e/d7777776806c579b761bac2f91721dda9f04c7a1b380213c5935cc750ae6/wrapt-2.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:379f670f45b7bb8993edd9f6fc36c6cc65edb81cffa0b504be34acb0303fff0a", size = 214351, upload-time = "2026-07-28T06:05:08.945Z" }, + { url = "https://files.pythonhosted.org/packages/63/27/2d64d394df7bf181955b3bb562bf33c4492fb4be113f53071106d43ad8b5/wrapt-2.3.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6208f302f110295d64b22a7ac96500c791bf492dce4366e622e4912b077c9687", size = 199020, upload-time = "2026-07-28T06:05:10.418Z" }, + { url = "https://files.pythonhosted.org/packages/3e/3d/fb31d3db7d9834d265fb1a27a2adf0ddf51557c67458c97b22439ad6ae3d/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ed635a9ca4f3a5a2b900c10c69e823373bc00ebc114b459383596d3487da3570", size = 209969, upload-time = "2026-07-28T06:05:11.983Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d1/8724b5da582e62070dc9bf4d8bf1972f317297eefd7ba1f2b5c6393ccf6c/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e3b9eaa742ae7a0aaaaad4ca4b69469d757af2d6e6663ef1dadc47adec0aeb41", size = 196324, upload-time = "2026-07-28T06:05:13.557Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/3d9ef411149543016ee6bcf3af707f787cebd946527452b94bf122e9b7b4/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0f7284f88f4833705132d06d3b425a43095c2cbd07c58166aac3ab646ba12a4", size = 202610, upload-time = "2026-07-28T06:05:15.048Z" }, + { url = "https://files.pythonhosted.org/packages/13/9b/4fc042ceb757866dd4a5fc057b3b736f2b360d3703ce9f830d83dc9226e0/wrapt-2.3.0-cp313-cp313t-win32.whl", hash = "sha256:7ebb274aba688b043429eb1500ff8a76ce0cb8ac0812ca3e301f06247b8722b3", size = 79178, upload-time = "2026-07-28T06:05:16.469Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ff/b94878f8eed809ca042685276bcea9f24e8c2ca7c9653bb80bbb920a68a5/wrapt-2.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c4bded758ad6f03b965830944a2f0bc5b2eb3767fe5a7310134315d1a6610e98", size = 82634, upload-time = "2026-07-28T06:05:18.026Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/663e1de5332a71685a729754312d327d4cada767c36e1c5a2db4c8de49e6/wrapt-2.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:d2cc64539da63e39ffb9c7ede849b6e8ddaaf7b3876b5cfb04efd85a5f3f4eb6", size = 81387, upload-time = "2026-07-28T06:05:19.417Z" }, + { url = "https://files.pythonhosted.org/packages/58/10/b073beaea89bc0d3670a75ff51139430a54b6af7ba7796507730634536dd/wrapt-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea52a0d0f08c584943d5764be0e84efa912c8da23c23e1e285ff2f5641c18fcc", size = 81978, upload-time = "2026-07-28T06:05:21.133Z" }, + { url = "https://files.pythonhosted.org/packages/b3/31/0916d9cebf848ed3f1a0c1888faee421747df77331e4db2bc527a9a85988/wrapt-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd85b0aa88efdb189d6ae2f35f4526943a8f091c38599c9c31478241c819e6a1", size = 82518, upload-time = "2026-07-28T06:05:22.562Z" }, + { url = "https://files.pythonhosted.org/packages/f5/73/31c1bf0f3384062751c2094dadb314916d70aa9b6bfd26d994b4a7b393fa/wrapt-2.3.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945", size = 170187, upload-time = "2026-07-28T06:05:23.904Z" }, + { url = "https://files.pythonhosted.org/packages/ed/25/fce087d54b79b8905f3c3c9dd5f454bbd8d8acb80b960c4a6aee5b4659b3/wrapt-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5", size = 169288, upload-time = "2026-07-28T06:05:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/c7/30/0d09e6dddc6b7a7230ac77f50254b5980ab4fcd22976f72f8cc8a0404458/wrapt-2.3.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3", size = 160932, upload-time = "2026-07-28T06:05:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ca/0913af0d2ec0c43865d32d615f518fea66c13c5c930e489e9b0de248e9a8/wrapt-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07", size = 169017, upload-time = "2026-07-28T06:05:28.501Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f2/3d1e47ea81b822210f5df1bf942fd90780a75c055243d569b664529dea88/wrapt-2.3.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f", size = 159065, upload-time = "2026-07-28T06:05:30.01Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/ef2066ced8e5fca204e2b361e9708e36555b40949c583d997ea3b590817d/wrapt-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23", size = 168821, upload-time = "2026-07-28T06:05:31.649Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/016104650d4e572fa91506eb396b3dd8efbccc9284fdc1c9479c3d21db28/wrapt-2.3.0-cp314-cp314-win32.whl", hash = "sha256:2935d5454b3f179a29b12cf390ee47246740ba2c3a7545b1b46ba31a5f2a4a0b", size = 78700, upload-time = "2026-07-28T06:05:33.391Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/6fdc20a9f2ca304748b3f0819cbf377d55260562777bf0b615431bc3c181/wrapt-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:cc2cea812e5cb179a796b766747e7d3b21088760d8deb95676d482b8c8e6fa7d", size = 81422, upload-time = "2026-07-28T06:05:34.774Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a4/9cbd53bf05746bea2c392af39cb052427a8ec95cbd494d930733d8f44681/wrapt-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:22cc5c0a717bd4da87018ae0bffd4c19c6fb679d3ff357216ba566ab26c76cab", size = 80639, upload-time = "2026-07-28T06:05:36.228Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/6c5e4a0f66ea0d2b2dd267e8dd05a0014eea56840b3c8595d40b0a5d1f91/wrapt-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a6b5984cd65dd639546f0eb4b8eacf1c31cb2fe9fb5c27bffe240987cdb2cf84", size = 84030, upload-time = "2026-07-28T06:05:37.714Z" }, + { url = "https://files.pythonhosted.org/packages/6a/eb/a1aedf03283bc9cbf8a1783995ddc54e3c5a86878f19002d2c428494f4c5/wrapt-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c88abcf53daef80e01a75c7530e727fa6e2c1888fe83e3dcdba4c96216a1f5c7", size = 84419, upload-time = "2026-07-28T06:05:39.131Z" }, + { url = "https://files.pythonhosted.org/packages/63/61/50d511c0dc5105563849e86daa3e16ac7feef699f79fb05af45ea70107d5/wrapt-2.3.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2", size = 207171, upload-time = "2026-07-28T06:05:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/3f/59/9b538cf7795217e810699d16bc88b96a830d9b5c403eb2ec2db6b5f2ae81/wrapt-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c", size = 214329, upload-time = "2026-07-28T06:05:42.287Z" }, + { url = "https://files.pythonhosted.org/packages/b3/28/9935d62b1499e5c8b3d191e99ba4eb31ca237a0b699142011a837e9dc7ea/wrapt-2.3.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295", size = 199079, upload-time = "2026-07-28T06:05:43.958Z" }, + { url = "https://files.pythonhosted.org/packages/2b/01/4446b80fa2ffa47a3449b250d004ba1c1937f07f64a179608fec735df866/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd", size = 209992, upload-time = "2026-07-28T06:05:45.677Z" }, + { url = "https://files.pythonhosted.org/packages/d4/07/56f26c9f9979586a021e8148747004aba4498f49458c90b0502969b904e1/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df", size = 196334, upload-time = "2026-07-28T06:05:47.608Z" }, + { url = "https://files.pythonhosted.org/packages/8b/41/6d7bcc895b0f28b2250e10908f060687b9165429dcd7f22ddb3d4c031b74/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109", size = 202644, upload-time = "2026-07-28T06:05:49.183Z" }, + { url = "https://files.pythonhosted.org/packages/cd/25/7860927edba06b758b8852a6f02e832be715563c67a6795d94350bc81099/wrapt-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:629d73378082c00a8173031f9fb30a3ac6abbc894a5bfdfae71fabc60642d501", size = 79685, upload-time = "2026-07-28T06:05:50.976Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0f/270bafe92fde3b069a39bc01e39ee79340895b335640df861d43d2a51885/wrapt-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:42869085687f0aefd57c0f636c3f9354f8ffb321a8ba9cb52d19beb796e561c5", size = 83104, upload-time = "2026-07-28T06:05:52.405Z" }, + { url = "https://files.pythonhosted.org/packages/55/b3/af176d79a8515a8a720eccdad9a96f6e31a30abf2865430c8c42adf2fd13/wrapt-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51", size = 81774, upload-time = "2026-07-28T06:05:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/00/39/3daf9f47be208606586de4568ba6713db53ebc8fd7a575aea1fe57983b69/wrapt-2.3.0-py3-none-any.whl", hash = "sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2", size = 61866, upload-time = "2026-07-28T06:06:12.9Z" }, +]