diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ff261ba..62c2d13 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -3,7 +3,6 @@ FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} USER vscode -RUN curl -sSf https://rye.astral.sh/get | RYE_VERSION="0.44.0" RYE_INSTALL_OPTION="--yes" bash -ENV PATH=/home/vscode/.rye/shims:$PATH +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c17fdc1..e01283d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,7 +7,7 @@ "context": ".." }, - "postStartCommand": "rye sync --all-features", + "postStartCommand": "uv sync --all-extras", "customizations": { "vscode": { @@ -20,7 +20,7 @@ "python.defaultInterpreterPath": ".venv/bin/python", "python.typeChecking": "basic", "terminal.integrated.env.linux": { - "PATH": "/home/vscode/.rye/shims:${env:PATH}" + "PATH": "${env:PATH}" } } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8f9721..1713129 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,14 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'stl-preview-head/**' - - 'stl-preview-base/**' + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' pull_request: branches-ignore: - 'stl-preview-head/**' @@ -17,26 +19,23 @@ jobs: timeout-minutes: 10 name: lint runs-on: ${{ github.repository == 'stainless-sdks/blooio-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - name: Install Rye - run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: '0.10.2' - name: Install dependencies - run: rye sync --all-features + run: uv sync --all-extras - name: Run lints run: ./scripts/lint build: - if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') timeout-minutes: 10 name: build permissions: @@ -44,31 +43,32 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/blooio-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - name: Install Rye - run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: '0.10.2' - name: Install dependencies - run: rye sync --all-features + run: uv sync --all-extras - name: Run build - run: rye build + run: uv build - name: Get GitHub OIDC Token - if: github.repository == 'stainless-sdks/blooio-python' + if: |- + github.repository == 'stainless-sdks/blooio-python' && + !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); - name: Upload tarball - if: github.repository == 'stainless-sdks/blooio-python' + if: |- + github.repository == 'stainless-sdks/blooio-python' && + !startsWith(github.ref, 'refs/heads/stl/') env: URL: https://pkg.stainless.com/s AUTH: ${{ steps.github-oidc.outputs.github_token }} @@ -81,15 +81,12 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/blooio-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - name: Install Rye - run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: '0.10.2' - name: Bootstrap run: ./scripts/bootstrap diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index d75aae2..208e780 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -14,15 +14,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - name: Install Rye - run: | - curl -sSf https://rye.astral.sh/get | bash - echo "$HOME/.rye/shims" >> $GITHUB_PATH - env: - RYE_VERSION: '0.44.0' - RYE_INSTALL_OPTION: '--yes' + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: '0.9.13' - name: Publish to PyPI run: | diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index fb0881c..8585f44 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'Blooio/blooio-python-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check release environment run: | diff --git a/.gitignore b/.gitignore index 95ceb18..3824f4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .prism.log +.stdy.log _dev __pycache__ diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1214610..2601677 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.5" + ".": "1.1.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 5906eac..453579c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 12 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio%2Fblooio-2d1e4d94ef5b48617299b653eb3509f32fe4e9d6315ee9f3bac50b83635d736b.yml -openapi_spec_hash: 27229154f39588fcc348a91e6b293664 -config_hash: 8cae0b4155e1a7413f8d8693d7253e37 +configured_endpoints: 54 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-da8c56bcab4e4ad076aacfdd5f7d5650de8ad6c3a4bb4be17ee3dca2e14724bc.yml +openapi_spec_hash: 11d9b0407bffe03ac9ca3a91f11b2c3d +config_hash: cade0be519ddc10336c13b95a97fa66b diff --git a/Brewfile b/Brewfile index 492ca37..c43041c 100644 --- a/Brewfile +++ b/Brewfile @@ -1,2 +1,2 @@ -brew "rye" +brew "uv" diff --git a/CHANGELOG.md b/CHANGELOG.md index 68fe48c..859b286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## 1.1.0 (2026-04-30) + +Full Changelog: [v1.0.5...v1.1.0](https://github.com/Blooio/blooio-python-sdk/compare/v1.0.5...v1.1.0) + +### Features + +* **api:** manual updates ([9d3f025](https://github.com/Blooio/blooio-python-sdk/commit/9d3f02507a83ab0e91689709e85ba1dccefba362)) +* **client:** add custom JSON encoder for extended type support ([c02699e](https://github.com/Blooio/blooio-python-sdk/commit/c02699e0bb5a7411efe9ba37898f74bce58fc4a8)) +* **client:** add support for binary request streaming ([0a46f0a](https://github.com/Blooio/blooio-python-sdk/commit/0a46f0a17f78a2a21aae637af66fb6b5671c12c5)) + + +### Chores + +* **ci:** upgrade `actions/github-script` ([5e006c2](https://github.com/Blooio/blooio-python-sdk/commit/5e006c28777fae0e393b573d148432644a5d181b)) +* format all `api.md` files ([3d045e8](https://github.com/Blooio/blooio-python-sdk/commit/3d045e8d5b9cc2afca1d6c85ded4b0e531c28228)) +* **internal:** add request options to SSE classes ([3d004d8](https://github.com/Blooio/blooio-python-sdk/commit/3d004d8b3892f4f8a212ef00745919dc59a2e099)) +* **internal:** bump dependencies ([e6b7ec8](https://github.com/Blooio/blooio-python-sdk/commit/e6b7ec8b91ed416baf78c830519f226a4a361a98)) +* **internal:** codegen related update ([bdc07aa](https://github.com/Blooio/blooio-python-sdk/commit/bdc07aa8716f9de74bebb431a448dbf1f19a0837)) +* **internal:** fix lint error on Python 3.14 ([34988f7](https://github.com/Blooio/blooio-python-sdk/commit/34988f7b47fd8e01bbb8a0ba61abf52d66a9598a)) +* **internal:** make `test_proxy_environment_variables` more resilient ([fa78df3](https://github.com/Blooio/blooio-python-sdk/commit/fa78df3c42ab33e1f47de28ff366be72c2732134)) +* **internal:** make `test_proxy_environment_variables` more resilient to env ([a70f1e5](https://github.com/Blooio/blooio-python-sdk/commit/a70f1e597d63bd7c03f5262d29b32a16ca7eff4f)) +* **internal:** remove mock server code ([6d1b141](https://github.com/Blooio/blooio-python-sdk/commit/6d1b141349ac7e1eefe21ef50dc3b5d8de6707d6)) +* **internal:** update `actions/checkout` version ([3f2ef54](https://github.com/Blooio/blooio-python-sdk/commit/3f2ef546ebf7281cfca3d17255de0cdc288e1013)) +* update mock server docs ([b1c88e2](https://github.com/Blooio/blooio-python-sdk/commit/b1c88e2d8f1aac114dac3b288d540cae6f35d70c)) +* update SDK settings ([4a31da6](https://github.com/Blooio/blooio-python-sdk/commit/4a31da6cf0415becb34c3a5f83e6616a34d3f18f)) +* update SDK settings ([d02f331](https://github.com/Blooio/blooio-python-sdk/commit/d02f3312e2cd11a7e5492653d7950df76a4e6c36)) + ## 1.0.5 (2025-12-19) Full Changelog: [v1.0.4...v1.0.5](https://github.com/Blooio/blooio-python-sdk/compare/v1.0.4...v1.0.5) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 59291dc..2bd91f0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,32 +1,32 @@ ## Setting up the environment -### With Rye +### With `uv` -We use [Rye](https://rye.astral.sh/) to manage dependencies because it will automatically provision a Python environment with the expected Python version. To set it up, run: +We use [uv](https://docs.astral.sh/uv/) to manage dependencies because it will automatically provision a Python environment with the expected Python version. To set it up, run: ```sh $ ./scripts/bootstrap ``` -Or [install Rye manually](https://rye.astral.sh/guide/installation/) and run: +Or [install uv manually](https://docs.astral.sh/uv/getting-started/installation/) and run: ```sh -$ rye sync --all-features +$ uv sync --all-extras ``` -You can then run scripts using `rye run python script.py` or by activating the virtual environment: +You can then run scripts using `uv run python script.py` or by manually activating the virtual environment: ```sh -# Activate the virtual environment - https://docs.python.org/3/library/venv.html#how-venvs-work +# manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work $ source .venv/bin/activate -# now you can omit the `rye run` prefix +# now you can omit the `uv run` prefix $ python script.py ``` -### Without Rye +### Without `uv` -Alternatively if you don't want to install `Rye`, you can stick with the standard `pip` setup by ensuring you have the Python version specified in `.python-version`, create a virtual environment however you desire and then install dependencies using this command: +Alternatively if you don't want to install `uv`, you can stick with the standard `pip` setup by ensuring you have the Python version specified in `.python-version`, create a virtual environment however you desire and then install dependencies using this command: ```sh $ pip install -r requirements-dev.lock @@ -45,7 +45,7 @@ All files in the `examples/` directory are not modified by the generator and can ```py # add an example to examples/.py -#!/usr/bin/env -S rye run python +#!/usr/bin/env -S uv run python … ``` @@ -72,7 +72,7 @@ Building this package will create two files in the `dist/` directory, a `.tar.gz To create a distributable version of the library, all you have to do is run this command: ```sh -$ rye build +$ uv build # or $ python -m build ``` @@ -85,13 +85,6 @@ $ pip install ./path-to-wheel-file.whl ## Running tests -Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. - -```sh -# you will need npm installed -$ npx prism mock path/to/your/openapi.yml -``` - ```sh $ ./scripts/test ``` diff --git a/README.md b/README.md index 870bb32..f07e800 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ It is generated with [Stainless](https://www.stainless.com/). ## Documentation -The full API of this library can be found in [api.md](api.md). +The REST API documentation can be found on [blooio.com](https://blooio.com). The full API of this library can be found in [api.md](api.md). ## Installation @@ -33,7 +33,7 @@ client = Blooio( ) me = client.me.retrieve() -print(me.valid) +print(me.organization_id) ``` While you can provide an `api_key` keyword argument, @@ -57,7 +57,7 @@ client = AsyncBlooio( async def main() -> None: me = await client.me.retrieve() - print(me.valid) + print(me.organization_id) asyncio.run(main()) @@ -91,7 +91,7 @@ async def main() -> None: http_client=DefaultAioHttpClient(), ) as client: me = await client.me.retrieve() - print(me.valid) + print(me.organization_id) asyncio.run(main()) @@ -106,6 +106,40 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. +## Nested params + +Nested parameters are dictionaries, typed using `TypedDict`, for example: + +```python +from blooio import Blooio + +client = Blooio() + +contact_card = client.me.numbers.contact_card.update( + number="number", + sharing={}, +) +print(contact_card.sharing) +``` + +## File uploads + +Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. + +```python +from pathlib import Path +from blooio import Blooio + +client = Blooio() + +client.groups.icon.set( + group_id="grp_abc123def456", + icon=Path("/path/to/file"), +) +``` + +The async client uses the exact same interface. If you pass a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance, the file contents will be read asynchronously automatically. + ## Handling errors When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `blooio.APIConnectionError` is raised. @@ -233,7 +267,7 @@ response = client.me.with_raw_response.retrieve() print(response.headers.get('X-My-Header')) me = response.parse() # get the object that `me.retrieve()` would have returned -print(me.valid) +print(me.organization_id) ``` These methods return an [`APIResponse`](https://github.com/Blooio/blooio-python-sdk/tree/main/src/blooio/_response.py) object. diff --git a/SECURITY.md b/SECURITY.md index ea78ff0..4262c82 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,6 +18,10 @@ before making any information public. If you encounter security issues that are not directly related to SDKs but pertain to the services or products provided by Blooio, please follow the respective company's security reporting guidelines. +### Blooio Terms and Policies + +Please contact support@blooio.com for any questions or concerns regarding the security of our services. + --- Thank you for helping us keep the SDKs and systems they interact with secure. diff --git a/api.md b/api.md index 2ee871c..f00d67d 100644 --- a/api.md +++ b/api.md @@ -8,60 +8,299 @@ from blooio.types import MeRetrieveResponse Methods: -- client.me.retrieve() -> MeRetrieveResponse +- client.me.retrieve() -> MeRetrieveResponse + +## Numbers + +Types: + +```python +from blooio.types.me import NumberListResponse +``` + +Methods: + +- client.me.numbers.list() -> NumberListResponse + +### ContactCard + +Types: + +```python +from blooio.types.me.numbers import ContactCardRetrieveResponse, ContactCardUpdateResponse +``` + +Methods: + +- client.me.numbers.contact_card.retrieve(number) -> ContactCardRetrieveResponse +- client.me.numbers.contact_card.update(number, \*\*params) -> ContactCardUpdateResponse # Contacts Types: ```python -from blooio.types import ContactCheckCapabilitiesResponse +from blooio.types import ( + Contact, + DeleteResponse, + Pagination, + ContactListResponse, + ContactCheckCapabilitiesResponse, +) +``` + +Methods: + +- client.contacts.create(\*\*params) -> Contact +- client.contacts.retrieve(contact_id) -> Contact +- client.contacts.update(contact_id, \*\*params) -> Contact +- client.contacts.list(\*\*params) -> ContactListResponse +- client.contacts.delete(contact_id) -> DeleteResponse +- client.contacts.check_capabilities(contact_id) -> ContactCheckCapabilitiesResponse + +## Tags + +Types: + +```python +from blooio.types.contacts import TagListResponse, TagAddResponse +``` + +Methods: + +- client.contacts.tags.list(contact_id) -> TagListResponse +- client.contacts.tags.add(contact_id, \*\*params) -> TagAddResponse +- client.contacts.tags.remove(tag, \*, contact_id) -> DeleteResponse + +# Location + +## Contacts + +Types: + +```python +from blooio.types.location import ContactLocation, ContactListResponse, ContactRefreshResponse +``` + +Methods: + +- client.location.contacts.retrieve(handle) -> ContactLocation +- client.location.contacts.list() -> ContactListResponse +- client.location.contacts.refresh() -> ContactRefreshResponse + +# Facetime + +Types: + +```python +from blooio.types import FacetimeInitiateCallResponse +``` + +Methods: + +- client.facetime.initiate_call(\*\*params) -> FacetimeInitiateCallResponse + +# Groups + +Types: + +```python +from blooio.types import ( + Group, + GroupCreateResponse, + GroupUpdateResponse, + GroupListResponse, + GroupDeleteResponse, +) +``` + +Methods: + +- client.groups.create(\*\*params) -> GroupCreateResponse +- client.groups.retrieve(group_id) -> Group +- client.groups.update(group_id, \*\*params) -> GroupUpdateResponse +- client.groups.list(\*\*params) -> GroupListResponse +- client.groups.delete(group_id) -> GroupDeleteResponse + +## Members + +Types: + +```python +from blooio.types.groups import ( + GroupMember, + MemberListResponse, + MemberAddResponse, + MemberRemoveResponse, +) +``` + +Methods: + +- client.groups.members.list(group_id, \*\*params) -> MemberListResponse +- client.groups.members.add(group_id, \*\*params) -> MemberAddResponse +- client.groups.members.remove(contact_id, \*, group_id) -> MemberRemoveResponse + +## Icon + +Types: + +```python +from blooio.types.groups import GroupIcon ``` Methods: -- client.contacts.check_capabilities(contact) -> ContactCheckCapabilitiesResponse +- client.groups.icon.remove(group_id) -> GroupIcon +- client.groups.icon.set(group_id, \*\*params) -> GroupIcon -# Messages +# Webhooks + +Types: + +```python +from blooio.types import Webhook, WebhookCreateResponse, WebhookListResponse, WebhookDeleteResponse +``` + +Methods: + +- client.webhooks.create(\*\*params) -> WebhookCreateResponse +- client.webhooks.retrieve(webhook_id) -> Webhook +- client.webhooks.update(webhook_id, \*\*params) -> Webhook +- client.webhooks.list() -> WebhookListResponse +- client.webhooks.delete(webhook_id) -> WebhookDeleteResponse + +## Secret + +Types: + +```python +from blooio.types.webhooks import SecretRotateResponse +``` + +Methods: + +- client.webhooks.secret.rotate(webhook_id) -> SecretRotateResponse + +## Logs + +Types: + +```python +from blooio.types.webhooks import LogListResponse, LogReplayResponse +``` + +Methods: + +- client.webhooks.logs.list(webhook_id, \*\*params) -> LogListResponse +- client.webhooks.logs.replay(event_id, \*, webhook_id) -> LogReplayResponse + +# Chats Types: ```python from blooio.types import ( + LastMessage, + ChatRetrieveResponse, + ChatListResponse, + ChatMarkAsReadResponse, + ChatShareContactCardResponse, +) +``` + +Methods: + +- client.chats.retrieve(chat_id) -> ChatRetrieveResponse +- client.chats.list(\*\*params) -> ChatListResponse +- client.chats.mark_as_read(chat_id) -> ChatMarkAsReadResponse +- client.chats.share_contact_card(chat_id) -> ChatShareContactCardResponse + +## Messages + +Types: + +```python +from blooio.types.chats import ( + LinkPreview, + Reaction, MessageRetrieveResponse, - MessageCancelResponse, + MessageListResponse, MessageGetStatusResponse, + MessageReactResponse, MessageSendResponse, ) ``` Methods: -- client.messages.retrieve(message_id) -> MessageRetrieveResponse -- client.messages.cancel(message_id) -> MessageCancelResponse -- client.messages.get_status(message_id) -> MessageGetStatusResponse -- client.messages.send(\*\*params) -> MessageSendResponse +- client.chats.messages.retrieve(message_id, \*, chat_id) -> MessageRetrieveResponse +- client.chats.messages.list(chat_id, \*\*params) -> MessageListResponse +- client.chats.messages.get_status(message_id, \*, chat_id) -> MessageGetStatusResponse +- client.chats.messages.react(message_id, \*, chat_id, \*\*params) -> MessageReactResponse +- client.chats.messages.send(chat_id, \*\*params) -> MessageSendResponse + +## Polls + +Types: + +```python +from blooio.types.chats import PollGetResultsResponse, PollSendResponse +``` + +Methods: + +- client.chats.polls.get_results(poll_id, \*, chat_id) -> PollGetResultsResponse +- client.chats.polls.send(chat_id, \*\*params) -> PollSendResponse + +## Typing + +Types: + +```python +from blooio.types.chats import TypingResponse +``` + +Methods: + +- client.chats.typing.start(chat_id) -> TypingResponse +- client.chats.typing.stop(chat_id) -> TypingResponse + +## Background + +Types: + +```python +from blooio.types.chats import ChatBackgroundResponse +``` -# Config +Methods: -## Webhook +- client.chats.background.retrieve(chat_id) -> ChatBackgroundResponse +- client.chats.background.remove(chat_id) -> ChatBackgroundResponse +- client.chats.background.set(chat_id, \*\*params) -> ChatBackgroundResponse + +# PhoneNumbers Types: ```python -from blooio.types.config import WebhookRetrieveResponse, WebhookUpdateResponse +from blooio.types import PhoneNumberBatchCreateResponse ``` Methods: -- client.config.webhook.retrieve() -> WebhookRetrieveResponse -- client.config.webhook.update(\*\*params) -> WebhookUpdateResponse +- client.phone_numbers.batch_create(\*\*params) -> PhoneNumberBatchCreateResponse + +## Lookup -# Batches +Types: + +```python +from blooio.types.phone_numbers import PhoneNumberLookupResult +``` Methods: -- client.batches.create() -> None -- client.batches.retrieve(batch_id) -> None -- client.batches.list_messages(batch_id) -> None -- client.batches.retrieve_status(batch_id) -> None +- client.phone_numbers.lookup.create(\*\*params) -> PhoneNumberLookupResult +- client.phone_numbers.lookup.retrieve(\*\*params) -> PhoneNumberLookupResult diff --git a/bin/publish-pypi b/bin/publish-pypi index 826054e..e72ca2f 100644 --- a/bin/publish-pypi +++ b/bin/publish-pypi @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -eux +rm -rf dist mkdir -p dist -rye build --clean -rye publish --yes --token=$PYPI_TOKEN +uv build +uv publish --token=$PYPI_TOKEN diff --git a/noxfile.py b/noxfile.py deleted file mode 100644 index 53bca7f..0000000 --- a/noxfile.py +++ /dev/null @@ -1,9 +0,0 @@ -import nox - - -@nox.session(reuse_venv=True, name="test-pydantic-v1") -def test_pydantic_v1(session: nox.Session) -> None: - session.install("-r", "requirements-dev.lock") - session.install("pydantic<2") - - session.run("pytest", "--showlocals", "--ignore=tests/functional", *session.posargs) diff --git a/pyproject.toml b/pyproject.toml index c399c78..b50dace 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,17 +1,17 @@ [project] name = "blooio" -version = "1.0.5" +version = "1.1.0" description = "The official Python library for the blooio API" dynamic = ["readme"] license = "Apache-2.0" authors = [ -{ name = "Blooio", email = "" }, +{ name = "Blooio", email = "support@blooio.com" }, ] dependencies = [ "httpx>=0.23.0, <1", "pydantic>=1.9.0, <3", - "typing-extensions>=4.10, <5", + "typing-extensions>=4.14, <5", "anyio>=3.5.0, <5", "distro>=1.7.0, <2", "sniffio", @@ -43,10 +43,19 @@ Repository = "https://github.com/Blooio/blooio-python-sdk" [project.optional-dependencies] aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"] -[tool.rye] +[tool.uv] managed = true -# version pins are in requirements-dev.lock -dev-dependencies = [ +required-version = ">=0.9" +conflicts = [ + [ + { group = "pydantic-v1" }, + { group = "pydantic-v2" }, + ], +] + +[dependency-groups] +# version pins are in uv.lock +dev = [ "pyright==1.1.399", "mypy==1.17", "respx", @@ -54,41 +63,18 @@ dev-dependencies = [ "pytest-asyncio", "ruff", "time-machine", - "nox", "dirty-equals>=0.6.0", "importlib-metadata>=6.7.0", "rich>=13.7.1", "pytest-xdist>=3.6.1", ] - -[tool.rye.scripts] -format = { chain = [ - "format:ruff", - "format:docs", - "fix:ruff", - # run formatting again to fix any inconsistencies when imports are stripped - "format:ruff", -]} -"format:docs" = "python scripts/utils/ruffen-docs.py README.md api.md" -"format:ruff" = "ruff format" - -"lint" = { chain = [ - "check:ruff", - "typecheck", - "check:importable", -]} -"check:ruff" = "ruff check ." -"fix:ruff" = "ruff check --fix ." - -"check:importable" = "python -c 'import blooio'" - -typecheck = { chain = [ - "typecheck:pyright", - "typecheck:mypy" -]} -"typecheck:pyright" = "pyright" -"typecheck:verify-types" = "pyright --verifytypes blooio --ignoreexternal" -"typecheck:mypy" = "mypy ." +pydantic-v1 = [ + "pydantic>=1.9.0,<2", +] +pydantic-v2 = [ + "pydantic~=2.0 ; python_full_version < '3.14'", + "pydantic~=2.12 ; python_full_version >= '3.14'", +] [build-system] requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"] diff --git a/requirements-dev.lock b/requirements-dev.lock index 8a92795..1d7d683 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -1,149 +1,110 @@ -# generated by rye -# use `rye lock` or `rye sync` to update this lockfile -# -# last locked with the following flags: -# pre: false -# features: [] -# all-features: true -# with-sources: false -# generate-hashes: false -# universal: false - --e file:. -aiohappyeyeballs==2.6.1 - # via aiohttp -aiohttp==3.13.2 - # via blooio - # via httpx-aiohttp -aiosignal==1.4.0 - # via aiohttp +# This file was autogenerated by uv via the following command: +# uv export -o requirements-dev.lock --no-hashes +-e . annotated-types==0.7.0 # via pydantic -anyio==4.12.0 - # via blooio - # via httpx -argcomplete==3.6.3 - # via nox -async-timeout==5.0.1 - # via aiohttp -attrs==25.4.0 - # via aiohttp - # via nox -backports-asyncio-runner==1.2.0 +anyio==4.12.1 + # via + # blooio + # httpx +backports-asyncio-runner==1.2.0 ; python_full_version < '3.11' # via pytest-asyncio -certifi==2025.11.12 - # via httpcore - # via httpx -colorlog==6.10.1 - # via nox -dependency-groups==1.3.1 - # via nox +certifi==2026.1.4 + # via + # httpcore + # httpx +colorama==0.4.6 ; sys_platform == 'win32' + # via pytest dirty-equals==0.11 -distlib==0.4.0 - # via virtualenv distro==1.9.0 # via blooio -exceptiongroup==1.3.1 - # via anyio - # via pytest +exceptiongroup==1.3.1 ; python_full_version < '3.11' + # via + # anyio + # pytest execnet==2.1.2 # via pytest-xdist -filelock==3.19.1 - # via virtualenv -frozenlist==1.8.0 - # via aiohttp - # via aiosignal h11==0.16.0 # via httpcore httpcore==1.0.9 # via httpx httpx==0.28.1 - # via blooio - # via httpx-aiohttp - # via respx -httpx-aiohttp==0.1.9 - # via blooio -humanize==4.13.0 - # via nox + # via + # blooio + # respx idna==3.11 - # via anyio - # via httpx - # via yarl -importlib-metadata==8.7.0 -iniconfig==2.1.0 + # via + # anyio + # httpx +importlib-metadata==8.7.1 +iniconfig==2.1.0 ; python_full_version < '3.10' + # via pytest +iniconfig==2.3.0 ; python_full_version >= '3.10' # via pytest -markdown-it-py==3.0.0 +markdown-it-py==3.0.0 ; python_full_version < '3.10' + # via rich +markdown-it-py==4.0.0 ; python_full_version >= '3.10' # via rich mdurl==0.1.2 # via markdown-it-py -multidict==6.7.0 - # via aiohttp - # via yarl mypy==1.17.0 mypy-extensions==1.1.0 # via mypy -nodeenv==1.9.1 +nodeenv==1.10.0 # via pyright -nox==2025.11.12 packaging==25.0 - # via dependency-groups - # via nox # via pytest -pathspec==0.12.1 +pathspec==1.0.3 # via mypy -platformdirs==4.4.0 - # via virtualenv pluggy==1.6.0 # via pytest -propcache==0.4.1 - # via aiohttp - # via yarl pydantic==2.12.5 # via blooio pydantic-core==2.41.5 # via pydantic pygments==2.19.2 - # via pytest - # via rich + # via + # pytest + # rich pyright==1.1.399 -pytest==8.4.2 - # via pytest-asyncio - # via pytest-xdist -pytest-asyncio==1.2.0 +pytest==8.4.2 ; python_full_version < '3.10' + # via + # pytest-asyncio + # pytest-xdist +pytest==9.0.2 ; python_full_version >= '3.10' + # via + # pytest-asyncio + # pytest-xdist +pytest-asyncio==1.2.0 ; python_full_version < '3.10' +pytest-asyncio==1.3.0 ; python_full_version >= '3.10' pytest-xdist==3.8.0 -python-dateutil==2.9.0.post0 +python-dateutil==2.9.0.post0 ; python_full_version < '3.10' # via time-machine respx==0.22.0 rich==14.2.0 -ruff==0.14.7 -six==1.17.0 +ruff==0.14.13 +six==1.17.0 ; python_full_version < '3.10' # via python-dateutil sniffio==1.3.1 # via blooio -time-machine==2.19.0 -tomli==2.3.0 - # via dependency-groups - # via mypy - # via nox - # via pytest +time-machine==2.19.0 ; python_full_version < '3.10' +time-machine==3.2.0 ; python_full_version >= '3.10' +tomli==2.4.0 ; python_full_version < '3.11' + # via + # mypy + # pytest typing-extensions==4.15.0 - # via aiosignal - # via anyio - # via blooio - # via exceptiongroup - # via multidict - # via mypy - # via pydantic - # via pydantic-core - # via pyright - # via pytest-asyncio - # via typing-inspection - # via virtualenv + # via + # anyio + # blooio + # exceptiongroup + # mypy + # pydantic + # pydantic-core + # pyright + # pytest-asyncio + # typing-inspection typing-inspection==0.4.2 # via pydantic -virtualenv==20.35.4 - # via nox -yarl==1.22.0 - # via aiohttp zipp==3.23.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock deleted file mode 100644 index 4360b3c..0000000 --- a/requirements.lock +++ /dev/null @@ -1,76 +0,0 @@ -# generated by rye -# use `rye lock` or `rye sync` to update this lockfile -# -# last locked with the following flags: -# pre: false -# features: [] -# all-features: true -# with-sources: false -# generate-hashes: false -# universal: false - --e file:. -aiohappyeyeballs==2.6.1 - # via aiohttp -aiohttp==3.13.2 - # via blooio - # via httpx-aiohttp -aiosignal==1.4.0 - # via aiohttp -annotated-types==0.7.0 - # via pydantic -anyio==4.12.0 - # via blooio - # via httpx -async-timeout==5.0.1 - # via aiohttp -attrs==25.4.0 - # via aiohttp -certifi==2025.11.12 - # via httpcore - # via httpx -distro==1.9.0 - # via blooio -exceptiongroup==1.3.1 - # via anyio -frozenlist==1.8.0 - # via aiohttp - # via aiosignal -h11==0.16.0 - # via httpcore -httpcore==1.0.9 - # via httpx -httpx==0.28.1 - # via blooio - # via httpx-aiohttp -httpx-aiohttp==0.1.9 - # via blooio -idna==3.11 - # via anyio - # via httpx - # via yarl -multidict==6.7.0 - # via aiohttp - # via yarl -propcache==0.4.1 - # via aiohttp - # via yarl -pydantic==2.12.5 - # via blooio -pydantic-core==2.41.5 - # via pydantic -sniffio==1.3.1 - # via blooio -typing-extensions==4.15.0 - # via aiosignal - # via anyio - # via blooio - # via exceptiongroup - # via multidict - # via pydantic - # via pydantic-core - # via typing-inspection -typing-inspection==0.4.2 - # via pydantic -yarl==1.22.0 - # via aiohttp diff --git a/scripts/bootstrap b/scripts/bootstrap index b430fee..5a23841 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then brew bundle check >/dev/null 2>&1 || { echo -n "==> Install Homebrew dependencies? (y/N): " read -r response @@ -19,9 +19,12 @@ if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] } fi -echo "==> Installing Python dependencies…" +echo "==> Installing Python…" +uv python install -# experimental uv support makes installations significantly faster -rye config --set-bool behavior.use-uv=true +echo "==> Installing Python dependencies…" +uv sync --all-extras -rye sync --all-features +echo "==> Exporting Python dependencies…" +# note: `--no-hashes` is required because of https://github.com/pypa/pip/issues/4995 +uv export -o requirements-dev.lock --no-hashes diff --git a/scripts/format b/scripts/format index 667ec2d..c8e1f69 100755 --- a/scripts/format +++ b/scripts/format @@ -4,5 +4,11 @@ set -e cd "$(dirname "$0")/.." -echo "==> Running formatters" -rye run format +echo "==> Running ruff" +uv run ruff format +uv run ruff check --fix . +# run formatting again to fix any inconsistencies when imports are stripped +uv run ruff format + +echo "==> Formatting docs" +uv run python scripts/utils/ruffen-docs.py README.md $(find . -type f -name api.md) diff --git a/scripts/lint b/scripts/lint index fafdf3e..dea1dbb 100755 --- a/scripts/lint +++ b/scripts/lint @@ -5,12 +5,18 @@ set -e cd "$(dirname "$0")/.." if [ "$1" = "--fix" ]; then - echo "==> Running lints with --fix" - rye run fix:ruff + echo "==> Running ruff with --fix" + uv run ruff check . --fix else - echo "==> Running lints" - rye run lint + echo "==> Running ruff" + uv run ruff check . fi +echo "==> Running pyright" +uv run pyright + +echo "==> Running mypy" +uv run mypy . + echo "==> Making sure it imports" -rye run python -c 'import blooio' +uv run python -c 'import blooio' diff --git a/scripts/mock b/scripts/mock deleted file mode 100755 index 0b28f6e..0000000 --- a/scripts/mock +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -if [[ -n "$1" && "$1" != '--'* ]]; then - URL="$1" - shift -else - URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" -fi - -# Check if the URL is empty -if [ -z "$URL" ]; then - echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" - exit 1 -fi - -echo "==> Starting mock server with URL ${URL}" - -# Run prism mock on the given spec -if [ "$1" == "--daemon" ]; then - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & - - # Wait for server to come online - echo -n "Waiting for server" - while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do - echo -n "." - sleep 0.1 - done - - if grep -q "✖ fatal" ".prism.log"; then - cat .prism.log - exit 1 - fi - - echo -else - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" -fi diff --git a/scripts/test b/scripts/test index dbeda2d..fe50ebb 100755 --- a/scripts/test +++ b/scripts/test @@ -4,58 +4,35 @@ set -e cd "$(dirname "$0")/.." -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' # No Color -function prism_is_running() { - curl --silent "http://localhost:4010" >/dev/null 2>&1 -} -kill_server_on_port() { - pids=$(lsof -t -i tcp:"$1" || echo "") - if [ "$pids" != "" ]; then - kill "$pids" - echo "Stopped $pids." - fi -} +export DEFER_PYDANTIC_BUILD=false -function is_overriding_api_base_url() { - [ -n "$TEST_API_BASE_URL" ] -} +# Note that we need to specify the patch version here so that uv +# won't use unstable (alpha, beta, rc) releases for the tests +PY_VERSION_MIN=">=3.9.0" +PY_VERSION_MAX=">=3.14.0" -if ! is_overriding_api_base_url && ! prism_is_running ; then - # When we exit this script, make sure to kill the background mock server process - trap 'kill_server_on_port 4010' EXIT +function run_tests() { + echo "==> Running tests with Pydantic v2" + uv run --isolated --all-extras pytest "$@" - # Start the dev server - ./scripts/mock --daemon -fi + # Skip Pydantic v1 tests on latest Python (not supported) + if [[ "$UV_PYTHON" != "$PY_VERSION_MAX" ]]; then + echo "==> Running tests with Pydantic v1" + uv run --isolated --all-extras --group=pydantic-v1 pytest "$@" + fi +} -if is_overriding_api_base_url ; then - echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" - echo -elif ! prism_is_running ; then - echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" - echo -e "running against your OpenAPI spec." - echo - echo -e "To run the server, pass in the path or url of your OpenAPI" - echo -e "spec to the prism command:" - echo - echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" - echo - - exit 1 +# If UV_PYTHON is already set in the environment, just run the command once +if [[ -n "$UV_PYTHON" ]]; then + run_tests "$@" else - echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" - echo -fi + # If UV_PYTHON is not set, run the command for min and max versions -export DEFER_PYDANTIC_BUILD=false + echo "==> Running tests for Python $PY_VERSION_MIN" + UV_PYTHON="$PY_VERSION_MIN" run_tests "$@" -echo "==> Running tests" -rye run pytest "$@" - -echo "==> Running Pydantic v1 tests" -rye run nox -s test-pydantic-v1 -- "$@" + echo "==> Running tests for Python $PY_VERSION_MAX" + UV_PYTHON="$PY_VERSION_MAX" run_tests "$@" +fi diff --git a/src/blooio/_base_client.py b/src/blooio/_base_client.py index 3293983..c2b1c4c 100644 --- a/src/blooio/_base_client.py +++ b/src/blooio/_base_client.py @@ -9,6 +9,7 @@ import inspect import logging import platform +import warnings import email.utils from types import TracebackType from random import random @@ -51,16 +52,18 @@ ResponseT, AnyMapping, PostParser, + BinaryTypes, RequestFiles, HttpxSendArgs, RequestOptions, + AsyncBinaryTypes, HttpxRequestFiles, ModelBuilderProtocol, not_given, ) from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping from ._compat import PYDANTIC_V1, model_copy, model_dump -from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type +from ._models import GenericModel, SecurityOptions, FinalRequestOptions, validate_type, construct_type from ._response import ( APIResponse, BaseAPIResponse, @@ -83,6 +86,7 @@ APIConnectionError, APIResponseValidationError, ) +from ._utils._json import openapi_dumps log: logging.Logger = logging.getLogger(__name__) @@ -428,9 +432,27 @@ def _make_status_error( ) -> _exceptions.APIStatusError: raise NotImplementedError() + def _auth_headers( + self, + security: SecurityOptions, # noqa: ARG002 + ) -> dict[str, str]: + return {} + + def _auth_query( + self, + security: SecurityOptions, # noqa: ARG002 + ) -> dict[str, str]: + return {} + + def _custom_auth( + self, + security: SecurityOptions, # noqa: ARG002 + ) -> httpx.Auth | None: + return None + def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0) -> httpx.Headers: custom_headers = options.headers or {} - headers_dict = _merge_mappings(self.default_headers, custom_headers) + headers_dict = _merge_mappings({**self._auth_headers(options.security), **self.default_headers}, custom_headers) self._validate_headers(headers_dict, custom_headers) # headers are case-insensitive while dictionaries are not. @@ -477,8 +499,19 @@ def _build_request( retries_taken: int = 0, ) -> httpx.Request: if log.isEnabledFor(logging.DEBUG): - log.debug("Request options: %s", model_dump(options, exclude_unset=True)) - + log.debug( + "Request options: %s", + model_dump( + options, + exclude_unset=True, + # Pydantic v1 can't dump every type we support in content, so we exclude it for now. + exclude={ + "content", + } + if PYDANTIC_V1 + else {}, + ), + ) kwargs: dict[str, Any] = {} json_data = options.json_data @@ -491,7 +524,7 @@ def _build_request( raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`") headers = self._build_headers(options, retries_taken=retries_taken) - params = _merge_mappings(self.default_query, options.params) + params = _merge_mappings({**self._auth_query(options.security), **self.default_query}, options.params) content_type = headers.get("Content-Type") files = options.files @@ -525,6 +558,10 @@ def _build_request( files = cast(HttpxRequestFiles, ForceMultipartDict()) prepared_url = self._prepare_url(options.url) + # preserve hard-coded query params from the url + if params and prepared_url.query: + params = {**dict(prepared_url.params.items()), **params} + prepared_url = prepared_url.copy_with(raw_path=prepared_url.raw_path.split(b"?", 1)[0]) if "_" in prepared_url.host: # work around https://github.com/encode/httpx/discussions/2880 kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} @@ -532,10 +569,18 @@ def _build_request( is_body_allowed = options.method.lower() != "get" if is_body_allowed: - if isinstance(json_data, bytes): + if options.content is not None and json_data is not None: + raise TypeError("Passing both `content` and `json_data` is not supported") + if options.content is not None and files is not None: + raise TypeError("Passing both `content` and `files` is not supported") + if options.content is not None: + kwargs["content"] = options.content + elif isinstance(json_data, bytes): kwargs["content"] = json_data - else: - kwargs["json"] = json_data if is_given(json_data) else None + elif not files: + # Don't set content when JSON is sent as multipart/form-data, + # since httpx's content param overrides other body arguments + kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None kwargs["files"] = files else: headers.pop("Content-Type", None) @@ -648,7 +693,6 @@ def default_headers(self) -> dict[str, str | Omit]: "Content-Type": "application/json", "User-Agent": self.user_agent, **self.platform_headers(), - **self.auth_headers, **self._custom_headers, } @@ -967,8 +1011,9 @@ def request( self._prepare_request(request) kwargs: HttpxSendArgs = {} - if self.custom_auth is not None: - kwargs["auth"] = self.custom_auth + custom_auth = self._custom_auth(options.security) + if custom_auth is not None: + kwargs["auth"] = custom_auth if options.follow_redirects is not None: kwargs["follow_redirects"] = options.follow_redirects @@ -1194,6 +1239,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[False] = False, @@ -1206,6 +1252,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: Literal[True], @@ -1219,6 +1266,7 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool, @@ -1231,13 +1279,25 @@ def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, files: RequestFiles | None = None, stream: bool = False, stream_cls: type[_StreamT] | None = None, ) -> ResponseT | _StreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) @@ -1247,11 +1307,23 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1261,11 +1333,23 @@ def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options ) return self.request(cast_to, opts) @@ -1275,9 +1359,19 @@ def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: BinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return self.request(cast_to, opts) def get_api_list( @@ -1717,6 +1811,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[False] = False, @@ -1729,6 +1824,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: Literal[True], @@ -1742,6 +1838,7 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool, @@ -1754,13 +1851,25 @@ async def post( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, stream: bool = False, stream_cls: type[_AsyncStreamT] | None = None, ) -> ResponseT | _AsyncStreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="post", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) @@ -1770,11 +1879,28 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="patch", + url=path, + json_data=body, + content=content, + files=await async_to_httpx_files(files), + **options, ) return await self.request(cast_to, opts) @@ -1784,11 +1910,23 @@ async def put( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) opts = FinalRequestOptions.construct( - method="put", url=path, json_data=body, files=await async_to_httpx_files(files), **options + method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) @@ -1798,9 +1936,19 @@ async def delete( *, cast_to: Type[ResponseT], body: Body | None = None, + content: AsyncBinaryTypes | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, **options) + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) return await self.request(cast_to, opts) def get_api_list( @@ -1826,6 +1974,7 @@ def make_request_options( idempotency_key: str | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, post_parser: PostParser | NotGiven = not_given, + security: SecurityOptions | None = None, ) -> RequestOptions: """Create a dict of type RequestOptions without keys of NotGiven values.""" options: RequestOptions = {} @@ -1851,6 +2000,9 @@ def make_request_options( # internal options["post_parser"] = post_parser # type: ignore + if security is not None: + options["security"] = security + return options diff --git a/src/blooio/_client.py b/src/blooio/_client.py index 540214b..9557526 100644 --- a/src/blooio/_client.py +++ b/src/blooio/_client.py @@ -19,8 +19,13 @@ RequestOptions, not_given, ) -from ._utils import is_given, get_async_library +from ._utils import ( + is_given, + is_mapping_t, + get_async_library, +) from ._compat import cached_property +from ._models import SecurityOptions from ._version import __version__ from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import BlooioError, APIStatusError @@ -31,12 +36,15 @@ ) if TYPE_CHECKING: - from .resources import me, config, batches, contacts, messages - from .resources.me import MeResource, AsyncMeResource - from .resources.batches import BatchesResource, AsyncBatchesResource - from .resources.contacts import ContactsResource, AsyncContactsResource - from .resources.messages import MessagesResource, AsyncMessagesResource - from .resources.config.config import ConfigResource, AsyncConfigResource + from .resources import me, chats, groups, contacts, facetime, location, webhooks, phone_numbers + from .resources.me.me import MeResource, AsyncMeResource + from .resources.facetime import FacetimeResource, AsyncFacetimeResource + from .resources.chats.chats import ChatsResource, AsyncChatsResource + from .resources.groups.groups import GroupsResource, AsyncGroupsResource + from .resources.contacts.contacts import ContactsResource, AsyncContactsResource + from .resources.location.location import LocationResource, AsyncLocationResource + from .resources.webhooks.webhooks import WebhooksResource, AsyncWebhooksResource + from .resources.phone_numbers.phone_numbers import PhoneNumbersResource, AsyncPhoneNumbersResource __all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Blooio", "AsyncBlooio", "Client", "AsyncClient"] @@ -83,7 +91,16 @@ def __init__( if base_url is None: base_url = os.environ.get("BLOOIO_BASE_URL") if base_url is None: - base_url = f"https://backend.blooio.com" + base_url = f"https://backend.blooio.com/v2/api" + + custom_headers_env = os.environ.get("BLOOIO_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} super().__init__( version=__version__, @@ -98,33 +115,60 @@ def __init__( @cached_property def me(self) -> MeResource: + """Authentication and account information""" from .resources.me import MeResource return MeResource(self) @cached_property def contacts(self) -> ContactsResource: + """Manage contacts (phone numbers and emails)""" from .resources.contacts import ContactsResource return ContactsResource(self) @cached_property - def messages(self) -> MessagesResource: - from .resources.messages import MessagesResource + def location(self) -> LocationResource: + from .resources.location import LocationResource + + return LocationResource(self) + + @cached_property + def facetime(self) -> FacetimeResource: + """Initiate FaceTime calls""" + from .resources.facetime import FacetimeResource + + return FacetimeResource(self) + + @cached_property + def groups(self) -> GroupsResource: + """Manage contact groups""" + from .resources.groups import GroupsResource + + return GroupsResource(self) + + @cached_property + def webhooks(self) -> WebhooksResource: + """Manage webhook subscriptions""" + from .resources.webhooks import WebhooksResource - return MessagesResource(self) + return WebhooksResource(self) @cached_property - def config(self) -> ConfigResource: - from .resources.config import ConfigResource + def chats(self) -> ChatsResource: + from .resources.chats import ChatsResource - return ConfigResource(self) + return ChatsResource(self) @cached_property - def batches(self) -> BatchesResource: - from .resources.batches import BatchesResource + def phone_numbers(self) -> PhoneNumbersResource: + """Phone number validation, formatting, and NANPA geocoding. + + Requires an Enterprise plan (Dedicated Enterprise). + """ + from .resources.phone_numbers import PhoneNumbersResource - return BatchesResource(self) + return PhoneNumbersResource(self) @cached_property def with_raw_response(self) -> BlooioWithRawResponse: @@ -139,9 +183,14 @@ def with_streaming_response(self) -> BlooioWithStreamedResponse: def qs(self) -> Querystring: return Querystring(array_format="comma") - @property @override - def auth_headers(self) -> dict[str, str]: + def _auth_headers(self, security: SecurityOptions) -> dict[str, str]: + return { + **(self._bearer_auth if security.get("bearer_auth", False) else {}), + } + + @property + def _bearer_auth(self) -> dict[str, str]: api_key = self.api_key return {"Authorization": f"Bearer {api_key}"} @@ -281,7 +330,16 @@ def __init__( if base_url is None: base_url = os.environ.get("BLOOIO_BASE_URL") if base_url is None: - base_url = f"https://backend.blooio.com" + base_url = f"https://backend.blooio.com/v2/api" + + custom_headers_env = os.environ.get("BLOOIO_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} super().__init__( version=__version__, @@ -296,33 +354,60 @@ def __init__( @cached_property def me(self) -> AsyncMeResource: + """Authentication and account information""" from .resources.me import AsyncMeResource return AsyncMeResource(self) @cached_property def contacts(self) -> AsyncContactsResource: + """Manage contacts (phone numbers and emails)""" from .resources.contacts import AsyncContactsResource return AsyncContactsResource(self) @cached_property - def messages(self) -> AsyncMessagesResource: - from .resources.messages import AsyncMessagesResource + def location(self) -> AsyncLocationResource: + from .resources.location import AsyncLocationResource + + return AsyncLocationResource(self) + + @cached_property + def facetime(self) -> AsyncFacetimeResource: + """Initiate FaceTime calls""" + from .resources.facetime import AsyncFacetimeResource + + return AsyncFacetimeResource(self) + + @cached_property + def groups(self) -> AsyncGroupsResource: + """Manage contact groups""" + from .resources.groups import AsyncGroupsResource - return AsyncMessagesResource(self) + return AsyncGroupsResource(self) @cached_property - def config(self) -> AsyncConfigResource: - from .resources.config import AsyncConfigResource + def webhooks(self) -> AsyncWebhooksResource: + """Manage webhook subscriptions""" + from .resources.webhooks import AsyncWebhooksResource - return AsyncConfigResource(self) + return AsyncWebhooksResource(self) @cached_property - def batches(self) -> AsyncBatchesResource: - from .resources.batches import AsyncBatchesResource + def chats(self) -> AsyncChatsResource: + from .resources.chats import AsyncChatsResource + + return AsyncChatsResource(self) + + @cached_property + def phone_numbers(self) -> AsyncPhoneNumbersResource: + """Phone number validation, formatting, and NANPA geocoding. + + Requires an Enterprise plan (Dedicated Enterprise). + """ + from .resources.phone_numbers import AsyncPhoneNumbersResource - return AsyncBatchesResource(self) + return AsyncPhoneNumbersResource(self) @cached_property def with_raw_response(self) -> AsyncBlooioWithRawResponse: @@ -337,9 +422,14 @@ def with_streaming_response(self) -> AsyncBlooioWithStreamedResponse: def qs(self) -> Querystring: return Querystring(array_format="comma") - @property @override - def auth_headers(self) -> dict[str, str]: + def _auth_headers(self, security: SecurityOptions) -> dict[str, str]: + return { + **(self._bearer_auth if security.get("bearer_auth", False) else {}), + } + + @property + def _bearer_auth(self) -> dict[str, str]: api_key = self.api_key return {"Authorization": f"Bearer {api_key}"} @@ -445,33 +535,60 @@ def __init__(self, client: Blooio) -> None: @cached_property def me(self) -> me.MeResourceWithRawResponse: + """Authentication and account information""" from .resources.me import MeResourceWithRawResponse return MeResourceWithRawResponse(self._client.me) @cached_property def contacts(self) -> contacts.ContactsResourceWithRawResponse: + """Manage contacts (phone numbers and emails)""" from .resources.contacts import ContactsResourceWithRawResponse return ContactsResourceWithRawResponse(self._client.contacts) @cached_property - def messages(self) -> messages.MessagesResourceWithRawResponse: - from .resources.messages import MessagesResourceWithRawResponse + def location(self) -> location.LocationResourceWithRawResponse: + from .resources.location import LocationResourceWithRawResponse - return MessagesResourceWithRawResponse(self._client.messages) + return LocationResourceWithRawResponse(self._client.location) @cached_property - def config(self) -> config.ConfigResourceWithRawResponse: - from .resources.config import ConfigResourceWithRawResponse + def facetime(self) -> facetime.FacetimeResourceWithRawResponse: + """Initiate FaceTime calls""" + from .resources.facetime import FacetimeResourceWithRawResponse - return ConfigResourceWithRawResponse(self._client.config) + return FacetimeResourceWithRawResponse(self._client.facetime) @cached_property - def batches(self) -> batches.BatchesResourceWithRawResponse: - from .resources.batches import BatchesResourceWithRawResponse + def groups(self) -> groups.GroupsResourceWithRawResponse: + """Manage contact groups""" + from .resources.groups import GroupsResourceWithRawResponse - return BatchesResourceWithRawResponse(self._client.batches) + return GroupsResourceWithRawResponse(self._client.groups) + + @cached_property + def webhooks(self) -> webhooks.WebhooksResourceWithRawResponse: + """Manage webhook subscriptions""" + from .resources.webhooks import WebhooksResourceWithRawResponse + + return WebhooksResourceWithRawResponse(self._client.webhooks) + + @cached_property + def chats(self) -> chats.ChatsResourceWithRawResponse: + from .resources.chats import ChatsResourceWithRawResponse + + return ChatsResourceWithRawResponse(self._client.chats) + + @cached_property + def phone_numbers(self) -> phone_numbers.PhoneNumbersResourceWithRawResponse: + """Phone number validation, formatting, and NANPA geocoding. + + Requires an Enterprise plan (Dedicated Enterprise). + """ + from .resources.phone_numbers import PhoneNumbersResourceWithRawResponse + + return PhoneNumbersResourceWithRawResponse(self._client.phone_numbers) class AsyncBlooioWithRawResponse: @@ -482,33 +599,60 @@ def __init__(self, client: AsyncBlooio) -> None: @cached_property def me(self) -> me.AsyncMeResourceWithRawResponse: + """Authentication and account information""" from .resources.me import AsyncMeResourceWithRawResponse return AsyncMeResourceWithRawResponse(self._client.me) @cached_property def contacts(self) -> contacts.AsyncContactsResourceWithRawResponse: + """Manage contacts (phone numbers and emails)""" from .resources.contacts import AsyncContactsResourceWithRawResponse return AsyncContactsResourceWithRawResponse(self._client.contacts) @cached_property - def messages(self) -> messages.AsyncMessagesResourceWithRawResponse: - from .resources.messages import AsyncMessagesResourceWithRawResponse + def location(self) -> location.AsyncLocationResourceWithRawResponse: + from .resources.location import AsyncLocationResourceWithRawResponse + + return AsyncLocationResourceWithRawResponse(self._client.location) + + @cached_property + def facetime(self) -> facetime.AsyncFacetimeResourceWithRawResponse: + """Initiate FaceTime calls""" + from .resources.facetime import AsyncFacetimeResourceWithRawResponse + + return AsyncFacetimeResourceWithRawResponse(self._client.facetime) + + @cached_property + def groups(self) -> groups.AsyncGroupsResourceWithRawResponse: + """Manage contact groups""" + from .resources.groups import AsyncGroupsResourceWithRawResponse + + return AsyncGroupsResourceWithRawResponse(self._client.groups) + + @cached_property + def webhooks(self) -> webhooks.AsyncWebhooksResourceWithRawResponse: + """Manage webhook subscriptions""" + from .resources.webhooks import AsyncWebhooksResourceWithRawResponse - return AsyncMessagesResourceWithRawResponse(self._client.messages) + return AsyncWebhooksResourceWithRawResponse(self._client.webhooks) @cached_property - def config(self) -> config.AsyncConfigResourceWithRawResponse: - from .resources.config import AsyncConfigResourceWithRawResponse + def chats(self) -> chats.AsyncChatsResourceWithRawResponse: + from .resources.chats import AsyncChatsResourceWithRawResponse - return AsyncConfigResourceWithRawResponse(self._client.config) + return AsyncChatsResourceWithRawResponse(self._client.chats) @cached_property - def batches(self) -> batches.AsyncBatchesResourceWithRawResponse: - from .resources.batches import AsyncBatchesResourceWithRawResponse + def phone_numbers(self) -> phone_numbers.AsyncPhoneNumbersResourceWithRawResponse: + """Phone number validation, formatting, and NANPA geocoding. - return AsyncBatchesResourceWithRawResponse(self._client.batches) + Requires an Enterprise plan (Dedicated Enterprise). + """ + from .resources.phone_numbers import AsyncPhoneNumbersResourceWithRawResponse + + return AsyncPhoneNumbersResourceWithRawResponse(self._client.phone_numbers) class BlooioWithStreamedResponse: @@ -519,33 +663,60 @@ def __init__(self, client: Blooio) -> None: @cached_property def me(self) -> me.MeResourceWithStreamingResponse: + """Authentication and account information""" from .resources.me import MeResourceWithStreamingResponse return MeResourceWithStreamingResponse(self._client.me) @cached_property def contacts(self) -> contacts.ContactsResourceWithStreamingResponse: + """Manage contacts (phone numbers and emails)""" from .resources.contacts import ContactsResourceWithStreamingResponse return ContactsResourceWithStreamingResponse(self._client.contacts) @cached_property - def messages(self) -> messages.MessagesResourceWithStreamingResponse: - from .resources.messages import MessagesResourceWithStreamingResponse + def location(self) -> location.LocationResourceWithStreamingResponse: + from .resources.location import LocationResourceWithStreamingResponse - return MessagesResourceWithStreamingResponse(self._client.messages) + return LocationResourceWithStreamingResponse(self._client.location) @cached_property - def config(self) -> config.ConfigResourceWithStreamingResponse: - from .resources.config import ConfigResourceWithStreamingResponse + def facetime(self) -> facetime.FacetimeResourceWithStreamingResponse: + """Initiate FaceTime calls""" + from .resources.facetime import FacetimeResourceWithStreamingResponse - return ConfigResourceWithStreamingResponse(self._client.config) + return FacetimeResourceWithStreamingResponse(self._client.facetime) @cached_property - def batches(self) -> batches.BatchesResourceWithStreamingResponse: - from .resources.batches import BatchesResourceWithStreamingResponse + def groups(self) -> groups.GroupsResourceWithStreamingResponse: + """Manage contact groups""" + from .resources.groups import GroupsResourceWithStreamingResponse - return BatchesResourceWithStreamingResponse(self._client.batches) + return GroupsResourceWithStreamingResponse(self._client.groups) + + @cached_property + def webhooks(self) -> webhooks.WebhooksResourceWithStreamingResponse: + """Manage webhook subscriptions""" + from .resources.webhooks import WebhooksResourceWithStreamingResponse + + return WebhooksResourceWithStreamingResponse(self._client.webhooks) + + @cached_property + def chats(self) -> chats.ChatsResourceWithStreamingResponse: + from .resources.chats import ChatsResourceWithStreamingResponse + + return ChatsResourceWithStreamingResponse(self._client.chats) + + @cached_property + def phone_numbers(self) -> phone_numbers.PhoneNumbersResourceWithStreamingResponse: + """Phone number validation, formatting, and NANPA geocoding. + + Requires an Enterprise plan (Dedicated Enterprise). + """ + from .resources.phone_numbers import PhoneNumbersResourceWithStreamingResponse + + return PhoneNumbersResourceWithStreamingResponse(self._client.phone_numbers) class AsyncBlooioWithStreamedResponse: @@ -556,33 +727,60 @@ def __init__(self, client: AsyncBlooio) -> None: @cached_property def me(self) -> me.AsyncMeResourceWithStreamingResponse: + """Authentication and account information""" from .resources.me import AsyncMeResourceWithStreamingResponse return AsyncMeResourceWithStreamingResponse(self._client.me) @cached_property def contacts(self) -> contacts.AsyncContactsResourceWithStreamingResponse: + """Manage contacts (phone numbers and emails)""" from .resources.contacts import AsyncContactsResourceWithStreamingResponse return AsyncContactsResourceWithStreamingResponse(self._client.contacts) @cached_property - def messages(self) -> messages.AsyncMessagesResourceWithStreamingResponse: - from .resources.messages import AsyncMessagesResourceWithStreamingResponse + def location(self) -> location.AsyncLocationResourceWithStreamingResponse: + from .resources.location import AsyncLocationResourceWithStreamingResponse - return AsyncMessagesResourceWithStreamingResponse(self._client.messages) + return AsyncLocationResourceWithStreamingResponse(self._client.location) @cached_property - def config(self) -> config.AsyncConfigResourceWithStreamingResponse: - from .resources.config import AsyncConfigResourceWithStreamingResponse + def facetime(self) -> facetime.AsyncFacetimeResourceWithStreamingResponse: + """Initiate FaceTime calls""" + from .resources.facetime import AsyncFacetimeResourceWithStreamingResponse - return AsyncConfigResourceWithStreamingResponse(self._client.config) + return AsyncFacetimeResourceWithStreamingResponse(self._client.facetime) @cached_property - def batches(self) -> batches.AsyncBatchesResourceWithStreamingResponse: - from .resources.batches import AsyncBatchesResourceWithStreamingResponse + def groups(self) -> groups.AsyncGroupsResourceWithStreamingResponse: + """Manage contact groups""" + from .resources.groups import AsyncGroupsResourceWithStreamingResponse + + return AsyncGroupsResourceWithStreamingResponse(self._client.groups) + + @cached_property + def webhooks(self) -> webhooks.AsyncWebhooksResourceWithStreamingResponse: + """Manage webhook subscriptions""" + from .resources.webhooks import AsyncWebhooksResourceWithStreamingResponse + + return AsyncWebhooksResourceWithStreamingResponse(self._client.webhooks) + + @cached_property + def chats(self) -> chats.AsyncChatsResourceWithStreamingResponse: + from .resources.chats import AsyncChatsResourceWithStreamingResponse + + return AsyncChatsResourceWithStreamingResponse(self._client.chats) + + @cached_property + def phone_numbers(self) -> phone_numbers.AsyncPhoneNumbersResourceWithStreamingResponse: + """Phone number validation, formatting, and NANPA geocoding. + + Requires an Enterprise plan (Dedicated Enterprise). + """ + from .resources.phone_numbers import AsyncPhoneNumbersResourceWithStreamingResponse - return AsyncBatchesResourceWithStreamingResponse(self._client.batches) + return AsyncPhoneNumbersResourceWithStreamingResponse(self._client.phone_numbers) Client = Blooio diff --git a/src/blooio/_compat.py b/src/blooio/_compat.py index bdef67f..e6690a4 100644 --- a/src/blooio/_compat.py +++ b/src/blooio/_compat.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload from datetime import date, datetime -from typing_extensions import Self, Literal +from typing_extensions import Self, Literal, TypedDict import pydantic from pydantic.fields import FieldInfo @@ -131,6 +131,10 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: return model.model_dump_json(indent=indent) +class _ModelDumpKwargs(TypedDict, total=False): + by_alias: bool + + def model_dump( model: pydantic.BaseModel, *, @@ -139,8 +143,12 @@ def model_dump( exclude_defaults: bool = False, warnings: bool = True, mode: Literal["json", "python"] = "python", + by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): + kwargs: _ModelDumpKwargs = {} + if by_alias is not None: + kwargs["by_alias"] = by_alias return model.model_dump( mode=mode, exclude=exclude, @@ -148,13 +156,12 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, + **kwargs, ) return cast( "dict[str, Any]", model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - exclude=exclude, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias) ), ) diff --git a/src/blooio/_files.py b/src/blooio/_files.py index cc14c14..903ab5d 100644 --- a/src/blooio/_files.py +++ b/src/blooio/_files.py @@ -3,8 +3,8 @@ import io import os import pathlib -from typing import overload -from typing_extensions import TypeGuard +from typing import Sequence, cast, overload +from typing_extensions import TypeVar, TypeGuard import anyio @@ -17,7 +17,9 @@ HttpxFileContent, HttpxRequestFiles, ) -from ._utils import is_tuple_t, is_mapping_t, is_sequence_t +from ._utils import is_list, is_mapping, is_tuple_t, is_mapping_t, is_sequence_t + +_T = TypeVar("_T") def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: @@ -34,7 +36,7 @@ def assert_is_file_content(obj: object, *, key: str | None = None) -> None: if not is_file_content(obj): prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" raise RuntimeError( - f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead." + f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead. See https://github.com/Blooio/blooio-python-sdk/tree/main#file-uploads" ) from None @@ -121,3 +123,51 @@ async def async_read_file_content(file: FileContent) -> HttpxFileContent: return await anyio.Path(file).read_bytes() return file + + +def deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]]) -> _T: + """Copy only the containers along the given paths. + + Used to guard against mutation by extract_files without copying the entire structure. + Only dicts and lists that lie on a path are copied; everything else + is returned by reference. + + For example, given paths=[["foo", "files", "file"]] and the structure: + { + "foo": { + "bar": {"baz": {}}, + "files": {"file": } + } + } + The root dict, "foo", and "files" are copied (they lie on the path). + "bar" and "baz" are returned by reference (off the path). + """ + return _deepcopy_with_paths(item, paths, 0) + + +def _deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]], index: int) -> _T: + if not paths: + return item + if is_mapping(item): + key_to_paths: dict[str, list[Sequence[str]]] = {} + for path in paths: + if index < len(path): + key_to_paths.setdefault(path[index], []).append(path) + + # if no path continues through this mapping, it won't be mutated and copying it is redundant + if not key_to_paths: + return item + + result = dict(item) + for key, subpaths in key_to_paths.items(): + if key in result: + result[key] = _deepcopy_with_paths(result[key], subpaths, index + 1) + return cast(_T, result) + if is_list(item): + array_paths = [path for path in paths if index < len(path) and path[index] == ""] + + # if no path expects a list here, nothing will be mutated inside it - return by reference + if not array_paths: + return cast(_T, item) + return cast(_T, [_deepcopy_with_paths(entry, array_paths, index + 1) for entry in item]) + return item diff --git a/src/blooio/_models.py b/src/blooio/_models.py index ca9500b..e22dd2a 100644 --- a/src/blooio/_models.py +++ b/src/blooio/_models.py @@ -3,7 +3,20 @@ import os import inspect import weakref -from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast +from typing import ( + IO, + TYPE_CHECKING, + Any, + Type, + Union, + Generic, + TypeVar, + Callable, + Iterable, + Optional, + AsyncIterable, + cast, +) from datetime import date, datetime from typing_extensions import ( List, @@ -778,6 +791,10 @@ def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]: return RootModel[type_] # type: ignore +class SecurityOptions(TypedDict, total=False): + bearer_auth: bool + + class FinalRequestOptionsInput(TypedDict, total=False): method: Required[str] url: Required[str] @@ -787,9 +804,11 @@ class FinalRequestOptionsInput(TypedDict, total=False): timeout: float | Timeout | None files: HttpxRequestFiles | None idempotency_key: str + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] json_data: Body extra_json: AnyMapping follow_redirects: bool + security: SecurityOptions @final @@ -804,7 +823,9 @@ class FinalRequestOptions(pydantic.BaseModel): idempotency_key: Union[str, None] = None post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None + security: SecurityOptions = {"bearer_auth": True} + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None # It should be noted that we cannot use `json` here as that would override # a BaseModel method in an incompatible fashion. json_data: Union[Body, None] = None diff --git a/src/blooio/_qs.py b/src/blooio/_qs.py index ada6fd3..4127c19 100644 --- a/src/blooio/_qs.py +++ b/src/blooio/_qs.py @@ -2,17 +2,13 @@ from typing import Any, List, Tuple, Union, Mapping, TypeVar from urllib.parse import parse_qs, urlencode -from typing_extensions import Literal, get_args +from typing_extensions import get_args -from ._types import NotGiven, not_given +from ._types import NotGiven, ArrayFormat, NestedFormat, not_given from ._utils import flatten _T = TypeVar("_T") - -ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] -NestedFormat = Literal["dots", "brackets"] - PrimitiveData = Union[str, int, float, bool, None] # this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"] # https://github.com/microsoft/pyright/issues/3555 @@ -101,7 +97,10 @@ def _stringify_item( items.extend(self._stringify_item(key, item, opts)) return items elif array_format == "indices": - raise NotImplementedError("The array indices format is not supported yet") + items = [] + for i, item in enumerate(value): + items.extend(self._stringify_item(f"{key}[{i}]", item, opts)) + return items elif array_format == "brackets": items = [] key = key + "[]" diff --git a/src/blooio/_response.py b/src/blooio/_response.py index 9be93fd..19ec5d4 100644 --- a/src/blooio/_response.py +++ b/src/blooio/_response.py @@ -152,6 +152,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: ), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -162,6 +163,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=extract_stream_chunk_type(self._stream_cls), response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) @@ -175,6 +177,7 @@ def _parse(self, *, to: type[_T] | None = None) -> R | _T: cast_to=cast_to, response=self.http_response, client=cast(Any, self._client), + options=self._options, ), ) diff --git a/src/blooio/_streaming.py b/src/blooio/_streaming.py index 3536bda..2bb5675 100644 --- a/src/blooio/_streaming.py +++ b/src/blooio/_streaming.py @@ -4,7 +4,7 @@ import json import inspect from types import TracebackType -from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, AsyncIterator, cast +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, Optional, AsyncIterator, cast from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable import httpx @@ -13,6 +13,7 @@ if TYPE_CHECKING: from ._client import Blooio, AsyncBlooio + from ._models import FinalRequestOptions _T = TypeVar("_T") @@ -22,7 +23,7 @@ class Stream(Generic[_T]): """Provides the core interface to iterate over a synchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEBytesDecoder def __init__( @@ -31,10 +32,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: Blooio, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() @@ -85,7 +88,7 @@ class AsyncStream(Generic[_T]): """Provides the core interface to iterate over an asynchronous stream response.""" response: httpx.Response - + _options: Optional[FinalRequestOptions] = None _decoder: SSEDecoder | SSEBytesDecoder def __init__( @@ -94,10 +97,12 @@ def __init__( cast_to: type[_T], response: httpx.Response, client: AsyncBlooio, + options: Optional[FinalRequestOptions] = None, ) -> None: self.response = response self._cast_to = cast_to self._client = client + self._options = options self._decoder = client._make_sse_decoder() self._iterator = self.__stream__() diff --git a/src/blooio/_types.py b/src/blooio/_types.py index b974bad..e3f5861 100644 --- a/src/blooio/_types.py +++ b/src/blooio/_types.py @@ -13,9 +13,11 @@ Mapping, TypeVar, Callable, + Iterable, Iterator, Optional, Sequence, + AsyncIterable, ) from typing_extensions import ( Set, @@ -34,7 +36,7 @@ from httpx import URL, Proxy, Timeout, Response, BaseTransport, AsyncBaseTransport if TYPE_CHECKING: - from ._models import BaseModel + from ._models import BaseModel, SecurityOptions from ._response import APIResponse, AsyncAPIResponse Transport = BaseTransport @@ -45,6 +47,9 @@ ModelT = TypeVar("ModelT", bound=pydantic.BaseModel) _T = TypeVar("_T") +ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] +NestedFormat = Literal["dots", "brackets"] + # Approximates httpx internal ProxiesTypes and RequestFiles types # while adding support for `PathLike` instances @@ -56,6 +61,13 @@ else: Base64FileInput = Union[IO[bytes], PathLike] FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. + + +# Used for sending raw binary data / streaming data in request bodies +# e.g. for file uploads without multipart encoding +BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]] +AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]] + FileTypes = Union[ # file (or bytes) FileContent, @@ -112,6 +124,7 @@ class RequestOptions(TypedDict, total=False): extra_json: AnyMapping idempotency_key: str follow_redirects: bool + security: SecurityOptions # Sentinel class used until PEP 0661 is accepted diff --git a/src/blooio/_utils/__init__.py b/src/blooio/_utils/__init__.py index dc64e29..1c090e5 100644 --- a/src/blooio/_utils/__init__.py +++ b/src/blooio/_utils/__init__.py @@ -1,3 +1,4 @@ +from ._path import path_template as path_template from ._sync import asyncify as asyncify from ._proxy import LazyProxy as LazyProxy from ._utils import ( @@ -23,7 +24,6 @@ coerce_integer as coerce_integer, file_from_path as file_from_path, strip_not_given as strip_not_given, - deepcopy_minimal as deepcopy_minimal, get_async_library as get_async_library, maybe_coerce_float as maybe_coerce_float, get_required_header as get_required_header, diff --git a/src/blooio/_utils/_compat.py b/src/blooio/_utils/_compat.py index dd70323..2c70b29 100644 --- a/src/blooio/_utils/_compat.py +++ b/src/blooio/_utils/_compat.py @@ -26,7 +26,7 @@ def is_union(tp: Optional[Type[Any]]) -> bool: else: import types - return tp is Union or tp is types.UnionType + return tp is Union or tp is types.UnionType # type: ignore[comparison-overlap] def is_typeddict(tp: Type[Any]) -> bool: diff --git a/src/blooio/_utils/_json.py b/src/blooio/_utils/_json.py new file mode 100644 index 0000000..6058421 --- /dev/null +++ b/src/blooio/_utils/_json.py @@ -0,0 +1,35 @@ +import json +from typing import Any +from datetime import datetime +from typing_extensions import override + +import pydantic + +from .._compat import model_dump + + +def openapi_dumps(obj: Any) -> bytes: + """ + Serialize an object to UTF-8 encoded JSON bytes. + + Extends the standard json.dumps with support for additional types + commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc. + """ + return json.dumps( + obj, + cls=_CustomEncoder, + # Uses the same defaults as httpx's JSON serialization + ensure_ascii=False, + separators=(",", ":"), + allow_nan=False, + ).encode() + + +class _CustomEncoder(json.JSONEncoder): + @override + def default(self, o: Any) -> Any: + if isinstance(o, datetime): + return o.isoformat() + if isinstance(o, pydantic.BaseModel): + return model_dump(o, exclude_unset=True, mode="json", by_alias=True) + return super().default(o) diff --git a/src/blooio/_utils/_path.py b/src/blooio/_utils/_path.py new file mode 100644 index 0000000..4d6e1e4 --- /dev/null +++ b/src/blooio/_utils/_path.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import re +from typing import ( + Any, + Mapping, + Callable, +) +from urllib.parse import quote + +# Matches '.' or '..' where each dot is either literal or percent-encoded (%2e / %2E). +_DOT_SEGMENT_RE = re.compile(r"^(?:\.|%2[eE]){1,2}$") + +_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}") + + +def _quote_path_segment_part(value: str) -> str: + """Percent-encode `value` for use in a URI path segment. + + Considers characters not in `pchar` set from RFC 3986 §3.3 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 + """ + # quote() already treats unreserved characters (letters, digits, and -._~) + # as safe, so we only need to add sub-delims, ':', and '@'. + # Notably, unlike the default `safe` for quote(), / is unsafe and must be quoted. + return quote(value, safe="!$&'()*+,;=:@") + + +def _quote_query_part(value: str) -> str: + """Percent-encode `value` for use in a URI query string. + + Considers &, = and characters not in `query` set from RFC 3986 §3.4 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.4 + """ + return quote(value, safe="!$'()*+,;:@/?") + + +def _quote_fragment_part(value: str) -> str: + """Percent-encode `value` for use in a URI fragment. + + Considers characters not in `fragment` set from RFC 3986 §3.5 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.5 + """ + return quote(value, safe="!$&'()*+,;=:@/?") + + +def _interpolate( + template: str, + values: Mapping[str, Any], + quoter: Callable[[str], str], +) -> str: + """Replace {name} placeholders in `template`, quoting each value with `quoter`. + + Placeholder names are looked up in `values`. + + Raises: + KeyError: If a placeholder is not found in `values`. + """ + # re.split with a capturing group returns alternating + # [text, name, text, name, ..., text] elements. + parts = _PLACEHOLDER_RE.split(template) + + for i in range(1, len(parts), 2): + name = parts[i] + if name not in values: + raise KeyError(f"a value for placeholder {{{name}}} was not provided") + val = values[name] + if val is None: + parts[i] = "null" + elif isinstance(val, bool): + parts[i] = "true" if val else "false" + else: + parts[i] = quoter(str(values[name])) + + return "".join(parts) + + +def path_template(template: str, /, **kwargs: Any) -> str: + """Interpolate {name} placeholders in `template` from keyword arguments. + + Args: + template: The template string containing {name} placeholders. + **kwargs: Keyword arguments to interpolate into the template. + + Returns: + The template with placeholders interpolated and percent-encoded. + + Safe characters for percent-encoding are dependent on the URI component. + Placeholders in path and fragment portions are percent-encoded where the `segment` + and `fragment` sets from RFC 3986 respectively are considered safe. + Placeholders in the query portion are percent-encoded where the `query` set from + RFC 3986 §3.3 is considered safe except for = and & characters. + + Raises: + KeyError: If a placeholder is not found in `kwargs`. + ValueError: If resulting path contains /./ or /../ segments (including percent-encoded dot-segments). + """ + # Split the template into path, query, and fragment portions. + fragment_template: str | None = None + query_template: str | None = None + + rest = template + if "#" in rest: + rest, fragment_template = rest.split("#", 1) + if "?" in rest: + rest, query_template = rest.split("?", 1) + path_template = rest + + # Interpolate each portion with the appropriate quoting rules. + path_result = _interpolate(path_template, kwargs, _quote_path_segment_part) + + # Reject dot-segments (. and ..) in the final assembled path. The check + # runs after interpolation so that adjacent placeholders or a mix of static + # text and placeholders that together form a dot-segment are caught. + # Also reject percent-encoded dot-segments to protect against incorrectly + # implemented normalization in servers/proxies. + for segment in path_result.split("/"): + if _DOT_SEGMENT_RE.match(segment): + raise ValueError(f"Constructed path {path_result!r} contains dot-segment {segment!r} which is not allowed") + + result = path_result + if query_template is not None: + result += "?" + _interpolate(query_template, kwargs, _quote_query_part) + if fragment_template is not None: + result += "#" + _interpolate(fragment_template, kwargs, _quote_fragment_part) + + return result diff --git a/src/blooio/_utils/_utils.py b/src/blooio/_utils/_utils.py index eec7f4a..199cd23 100644 --- a/src/blooio/_utils/_utils.py +++ b/src/blooio/_utils/_utils.py @@ -17,11 +17,11 @@ ) from pathlib import Path from datetime import date, datetime -from typing_extensions import TypeGuard +from typing_extensions import TypeGuard, get_args import sniffio -from .._types import Omit, NotGiven, FileTypes, HeadersLike +from .._types import Omit, NotGiven, FileTypes, ArrayFormat, HeadersLike _T = TypeVar("_T") _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) @@ -40,25 +40,45 @@ def extract_files( query: Mapping[str, object], *, paths: Sequence[Sequence[str]], + array_format: ArrayFormat = "brackets", ) -> list[tuple[str, FileTypes]]: """Recursively extract files from the given dictionary based on specified paths. A path may look like this ['foo', 'files', '', 'data']. + ``array_format`` controls how ```` segments contribute to the emitted + field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and + ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``). + Note: this mutates the given dictionary. """ files: list[tuple[str, FileTypes]] = [] for path in paths: - files.extend(_extract_items(query, path, index=0, flattened_key=None)) + files.extend(_extract_items(query, path, index=0, flattened_key=None, array_format=array_format)) return files +def _array_suffix(array_format: ArrayFormat, array_index: int) -> str: + if array_format == "brackets": + return "[]" + if array_format == "indices": + return f"[{array_index}]" + if array_format == "repeat" or array_format == "comma": + # Both repeat the bare field name for each file part; there is no + # meaningful way to comma-join binary parts. + return "" + raise NotImplementedError( + f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" + ) + + def _extract_items( obj: object, path: Sequence[str], *, index: int, flattened_key: str | None, + array_format: ArrayFormat, ) -> list[tuple[str, FileTypes]]: try: key = path[index] @@ -75,9 +95,11 @@ def _extract_items( if is_list(obj): files: list[tuple[str, FileTypes]] = [] - for entry in obj: - assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "") - files.append((flattened_key + "[]", cast(FileTypes, entry))) + for array_index, entry in enumerate(obj): + suffix = _array_suffix(array_format, array_index) + emitted_key = (flattened_key + suffix) if flattened_key else suffix + assert_is_file_content(entry, key=emitted_key) + files.append((emitted_key, cast(FileTypes, entry))) return files assert_is_file_content(obj, key=flattened_key) @@ -86,8 +108,9 @@ def _extract_items( index += 1 if is_dict(obj): try: - # We are at the last entry in the path so we must remove the field - if (len(path)) == index: + # Remove the field if there are no more dict keys in the path, + # only "" traversal markers or end. + if all(p == "" for p in path[index:]): item = obj.pop(key) else: item = obj[key] @@ -105,6 +128,7 @@ def _extract_items( path, index=index, flattened_key=flattened_key, + array_format=array_format, ) elif is_list(obj): if key != "": @@ -116,9 +140,12 @@ def _extract_items( item, path, index=index, - flattened_key=flattened_key + "[]" if flattened_key is not None else "[]", + flattened_key=( + (flattened_key if flattened_key is not None else "") + _array_suffix(array_format, array_index) + ), + array_format=array_format, ) - for item in obj + for array_index, item in enumerate(obj) ] ) @@ -176,21 +203,6 @@ def is_iterable(obj: object) -> TypeGuard[Iterable[object]]: return isinstance(obj, Iterable) -def deepcopy_minimal(item: _T) -> _T: - """Minimal reimplementation of copy.deepcopy() that will only copy certain object types: - - - mappings, e.g. `dict` - - list - - This is done for performance reasons. - """ - if is_mapping(item): - return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()}) - if is_list(item): - return cast(_T, [deepcopy_minimal(entry) for entry in item]) - return item - - # copied from https://github.com/Rapptz/RoboDanny def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str: size = len(seq) diff --git a/src/blooio/_version.py b/src/blooio/_version.py index dcf7039..b7d98e7 100644 --- a/src/blooio/_version.py +++ b/src/blooio/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "blooio" -__version__ = "1.0.5" # x-release-please-version +__version__ = "1.1.0" # x-release-please-version diff --git a/src/blooio/resources/__init__.py b/src/blooio/resources/__init__.py index 8783929..be39f35 100644 --- a/src/blooio/resources/__init__.py +++ b/src/blooio/resources/__init__.py @@ -8,21 +8,21 @@ MeResourceWithStreamingResponse, AsyncMeResourceWithStreamingResponse, ) -from .config import ( - ConfigResource, - AsyncConfigResource, - ConfigResourceWithRawResponse, - AsyncConfigResourceWithRawResponse, - ConfigResourceWithStreamingResponse, - AsyncConfigResourceWithStreamingResponse, +from .chats import ( + ChatsResource, + AsyncChatsResource, + ChatsResourceWithRawResponse, + AsyncChatsResourceWithRawResponse, + ChatsResourceWithStreamingResponse, + AsyncChatsResourceWithStreamingResponse, ) -from .batches import ( - BatchesResource, - AsyncBatchesResource, - BatchesResourceWithRawResponse, - AsyncBatchesResourceWithRawResponse, - BatchesResourceWithStreamingResponse, - AsyncBatchesResourceWithStreamingResponse, +from .groups import ( + GroupsResource, + AsyncGroupsResource, + GroupsResourceWithRawResponse, + AsyncGroupsResourceWithRawResponse, + GroupsResourceWithStreamingResponse, + AsyncGroupsResourceWithStreamingResponse, ) from .contacts import ( ContactsResource, @@ -32,13 +32,37 @@ ContactsResourceWithStreamingResponse, AsyncContactsResourceWithStreamingResponse, ) -from .messages import ( - MessagesResource, - AsyncMessagesResource, - MessagesResourceWithRawResponse, - AsyncMessagesResourceWithRawResponse, - MessagesResourceWithStreamingResponse, - AsyncMessagesResourceWithStreamingResponse, +from .facetime import ( + FacetimeResource, + AsyncFacetimeResource, + FacetimeResourceWithRawResponse, + AsyncFacetimeResourceWithRawResponse, + FacetimeResourceWithStreamingResponse, + AsyncFacetimeResourceWithStreamingResponse, +) +from .location import ( + LocationResource, + AsyncLocationResource, + LocationResourceWithRawResponse, + AsyncLocationResourceWithRawResponse, + LocationResourceWithStreamingResponse, + AsyncLocationResourceWithStreamingResponse, +) +from .webhooks import ( + WebhooksResource, + AsyncWebhooksResource, + WebhooksResourceWithRawResponse, + AsyncWebhooksResourceWithRawResponse, + WebhooksResourceWithStreamingResponse, + AsyncWebhooksResourceWithStreamingResponse, +) +from .phone_numbers import ( + PhoneNumbersResource, + AsyncPhoneNumbersResource, + PhoneNumbersResourceWithRawResponse, + AsyncPhoneNumbersResourceWithRawResponse, + PhoneNumbersResourceWithStreamingResponse, + AsyncPhoneNumbersResourceWithStreamingResponse, ) __all__ = [ @@ -54,22 +78,40 @@ "AsyncContactsResourceWithRawResponse", "ContactsResourceWithStreamingResponse", "AsyncContactsResourceWithStreamingResponse", - "MessagesResource", - "AsyncMessagesResource", - "MessagesResourceWithRawResponse", - "AsyncMessagesResourceWithRawResponse", - "MessagesResourceWithStreamingResponse", - "AsyncMessagesResourceWithStreamingResponse", - "ConfigResource", - "AsyncConfigResource", - "ConfigResourceWithRawResponse", - "AsyncConfigResourceWithRawResponse", - "ConfigResourceWithStreamingResponse", - "AsyncConfigResourceWithStreamingResponse", - "BatchesResource", - "AsyncBatchesResource", - "BatchesResourceWithRawResponse", - "AsyncBatchesResourceWithRawResponse", - "BatchesResourceWithStreamingResponse", - "AsyncBatchesResourceWithStreamingResponse", + "LocationResource", + "AsyncLocationResource", + "LocationResourceWithRawResponse", + "AsyncLocationResourceWithRawResponse", + "LocationResourceWithStreamingResponse", + "AsyncLocationResourceWithStreamingResponse", + "FacetimeResource", + "AsyncFacetimeResource", + "FacetimeResourceWithRawResponse", + "AsyncFacetimeResourceWithRawResponse", + "FacetimeResourceWithStreamingResponse", + "AsyncFacetimeResourceWithStreamingResponse", + "GroupsResource", + "AsyncGroupsResource", + "GroupsResourceWithRawResponse", + "AsyncGroupsResourceWithRawResponse", + "GroupsResourceWithStreamingResponse", + "AsyncGroupsResourceWithStreamingResponse", + "WebhooksResource", + "AsyncWebhooksResource", + "WebhooksResourceWithRawResponse", + "AsyncWebhooksResourceWithRawResponse", + "WebhooksResourceWithStreamingResponse", + "AsyncWebhooksResourceWithStreamingResponse", + "ChatsResource", + "AsyncChatsResource", + "ChatsResourceWithRawResponse", + "AsyncChatsResourceWithRawResponse", + "ChatsResourceWithStreamingResponse", + "AsyncChatsResourceWithStreamingResponse", + "PhoneNumbersResource", + "AsyncPhoneNumbersResource", + "PhoneNumbersResourceWithRawResponse", + "AsyncPhoneNumbersResourceWithRawResponse", + "PhoneNumbersResourceWithStreamingResponse", + "AsyncPhoneNumbersResourceWithStreamingResponse", ] diff --git a/src/blooio/resources/chats/__init__.py b/src/blooio/resources/chats/__init__.py new file mode 100644 index 0000000..a1ed50d --- /dev/null +++ b/src/blooio/resources/chats/__init__.py @@ -0,0 +1,75 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .chats import ( + ChatsResource, + AsyncChatsResource, + ChatsResourceWithRawResponse, + AsyncChatsResourceWithRawResponse, + ChatsResourceWithStreamingResponse, + AsyncChatsResourceWithStreamingResponse, +) +from .polls import ( + PollsResource, + AsyncPollsResource, + PollsResourceWithRawResponse, + AsyncPollsResourceWithRawResponse, + PollsResourceWithStreamingResponse, + AsyncPollsResourceWithStreamingResponse, +) +from .typing import ( + TypingResource, + AsyncTypingResource, + TypingResourceWithRawResponse, + AsyncTypingResourceWithRawResponse, + TypingResourceWithStreamingResponse, + AsyncTypingResourceWithStreamingResponse, +) +from .messages import ( + MessagesResource, + AsyncMessagesResource, + MessagesResourceWithRawResponse, + AsyncMessagesResourceWithRawResponse, + MessagesResourceWithStreamingResponse, + AsyncMessagesResourceWithStreamingResponse, +) +from .background import ( + BackgroundResource, + AsyncBackgroundResource, + BackgroundResourceWithRawResponse, + AsyncBackgroundResourceWithRawResponse, + BackgroundResourceWithStreamingResponse, + AsyncBackgroundResourceWithStreamingResponse, +) + +__all__ = [ + "MessagesResource", + "AsyncMessagesResource", + "MessagesResourceWithRawResponse", + "AsyncMessagesResourceWithRawResponse", + "MessagesResourceWithStreamingResponse", + "AsyncMessagesResourceWithStreamingResponse", + "PollsResource", + "AsyncPollsResource", + "PollsResourceWithRawResponse", + "AsyncPollsResourceWithRawResponse", + "PollsResourceWithStreamingResponse", + "AsyncPollsResourceWithStreamingResponse", + "TypingResource", + "AsyncTypingResource", + "TypingResourceWithRawResponse", + "AsyncTypingResourceWithRawResponse", + "TypingResourceWithStreamingResponse", + "AsyncTypingResourceWithStreamingResponse", + "BackgroundResource", + "AsyncBackgroundResource", + "BackgroundResourceWithRawResponse", + "AsyncBackgroundResourceWithRawResponse", + "BackgroundResourceWithStreamingResponse", + "AsyncBackgroundResourceWithStreamingResponse", + "ChatsResource", + "AsyncChatsResource", + "ChatsResourceWithRawResponse", + "AsyncChatsResourceWithRawResponse", + "ChatsResourceWithStreamingResponse", + "AsyncChatsResourceWithStreamingResponse", +] diff --git a/src/blooio/resources/chats/background.py b/src/blooio/resources/chats/background.py new file mode 100644 index 0000000..a0c1121 --- /dev/null +++ b/src/blooio/resources/chats/background.py @@ -0,0 +1,368 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Mapping, cast + +import httpx + +from ..._files import deepcopy_with_paths +from ..._types import Body, Query, Headers, NotGiven, FileTypes, not_given +from ..._utils import extract_files, path_template, maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...types.chats import background_set_params +from ..._base_client import make_request_options +from ...types.chats.chat_background_response import ChatBackgroundResponse + +__all__ = ["BackgroundResource", "AsyncBackgroundResource"] + + +class BackgroundResource(SyncAPIResource): + """Set, get, and remove conversation backgrounds""" + + @cached_property + def with_raw_response(self) -> BackgroundResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return BackgroundResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> BackgroundResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return BackgroundResourceWithStreamingResponse(self) + + def retrieve( + self, + chat_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ChatBackgroundResponse: + """Get the current background image metadata for a conversation. + + Works for both + 1-on-1 and group chats. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return self._get( + path_template("/chats/{chat_id}/background", chat_id=chat_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ChatBackgroundResponse, + ) + + def remove( + self, + chat_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ChatBackgroundResponse: + """ + Remove the background image from a conversation, reverting to the default + appearance. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return self._delete( + path_template("/chats/{chat_id}/background", chat_id=chat_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ChatBackgroundResponse, + ) + + def set( + self, + chat_id: str, + *, + background: FileTypes, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ChatBackgroundResponse: + """Set or update the background image for a conversation. + + Works for both 1-on-1 and + group chats. + + The uploaded image is converted into a PosterKit-compatible archive and applied + to the iMessage conversation on the linked device. Supported formats: JPEG, PNG, + GIF, WebP, HEIC/HEIF. Maximum file size: 10 MB. + + Args: + background: The image file to set as the chat background + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + body = deepcopy_with_paths({"background": background}, [["background"]]) + files = extract_files(cast(Mapping[str, object], body), paths=[["background"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._put( + path_template("/chats/{chat_id}/background", chat_id=chat_id), + body=maybe_transform(body, background_set_params.BackgroundSetParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ChatBackgroundResponse, + ) + + +class AsyncBackgroundResource(AsyncAPIResource): + """Set, get, and remove conversation backgrounds""" + + @cached_property + def with_raw_response(self) -> AsyncBackgroundResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncBackgroundResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncBackgroundResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return AsyncBackgroundResourceWithStreamingResponse(self) + + async def retrieve( + self, + chat_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ChatBackgroundResponse: + """Get the current background image metadata for a conversation. + + Works for both + 1-on-1 and group chats. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return await self._get( + path_template("/chats/{chat_id}/background", chat_id=chat_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ChatBackgroundResponse, + ) + + async def remove( + self, + chat_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ChatBackgroundResponse: + """ + Remove the background image from a conversation, reverting to the default + appearance. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return await self._delete( + path_template("/chats/{chat_id}/background", chat_id=chat_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ChatBackgroundResponse, + ) + + async def set( + self, + chat_id: str, + *, + background: FileTypes, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ChatBackgroundResponse: + """Set or update the background image for a conversation. + + Works for both 1-on-1 and + group chats. + + The uploaded image is converted into a PosterKit-compatible archive and applied + to the iMessage conversation on the linked device. Supported formats: JPEG, PNG, + GIF, WebP, HEIC/HEIF. Maximum file size: 10 MB. + + Args: + background: The image file to set as the chat background + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + body = deepcopy_with_paths({"background": background}, [["background"]]) + files = extract_files(cast(Mapping[str, object], body), paths=[["background"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._put( + path_template("/chats/{chat_id}/background", chat_id=chat_id), + body=await async_maybe_transform(body, background_set_params.BackgroundSetParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ChatBackgroundResponse, + ) + + +class BackgroundResourceWithRawResponse: + def __init__(self, background: BackgroundResource) -> None: + self._background = background + + self.retrieve = to_raw_response_wrapper( + background.retrieve, + ) + self.remove = to_raw_response_wrapper( + background.remove, + ) + self.set = to_raw_response_wrapper( + background.set, + ) + + +class AsyncBackgroundResourceWithRawResponse: + def __init__(self, background: AsyncBackgroundResource) -> None: + self._background = background + + self.retrieve = async_to_raw_response_wrapper( + background.retrieve, + ) + self.remove = async_to_raw_response_wrapper( + background.remove, + ) + self.set = async_to_raw_response_wrapper( + background.set, + ) + + +class BackgroundResourceWithStreamingResponse: + def __init__(self, background: BackgroundResource) -> None: + self._background = background + + self.retrieve = to_streamed_response_wrapper( + background.retrieve, + ) + self.remove = to_streamed_response_wrapper( + background.remove, + ) + self.set = to_streamed_response_wrapper( + background.set, + ) + + +class AsyncBackgroundResourceWithStreamingResponse: + def __init__(self, background: AsyncBackgroundResource) -> None: + self._background = background + + self.retrieve = async_to_streamed_response_wrapper( + background.retrieve, + ) + self.remove = async_to_streamed_response_wrapper( + background.remove, + ) + self.set = async_to_streamed_response_wrapper( + background.set, + ) diff --git a/src/blooio/resources/chats/chats.py b/src/blooio/resources/chats/chats.py new file mode 100644 index 0000000..95f2bce --- /dev/null +++ b/src/blooio/resources/chats/chats.py @@ -0,0 +1,620 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from .polls import ( + PollsResource, + AsyncPollsResource, + PollsResourceWithRawResponse, + AsyncPollsResourceWithRawResponse, + PollsResourceWithStreamingResponse, + AsyncPollsResourceWithStreamingResponse, +) +from .typing import ( + TypingResource, + AsyncTypingResource, + TypingResourceWithRawResponse, + AsyncTypingResourceWithRawResponse, + TypingResourceWithStreamingResponse, + AsyncTypingResourceWithStreamingResponse, +) +from ...types import chat_list_params +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._utils import path_template, maybe_transform, async_maybe_transform +from .messages import ( + MessagesResource, + AsyncMessagesResource, + MessagesResourceWithRawResponse, + AsyncMessagesResourceWithRawResponse, + MessagesResourceWithStreamingResponse, + AsyncMessagesResourceWithStreamingResponse, +) +from ..._compat import cached_property +from .background import ( + BackgroundResource, + AsyncBackgroundResource, + BackgroundResourceWithRawResponse, + AsyncBackgroundResourceWithRawResponse, + BackgroundResourceWithStreamingResponse, + AsyncBackgroundResourceWithStreamingResponse, +) +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.chat_list_response import ChatListResponse +from ...types.chat_retrieve_response import ChatRetrieveResponse +from ...types.chat_mark_as_read_response import ChatMarkAsReadResponse +from ...types.chat_share_contact_card_response import ChatShareContactCardResponse + +__all__ = ["ChatsResource", "AsyncChatsResource"] + + +class ChatsResource(SyncAPIResource): + @cached_property + def messages(self) -> MessagesResource: + return MessagesResource(self._client) + + @cached_property + def polls(self) -> PollsResource: + """Send native iMessage polls and retrieve poll results with vote counts. + + Poll events are delivered via separate webhook event types (poll.received, poll.created, poll.voted) and require webhook_type 'poll' or 'all'. + """ + return PollsResource(self._client) + + @cached_property + def typing(self) -> TypingResource: + """Control typing indicators for conversations""" + return TypingResource(self._client) + + @cached_property + def background(self) -> BackgroundResource: + """Set, get, and remove conversation backgrounds""" + return BackgroundResource(self._client) + + @cached_property + def with_raw_response(self) -> ChatsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return ChatsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ChatsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return ChatsResourceWithStreamingResponse(self) + + def retrieve( + self, + chat_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ChatRetrieveResponse: + """ + Get details for a specific conversation. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return self._get( + path_template("/chats/{chat_id}", chat_id=chat_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ChatRetrieveResponse, + ) + + def list( + self, + *, + limit: int | Omit = omit, + offset: int | Omit = omit, + q: str | Omit = omit, + sort: Literal["recent", "oldest"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ChatListResponse: + """ + List all unique conversations for the organization, sorted by most recent + message. + + Args: + limit: Maximum number of items to return (1-200) + + offset: Number of items to skip + + q: Search query (matches phone/email or contact name) + + sort: Sort order + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get( + "/chats", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + "q": q, + "sort": sort, + }, + chat_list_params.ChatListParams, + ), + ), + cast_to=ChatListResponse, + ) + + def mark_as_read( + self, + chat_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ChatMarkAsReadResponse: + """Mark all messages in a chat as read. + + This sends a read receipt to the sender. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return self._post( + path_template("/chats/{chat_id}/read", chat_id=chat_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ChatMarkAsReadResponse, + ) + + def share_contact_card( + self, + chat_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ChatShareContactCardResponse: + """Stage the contact card (Name & Photo) for sharing in a chat. + + The contact card + will be piggybacked onto the next outgoing message (text or attachment) sent to + this chat. This is idempotent — calling it multiple times is harmless. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return self._post( + path_template("/chats/{chat_id}/contact-card", chat_id=chat_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ChatShareContactCardResponse, + ) + + +class AsyncChatsResource(AsyncAPIResource): + @cached_property + def messages(self) -> AsyncMessagesResource: + return AsyncMessagesResource(self._client) + + @cached_property + def polls(self) -> AsyncPollsResource: + """Send native iMessage polls and retrieve poll results with vote counts. + + Poll events are delivered via separate webhook event types (poll.received, poll.created, poll.voted) and require webhook_type 'poll' or 'all'. + """ + return AsyncPollsResource(self._client) + + @cached_property + def typing(self) -> AsyncTypingResource: + """Control typing indicators for conversations""" + return AsyncTypingResource(self._client) + + @cached_property + def background(self) -> AsyncBackgroundResource: + """Set, get, and remove conversation backgrounds""" + return AsyncBackgroundResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncChatsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncChatsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncChatsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return AsyncChatsResourceWithStreamingResponse(self) + + async def retrieve( + self, + chat_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ChatRetrieveResponse: + """ + Get details for a specific conversation. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return await self._get( + path_template("/chats/{chat_id}", chat_id=chat_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ChatRetrieveResponse, + ) + + async def list( + self, + *, + limit: int | Omit = omit, + offset: int | Omit = omit, + q: str | Omit = omit, + sort: Literal["recent", "oldest"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ChatListResponse: + """ + List all unique conversations for the organization, sorted by most recent + message. + + Args: + limit: Maximum number of items to return (1-200) + + offset: Number of items to skip + + q: Search query (matches phone/email or contact name) + + sort: Sort order + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._get( + "/chats", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "limit": limit, + "offset": offset, + "q": q, + "sort": sort, + }, + chat_list_params.ChatListParams, + ), + ), + cast_to=ChatListResponse, + ) + + async def mark_as_read( + self, + chat_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ChatMarkAsReadResponse: + """Mark all messages in a chat as read. + + This sends a read receipt to the sender. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return await self._post( + path_template("/chats/{chat_id}/read", chat_id=chat_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ChatMarkAsReadResponse, + ) + + async def share_contact_card( + self, + chat_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ChatShareContactCardResponse: + """Stage the contact card (Name & Photo) for sharing in a chat. + + The contact card + will be piggybacked onto the next outgoing message (text or attachment) sent to + this chat. This is idempotent — calling it multiple times is harmless. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return await self._post( + path_template("/chats/{chat_id}/contact-card", chat_id=chat_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ChatShareContactCardResponse, + ) + + +class ChatsResourceWithRawResponse: + def __init__(self, chats: ChatsResource) -> None: + self._chats = chats + + self.retrieve = to_raw_response_wrapper( + chats.retrieve, + ) + self.list = to_raw_response_wrapper( + chats.list, + ) + self.mark_as_read = to_raw_response_wrapper( + chats.mark_as_read, + ) + self.share_contact_card = to_raw_response_wrapper( + chats.share_contact_card, + ) + + @cached_property + def messages(self) -> MessagesResourceWithRawResponse: + return MessagesResourceWithRawResponse(self._chats.messages) + + @cached_property + def polls(self) -> PollsResourceWithRawResponse: + """Send native iMessage polls and retrieve poll results with vote counts. + + Poll events are delivered via separate webhook event types (poll.received, poll.created, poll.voted) and require webhook_type 'poll' or 'all'. + """ + return PollsResourceWithRawResponse(self._chats.polls) + + @cached_property + def typing(self) -> TypingResourceWithRawResponse: + """Control typing indicators for conversations""" + return TypingResourceWithRawResponse(self._chats.typing) + + @cached_property + def background(self) -> BackgroundResourceWithRawResponse: + """Set, get, and remove conversation backgrounds""" + return BackgroundResourceWithRawResponse(self._chats.background) + + +class AsyncChatsResourceWithRawResponse: + def __init__(self, chats: AsyncChatsResource) -> None: + self._chats = chats + + self.retrieve = async_to_raw_response_wrapper( + chats.retrieve, + ) + self.list = async_to_raw_response_wrapper( + chats.list, + ) + self.mark_as_read = async_to_raw_response_wrapper( + chats.mark_as_read, + ) + self.share_contact_card = async_to_raw_response_wrapper( + chats.share_contact_card, + ) + + @cached_property + def messages(self) -> AsyncMessagesResourceWithRawResponse: + return AsyncMessagesResourceWithRawResponse(self._chats.messages) + + @cached_property + def polls(self) -> AsyncPollsResourceWithRawResponse: + """Send native iMessage polls and retrieve poll results with vote counts. + + Poll events are delivered via separate webhook event types (poll.received, poll.created, poll.voted) and require webhook_type 'poll' or 'all'. + """ + return AsyncPollsResourceWithRawResponse(self._chats.polls) + + @cached_property + def typing(self) -> AsyncTypingResourceWithRawResponse: + """Control typing indicators for conversations""" + return AsyncTypingResourceWithRawResponse(self._chats.typing) + + @cached_property + def background(self) -> AsyncBackgroundResourceWithRawResponse: + """Set, get, and remove conversation backgrounds""" + return AsyncBackgroundResourceWithRawResponse(self._chats.background) + + +class ChatsResourceWithStreamingResponse: + def __init__(self, chats: ChatsResource) -> None: + self._chats = chats + + self.retrieve = to_streamed_response_wrapper( + chats.retrieve, + ) + self.list = to_streamed_response_wrapper( + chats.list, + ) + self.mark_as_read = to_streamed_response_wrapper( + chats.mark_as_read, + ) + self.share_contact_card = to_streamed_response_wrapper( + chats.share_contact_card, + ) + + @cached_property + def messages(self) -> MessagesResourceWithStreamingResponse: + return MessagesResourceWithStreamingResponse(self._chats.messages) + + @cached_property + def polls(self) -> PollsResourceWithStreamingResponse: + """Send native iMessage polls and retrieve poll results with vote counts. + + Poll events are delivered via separate webhook event types (poll.received, poll.created, poll.voted) and require webhook_type 'poll' or 'all'. + """ + return PollsResourceWithStreamingResponse(self._chats.polls) + + @cached_property + def typing(self) -> TypingResourceWithStreamingResponse: + """Control typing indicators for conversations""" + return TypingResourceWithStreamingResponse(self._chats.typing) + + @cached_property + def background(self) -> BackgroundResourceWithStreamingResponse: + """Set, get, and remove conversation backgrounds""" + return BackgroundResourceWithStreamingResponse(self._chats.background) + + +class AsyncChatsResourceWithStreamingResponse: + def __init__(self, chats: AsyncChatsResource) -> None: + self._chats = chats + + self.retrieve = async_to_streamed_response_wrapper( + chats.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + chats.list, + ) + self.mark_as_read = async_to_streamed_response_wrapper( + chats.mark_as_read, + ) + self.share_contact_card = async_to_streamed_response_wrapper( + chats.share_contact_card, + ) + + @cached_property + def messages(self) -> AsyncMessagesResourceWithStreamingResponse: + return AsyncMessagesResourceWithStreamingResponse(self._chats.messages) + + @cached_property + def polls(self) -> AsyncPollsResourceWithStreamingResponse: + """Send native iMessage polls and retrieve poll results with vote counts. + + Poll events are delivered via separate webhook event types (poll.received, poll.created, poll.voted) and require webhook_type 'poll' or 'all'. + """ + return AsyncPollsResourceWithStreamingResponse(self._chats.polls) + + @cached_property + def typing(self) -> AsyncTypingResourceWithStreamingResponse: + """Control typing indicators for conversations""" + return AsyncTypingResourceWithStreamingResponse(self._chats.typing) + + @cached_property + def background(self) -> AsyncBackgroundResourceWithStreamingResponse: + """Set, get, and remove conversation backgrounds""" + return AsyncBackgroundResourceWithStreamingResponse(self._chats.background) diff --git a/src/blooio/resources/chats/messages.py b/src/blooio/resources/chats/messages.py new file mode 100644 index 0000000..61336a7 --- /dev/null +++ b/src/blooio/resources/chats/messages.py @@ -0,0 +1,745 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Iterable, Optional +from typing_extensions import Literal + +import httpx + +from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given +from ..._utils import path_template, maybe_transform, strip_not_given, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...types.chats import message_list_params, message_send_params, message_react_params +from ..._base_client import make_request_options +from ...types.chats.link_preview_param import LinkPreviewParam +from ...types.chats.message_list_response import MessageListResponse +from ...types.chats.message_send_response import MessageSendResponse +from ...types.chats.message_react_response import MessageReactResponse +from ...types.chats.message_retrieve_response import MessageRetrieveResponse +from ...types.chats.message_get_status_response import MessageGetStatusResponse + +__all__ = ["MessagesResource", "AsyncMessagesResource"] + + +class MessagesResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> MessagesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return MessagesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> MessagesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return MessagesResourceWithStreamingResponse(self) + + def retrieve( + self, + message_id: str, + *, + chat_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MessageRetrieveResponse: + """ + Get details for a specific message. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + if not message_id: + raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") + return self._get( + path_template("/chats/{chat_id}/messages/{message_id}", chat_id=chat_id, message_id=message_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MessageRetrieveResponse, + ) + + def list( + self, + chat_id: str, + *, + direction: Literal["inbound", "outbound"] | Omit = omit, + limit: int | Omit = omit, + offset: int | Omit = omit, + since: int | Omit = omit, + sort: Literal["asc", "desc"] | Omit = omit, + until: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MessageListResponse: + """ + List all messages in a conversation with optional filtering. + + Args: + direction: Filter by message direction + + limit: Maximum number of items to return (1-200) + + offset: Number of items to skip + + since: Only messages sent after this timestamp (ms) + + sort: Sort order by time + + until: Only messages sent before this timestamp (ms) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return self._get( + path_template("/chats/{chat_id}/messages", chat_id=chat_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "direction": direction, + "limit": limit, + "offset": offset, + "since": since, + "sort": sort, + "until": until, + }, + message_list_params.MessageListParams, + ), + ), + cast_to=MessageListResponse, + ) + + def get_status( + self, + message_id: str, + *, + chat_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MessageGetStatusResponse: + """ + Get delivery status for a specific message. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + if not message_id: + raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") + return self._get( + path_template("/chats/{chat_id}/messages/{message_id}/status", chat_id=chat_id, message_id=message_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MessageGetStatusResponse, + ) + + def react( + self, + message_id: str, + *, + chat_id: str, + reaction: str, + direction: Literal["inbound", "outbound"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MessageReactResponse: + """Add or remove a reaction to a message. + + Supports classic iMessage tapbacks (love, + like, dislike, laugh, emphasize, question) and emoji reactions (e.g. +😂, -😂). + + The messageId can be an explicit message ID (e.g., msg_xxx) or a relative index + (-1 for last message, -2 for second-to-last, etc.). When using relative indices, + you can optionally filter by message direction (inbound/outbound only). + + Emoji reactions require macOS 14 (Sonoma) or later on the device. + + Args: + reaction: The reaction to add or remove. Must be prefixed with `+` to add or `-` to + remove. + + **Classic tapbacks:** `+love`, `-love`, `+like`, `-like`, `+dislike`, + `-dislike`, `+laugh`, `-laugh`, `+emphasize`, `-emphasize`, `+question`, + `-question` + + **Emoji reactions:** Any emoji prefixed with `+` or `-` (e.g. `+😂`, `-😂`, + `+👍`, `-🔥`). Emoji reactions require macOS 14 (Sonoma) or later on the device. + + direction: Filter by message direction (only used when messageId is a relative index like + -1, -2) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + if not message_id: + raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") + return self._post( + path_template("/chats/{chat_id}/messages/{message_id}/reactions", chat_id=chat_id, message_id=message_id), + body=maybe_transform( + { + "reaction": reaction, + "direction": direction, + }, + message_react_params.MessageReactParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MessageReactResponse, + ) + + def send( + self, + chat_id: str, + *, + attachments: SequenceNotStr[message_send_params.Attachment] | Omit = omit, + from_number: str | Omit = omit, + link_preview: Optional[LinkPreviewParam] | Omit = omit, + parts: Iterable[message_send_params.Part] | Omit = omit, + share_contact: bool | Omit = omit, + text: Union[str, SequenceNotStr[str]] | Omit = omit, + use_typing_indicator: bool | Omit = omit, + idempotency_key: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MessageSendResponse: + """Send a message to a chat. + + The chatId can be: (1) E.164 phone number, (2) email + address, (3) group ID (grp_xxxx), or (4) comma-separated list of phone/email for + multi-recipient chats. For multi-recipient, an unnamed group is automatically + created or reused if the exact participant combination already exists. For + explicit groups, the group must be linked to an existing iMessage chat. + + Args: + attachments: Array of attachment URLs or objects with url/name + + from_number: E.164 phone number to send from. For Twilio API keys, this is optional — if + omitted, the first assigned Twilio number is auto-selected. For Blooio + (iMessage) API keys, this selects a specific number from your pool. Must be a + number assigned to your API key. + + link_preview: Rich-link-preview overrides for URL messages (iMessage URL balloon). All fields + are optional. Only applies when the message text (or the concatenated part text) + is exactly a single http(s) URL. If omitted but the text is a URL, Blooio + auto-fetches the page's Open Graph metadata to generate a preview. If the image + download fails, the send still succeeds — Blooio silently falls back to the + auto-generated preview. + + parts: + Ordered array of message parts. Two modes: + + 1. **Multipart mode** — parts sent as a single unified iMessage bubble (mix of + text and attachment parts). This is the default. + 2. **URL-balloon batch mode** — triggered when any part has a `link_preview` + object. Each part becomes its own rich-link-preview iMessage; parts are sent + sequentially in array order. In batch mode every part must be text-only with + `text` being a single http(s) URL. Response contains `message_ids[]` + + `count` instead of `message_id`. + + share_contact: If true, the contact card (Name & Photo) will be shared with this message. The + contact card is piggybacked onto the outgoing message. Defaults to false. + + text: Message text. Can be a single string or array of strings (each becomes a + separate message) + + use_typing_indicator: Whether to show typing indicator before sending. Defaults to org preference. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + extra_headers = {**strip_not_given({"Idempotency-Key": idempotency_key}), **(extra_headers or {})} + return self._post( + path_template("/chats/{chat_id}/messages", chat_id=chat_id), + body=maybe_transform( + { + "attachments": attachments, + "from_number": from_number, + "link_preview": link_preview, + "parts": parts, + "share_contact": share_contact, + "text": text, + "use_typing_indicator": use_typing_indicator, + }, + message_send_params.MessageSendParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MessageSendResponse, + ) + + +class AsyncMessagesResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncMessagesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncMessagesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncMessagesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return AsyncMessagesResourceWithStreamingResponse(self) + + async def retrieve( + self, + message_id: str, + *, + chat_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MessageRetrieveResponse: + """ + Get details for a specific message. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + if not message_id: + raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") + return await self._get( + path_template("/chats/{chat_id}/messages/{message_id}", chat_id=chat_id, message_id=message_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MessageRetrieveResponse, + ) + + async def list( + self, + chat_id: str, + *, + direction: Literal["inbound", "outbound"] | Omit = omit, + limit: int | Omit = omit, + offset: int | Omit = omit, + since: int | Omit = omit, + sort: Literal["asc", "desc"] | Omit = omit, + until: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MessageListResponse: + """ + List all messages in a conversation with optional filtering. + + Args: + direction: Filter by message direction + + limit: Maximum number of items to return (1-200) + + offset: Number of items to skip + + since: Only messages sent after this timestamp (ms) + + sort: Sort order by time + + until: Only messages sent before this timestamp (ms) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return await self._get( + path_template("/chats/{chat_id}/messages", chat_id=chat_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "direction": direction, + "limit": limit, + "offset": offset, + "since": since, + "sort": sort, + "until": until, + }, + message_list_params.MessageListParams, + ), + ), + cast_to=MessageListResponse, + ) + + async def get_status( + self, + message_id: str, + *, + chat_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MessageGetStatusResponse: + """ + Get delivery status for a specific message. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + if not message_id: + raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") + return await self._get( + path_template("/chats/{chat_id}/messages/{message_id}/status", chat_id=chat_id, message_id=message_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MessageGetStatusResponse, + ) + + async def react( + self, + message_id: str, + *, + chat_id: str, + reaction: str, + direction: Literal["inbound", "outbound"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MessageReactResponse: + """Add or remove a reaction to a message. + + Supports classic iMessage tapbacks (love, + like, dislike, laugh, emphasize, question) and emoji reactions (e.g. +😂, -😂). + + The messageId can be an explicit message ID (e.g., msg_xxx) or a relative index + (-1 for last message, -2 for second-to-last, etc.). When using relative indices, + you can optionally filter by message direction (inbound/outbound only). + + Emoji reactions require macOS 14 (Sonoma) or later on the device. + + Args: + reaction: The reaction to add or remove. Must be prefixed with `+` to add or `-` to + remove. + + **Classic tapbacks:** `+love`, `-love`, `+like`, `-like`, `+dislike`, + `-dislike`, `+laugh`, `-laugh`, `+emphasize`, `-emphasize`, `+question`, + `-question` + + **Emoji reactions:** Any emoji prefixed with `+` or `-` (e.g. `+😂`, `-😂`, + `+👍`, `-🔥`). Emoji reactions require macOS 14 (Sonoma) or later on the device. + + direction: Filter by message direction (only used when messageId is a relative index like + -1, -2) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + if not message_id: + raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") + return await self._post( + path_template("/chats/{chat_id}/messages/{message_id}/reactions", chat_id=chat_id, message_id=message_id), + body=await async_maybe_transform( + { + "reaction": reaction, + "direction": direction, + }, + message_react_params.MessageReactParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MessageReactResponse, + ) + + async def send( + self, + chat_id: str, + *, + attachments: SequenceNotStr[message_send_params.Attachment] | Omit = omit, + from_number: str | Omit = omit, + link_preview: Optional[LinkPreviewParam] | Omit = omit, + parts: Iterable[message_send_params.Part] | Omit = omit, + share_contact: bool | Omit = omit, + text: Union[str, SequenceNotStr[str]] | Omit = omit, + use_typing_indicator: bool | Omit = omit, + idempotency_key: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MessageSendResponse: + """Send a message to a chat. + + The chatId can be: (1) E.164 phone number, (2) email + address, (3) group ID (grp_xxxx), or (4) comma-separated list of phone/email for + multi-recipient chats. For multi-recipient, an unnamed group is automatically + created or reused if the exact participant combination already exists. For + explicit groups, the group must be linked to an existing iMessage chat. + + Args: + attachments: Array of attachment URLs or objects with url/name + + from_number: E.164 phone number to send from. For Twilio API keys, this is optional — if + omitted, the first assigned Twilio number is auto-selected. For Blooio + (iMessage) API keys, this selects a specific number from your pool. Must be a + number assigned to your API key. + + link_preview: Rich-link-preview overrides for URL messages (iMessage URL balloon). All fields + are optional. Only applies when the message text (or the concatenated part text) + is exactly a single http(s) URL. If omitted but the text is a URL, Blooio + auto-fetches the page's Open Graph metadata to generate a preview. If the image + download fails, the send still succeeds — Blooio silently falls back to the + auto-generated preview. + + parts: + Ordered array of message parts. Two modes: + + 1. **Multipart mode** — parts sent as a single unified iMessage bubble (mix of + text and attachment parts). This is the default. + 2. **URL-balloon batch mode** — triggered when any part has a `link_preview` + object. Each part becomes its own rich-link-preview iMessage; parts are sent + sequentially in array order. In batch mode every part must be text-only with + `text` being a single http(s) URL. Response contains `message_ids[]` + + `count` instead of `message_id`. + + share_contact: If true, the contact card (Name & Photo) will be shared with this message. The + contact card is piggybacked onto the outgoing message. Defaults to false. + + text: Message text. Can be a single string or array of strings (each becomes a + separate message) + + use_typing_indicator: Whether to show typing indicator before sending. Defaults to org preference. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + extra_headers = {**strip_not_given({"Idempotency-Key": idempotency_key}), **(extra_headers or {})} + return await self._post( + path_template("/chats/{chat_id}/messages", chat_id=chat_id), + body=await async_maybe_transform( + { + "attachments": attachments, + "from_number": from_number, + "link_preview": link_preview, + "parts": parts, + "share_contact": share_contact, + "text": text, + "use_typing_indicator": use_typing_indicator, + }, + message_send_params.MessageSendParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MessageSendResponse, + ) + + +class MessagesResourceWithRawResponse: + def __init__(self, messages: MessagesResource) -> None: + self._messages = messages + + self.retrieve = to_raw_response_wrapper( + messages.retrieve, + ) + self.list = to_raw_response_wrapper( + messages.list, + ) + self.get_status = to_raw_response_wrapper( + messages.get_status, + ) + self.react = to_raw_response_wrapper( + messages.react, + ) + self.send = to_raw_response_wrapper( + messages.send, + ) + + +class AsyncMessagesResourceWithRawResponse: + def __init__(self, messages: AsyncMessagesResource) -> None: + self._messages = messages + + self.retrieve = async_to_raw_response_wrapper( + messages.retrieve, + ) + self.list = async_to_raw_response_wrapper( + messages.list, + ) + self.get_status = async_to_raw_response_wrapper( + messages.get_status, + ) + self.react = async_to_raw_response_wrapper( + messages.react, + ) + self.send = async_to_raw_response_wrapper( + messages.send, + ) + + +class MessagesResourceWithStreamingResponse: + def __init__(self, messages: MessagesResource) -> None: + self._messages = messages + + self.retrieve = to_streamed_response_wrapper( + messages.retrieve, + ) + self.list = to_streamed_response_wrapper( + messages.list, + ) + self.get_status = to_streamed_response_wrapper( + messages.get_status, + ) + self.react = to_streamed_response_wrapper( + messages.react, + ) + self.send = to_streamed_response_wrapper( + messages.send, + ) + + +class AsyncMessagesResourceWithStreamingResponse: + def __init__(self, messages: AsyncMessagesResource) -> None: + self._messages = messages + + self.retrieve = async_to_streamed_response_wrapper( + messages.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + messages.list, + ) + self.get_status = async_to_streamed_response_wrapper( + messages.get_status, + ) + self.react = async_to_streamed_response_wrapper( + messages.react, + ) + self.send = async_to_streamed_response_wrapper( + messages.send, + ) diff --git a/src/blooio/resources/chats/polls.py b/src/blooio/resources/chats/polls.py new file mode 100644 index 0000000..71df3f3 --- /dev/null +++ b/src/blooio/resources/chats/polls.py @@ -0,0 +1,294 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given +from ..._utils import path_template, maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...types.chats import poll_send_params +from ..._base_client import make_request_options +from ...types.chats.poll_send_response import PollSendResponse +from ...types.chats.poll_get_results_response import PollGetResultsResponse + +__all__ = ["PollsResource", "AsyncPollsResource"] + + +class PollsResource(SyncAPIResource): + """Send native iMessage polls and retrieve poll results with vote counts. + + Poll events are delivered via separate webhook event types (poll.received, poll.created, poll.voted) and require webhook_type 'poll' or 'all'. + """ + + @cached_property + def with_raw_response(self) -> PollsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return PollsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> PollsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return PollsResourceWithStreamingResponse(self) + + def get_results( + self, + poll_id: str, + *, + chat_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PollGetResultsResponse: + """Retrieve a poll's definition and aggregated vote counts. + + The pollId is the + poll_id returned in the poll.received or poll.created webhook event. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + if not poll_id: + raise ValueError(f"Expected a non-empty value for `poll_id` but received {poll_id!r}") + return self._get( + path_template("/chats/{chat_id}/polls/{poll_id}", chat_id=chat_id, poll_id=poll_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PollGetResultsResponse, + ) + + def send( + self, + chat_id: str, + *, + options: SequenceNotStr[str], + title: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PollSendResponse: + """Send a native iMessage poll to a chat. + + The poll appears as an interactive ballot + that recipients can vote on. + + Args: + options: Array of 2-10 option strings for the poll + + title: Poll question or title (optional) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return self._post( + path_template("/chats/{chat_id}/polls", chat_id=chat_id), + body=maybe_transform( + { + "options": options, + "title": title, + }, + poll_send_params.PollSendParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PollSendResponse, + ) + + +class AsyncPollsResource(AsyncAPIResource): + """Send native iMessage polls and retrieve poll results with vote counts. + + Poll events are delivered via separate webhook event types (poll.received, poll.created, poll.voted) and require webhook_type 'poll' or 'all'. + """ + + @cached_property + def with_raw_response(self) -> AsyncPollsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncPollsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncPollsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return AsyncPollsResourceWithStreamingResponse(self) + + async def get_results( + self, + poll_id: str, + *, + chat_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PollGetResultsResponse: + """Retrieve a poll's definition and aggregated vote counts. + + The pollId is the + poll_id returned in the poll.received or poll.created webhook event. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + if not poll_id: + raise ValueError(f"Expected a non-empty value for `poll_id` but received {poll_id!r}") + return await self._get( + path_template("/chats/{chat_id}/polls/{poll_id}", chat_id=chat_id, poll_id=poll_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PollGetResultsResponse, + ) + + async def send( + self, + chat_id: str, + *, + options: SequenceNotStr[str], + title: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PollSendResponse: + """Send a native iMessage poll to a chat. + + The poll appears as an interactive ballot + that recipients can vote on. + + Args: + options: Array of 2-10 option strings for the poll + + title: Poll question or title (optional) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return await self._post( + path_template("/chats/{chat_id}/polls", chat_id=chat_id), + body=await async_maybe_transform( + { + "options": options, + "title": title, + }, + poll_send_params.PollSendParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PollSendResponse, + ) + + +class PollsResourceWithRawResponse: + def __init__(self, polls: PollsResource) -> None: + self._polls = polls + + self.get_results = to_raw_response_wrapper( + polls.get_results, + ) + self.send = to_raw_response_wrapper( + polls.send, + ) + + +class AsyncPollsResourceWithRawResponse: + def __init__(self, polls: AsyncPollsResource) -> None: + self._polls = polls + + self.get_results = async_to_raw_response_wrapper( + polls.get_results, + ) + self.send = async_to_raw_response_wrapper( + polls.send, + ) + + +class PollsResourceWithStreamingResponse: + def __init__(self, polls: PollsResource) -> None: + self._polls = polls + + self.get_results = to_streamed_response_wrapper( + polls.get_results, + ) + self.send = to_streamed_response_wrapper( + polls.send, + ) + + +class AsyncPollsResourceWithStreamingResponse: + def __init__(self, polls: AsyncPollsResource) -> None: + self._polls = polls + + self.get_results = async_to_streamed_response_wrapper( + polls.get_results, + ) + self.send = async_to_streamed_response_wrapper( + polls.send, + ) diff --git a/src/blooio/resources/config/webhook.py b/src/blooio/resources/chats/typing.py similarity index 51% rename from src/blooio/resources/config/webhook.py rename to src/blooio/resources/chats/typing.py index a0c953f..ac08cb3 100644 --- a/src/blooio/resources/config/webhook.py +++ b/src/blooio/resources/chats/typing.py @@ -5,7 +5,7 @@ import httpx from ..._types import Body, Query, Headers, NotGiven, not_given -from ..._utils import maybe_transform, async_maybe_transform +from ..._utils import path_template from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource from ..._response import ( @@ -15,35 +15,36 @@ async_to_streamed_response_wrapper, ) from ..._base_client import make_request_options -from ...types.config import webhook_update_params -from ...types.config.webhook_update_response import WebhookUpdateResponse -from ...types.config.webhook_retrieve_response import WebhookRetrieveResponse +from ...types.chats.typing_response import TypingResponse -__all__ = ["WebhookResource", "AsyncWebhookResource"] +__all__ = ["TypingResource", "AsyncTypingResource"] -class WebhookResource(SyncAPIResource): +class TypingResource(SyncAPIResource): + """Control typing indicators for conversations""" + @cached_property - def with_raw_response(self) -> WebhookResourceWithRawResponse: + def with_raw_response(self) -> TypingResourceWithRawResponse: """ This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers """ - return WebhookResourceWithRawResponse(self) + return TypingResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> WebhookResourceWithStreamingResponse: + def with_streaming_response(self) -> TypingResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response """ - return WebhookResourceWithStreamingResponse(self) + return TypingResourceWithStreamingResponse(self) - def retrieve( + def start( self, + chat_id: str, *, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -51,40 +52,44 @@ def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> WebhookRetrieveResponse: - """Get the current webhook URL""" - return self._get( - "/v1/api/config/webhook", + ) -> TypingResponse: + """Start the typing indicator for a chat. + + The indicator shows the recipient that + you are typing. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return self._post( + path_template("/chats/{chat_id}/typing", chat_id=chat_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=WebhookRetrieveResponse, + cast_to=TypingResponse, ) - def update( + def stop( self, + chat_id: str, *, - webhook_url: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> WebhookUpdateResponse: - """Configure the webhook URL that will receive all message events. - - Once configured, - your endpoint will receive POST requests with the following event types: - - - `message.received` - When you receive an inbound message - - `message.sent` - When your outbound message is sent to the carrier - - `message.delivered` - When your outbound message is delivered to the recipient - - `message.failed` - When your outbound message fails to deliver - - `message.read` - When your outbound message is read by the recipient (iMessage - only, recipient must have read receipts enabled) - - See the webhook event schemas for detailed payload formats. + ) -> TypingResponse: + """ + Stop the typing indicator for a chat. Args: extra_headers: Send extra headers @@ -95,38 +100,42 @@ def update( timeout: Override the client-level default timeout for this request, in seconds """ - return self._put( - "/v1/api/config/webhook", - body=maybe_transform({"webhook_url": webhook_url}, webhook_update_params.WebhookUpdateParams), + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return self._delete( + path_template("/chats/{chat_id}/typing", chat_id=chat_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=WebhookUpdateResponse, + cast_to=TypingResponse, ) -class AsyncWebhookResource(AsyncAPIResource): +class AsyncTypingResource(AsyncAPIResource): + """Control typing indicators for conversations""" + @cached_property - def with_raw_response(self) -> AsyncWebhookResourceWithRawResponse: + def with_raw_response(self) -> AsyncTypingResourceWithRawResponse: """ This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers """ - return AsyncWebhookResourceWithRawResponse(self) + return AsyncTypingResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> AsyncWebhookResourceWithStreamingResponse: + def with_streaming_response(self) -> AsyncTypingResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response """ - return AsyncWebhookResourceWithStreamingResponse(self) + return AsyncTypingResourceWithStreamingResponse(self) - async def retrieve( + async def start( self, + chat_id: str, *, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -134,40 +143,44 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> WebhookRetrieveResponse: - """Get the current webhook URL""" - return await self._get( - "/v1/api/config/webhook", + ) -> TypingResponse: + """Start the typing indicator for a chat. + + The indicator shows the recipient that + you are typing. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return await self._post( + path_template("/chats/{chat_id}/typing", chat_id=chat_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=WebhookRetrieveResponse, + cast_to=TypingResponse, ) - async def update( + async def stop( self, + chat_id: str, *, - webhook_url: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> WebhookUpdateResponse: - """Configure the webhook URL that will receive all message events. - - Once configured, - your endpoint will receive POST requests with the following event types: - - - `message.received` - When you receive an inbound message - - `message.sent` - When your outbound message is sent to the carrier - - `message.delivered` - When your outbound message is delivered to the recipient - - `message.failed` - When your outbound message fails to deliver - - `message.read` - When your outbound message is read by the recipient (iMessage - only, recipient must have read receipts enabled) - - See the webhook event schemas for detailed payload formats. + ) -> TypingResponse: + """ + Stop the typing indicator for a chat. Args: extra_headers: Send extra headers @@ -178,59 +191,60 @@ async def update( timeout: Override the client-level default timeout for this request, in seconds """ - return await self._put( - "/v1/api/config/webhook", - body=await async_maybe_transform({"webhook_url": webhook_url}, webhook_update_params.WebhookUpdateParams), + if not chat_id: + raise ValueError(f"Expected a non-empty value for `chat_id` but received {chat_id!r}") + return await self._delete( + path_template("/chats/{chat_id}/typing", chat_id=chat_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=WebhookUpdateResponse, + cast_to=TypingResponse, ) -class WebhookResourceWithRawResponse: - def __init__(self, webhook: WebhookResource) -> None: - self._webhook = webhook +class TypingResourceWithRawResponse: + def __init__(self, typing: TypingResource) -> None: + self._typing = typing - self.retrieve = to_raw_response_wrapper( - webhook.retrieve, + self.start = to_raw_response_wrapper( + typing.start, ) - self.update = to_raw_response_wrapper( - webhook.update, + self.stop = to_raw_response_wrapper( + typing.stop, ) -class AsyncWebhookResourceWithRawResponse: - def __init__(self, webhook: AsyncWebhookResource) -> None: - self._webhook = webhook +class AsyncTypingResourceWithRawResponse: + def __init__(self, typing: AsyncTypingResource) -> None: + self._typing = typing - self.retrieve = async_to_raw_response_wrapper( - webhook.retrieve, + self.start = async_to_raw_response_wrapper( + typing.start, ) - self.update = async_to_raw_response_wrapper( - webhook.update, + self.stop = async_to_raw_response_wrapper( + typing.stop, ) -class WebhookResourceWithStreamingResponse: - def __init__(self, webhook: WebhookResource) -> None: - self._webhook = webhook +class TypingResourceWithStreamingResponse: + def __init__(self, typing: TypingResource) -> None: + self._typing = typing - self.retrieve = to_streamed_response_wrapper( - webhook.retrieve, + self.start = to_streamed_response_wrapper( + typing.start, ) - self.update = to_streamed_response_wrapper( - webhook.update, + self.stop = to_streamed_response_wrapper( + typing.stop, ) -class AsyncWebhookResourceWithStreamingResponse: - def __init__(self, webhook: AsyncWebhookResource) -> None: - self._webhook = webhook +class AsyncTypingResourceWithStreamingResponse: + def __init__(self, typing: AsyncTypingResource) -> None: + self._typing = typing - self.retrieve = async_to_streamed_response_wrapper( - webhook.retrieve, + self.start = async_to_streamed_response_wrapper( + typing.start, ) - self.update = async_to_streamed_response_wrapper( - webhook.update, + self.stop = async_to_streamed_response_wrapper( + typing.stop, ) diff --git a/src/blooio/resources/config/__init__.py b/src/blooio/resources/config/__init__.py deleted file mode 100644 index f0daf18..0000000 --- a/src/blooio/resources/config/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from .config import ( - ConfigResource, - AsyncConfigResource, - ConfigResourceWithRawResponse, - AsyncConfigResourceWithRawResponse, - ConfigResourceWithStreamingResponse, - AsyncConfigResourceWithStreamingResponse, -) -from .webhook import ( - WebhookResource, - AsyncWebhookResource, - WebhookResourceWithRawResponse, - AsyncWebhookResourceWithRawResponse, - WebhookResourceWithStreamingResponse, - AsyncWebhookResourceWithStreamingResponse, -) - -__all__ = [ - "WebhookResource", - "AsyncWebhookResource", - "WebhookResourceWithRawResponse", - "AsyncWebhookResourceWithRawResponse", - "WebhookResourceWithStreamingResponse", - "AsyncWebhookResourceWithStreamingResponse", - "ConfigResource", - "AsyncConfigResource", - "ConfigResourceWithRawResponse", - "AsyncConfigResourceWithRawResponse", - "ConfigResourceWithStreamingResponse", - "AsyncConfigResourceWithStreamingResponse", -] diff --git a/src/blooio/resources/config/config.py b/src/blooio/resources/config/config.py deleted file mode 100644 index c748805..0000000 --- a/src/blooio/resources/config/config.py +++ /dev/null @@ -1,102 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from .webhook import ( - WebhookResource, - AsyncWebhookResource, - WebhookResourceWithRawResponse, - AsyncWebhookResourceWithRawResponse, - WebhookResourceWithStreamingResponse, - AsyncWebhookResourceWithStreamingResponse, -) -from ..._compat import cached_property -from ..._resource import SyncAPIResource, AsyncAPIResource - -__all__ = ["ConfigResource", "AsyncConfigResource"] - - -class ConfigResource(SyncAPIResource): - @cached_property - def webhook(self) -> WebhookResource: - return WebhookResource(self._client) - - @cached_property - def with_raw_response(self) -> ConfigResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers - """ - return ConfigResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> ConfigResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response - """ - return ConfigResourceWithStreamingResponse(self) - - -class AsyncConfigResource(AsyncAPIResource): - @cached_property - def webhook(self) -> AsyncWebhookResource: - return AsyncWebhookResource(self._client) - - @cached_property - def with_raw_response(self) -> AsyncConfigResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers - """ - return AsyncConfigResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncConfigResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response - """ - return AsyncConfigResourceWithStreamingResponse(self) - - -class ConfigResourceWithRawResponse: - def __init__(self, config: ConfigResource) -> None: - self._config = config - - @cached_property - def webhook(self) -> WebhookResourceWithRawResponse: - return WebhookResourceWithRawResponse(self._config.webhook) - - -class AsyncConfigResourceWithRawResponse: - def __init__(self, config: AsyncConfigResource) -> None: - self._config = config - - @cached_property - def webhook(self) -> AsyncWebhookResourceWithRawResponse: - return AsyncWebhookResourceWithRawResponse(self._config.webhook) - - -class ConfigResourceWithStreamingResponse: - def __init__(self, config: ConfigResource) -> None: - self._config = config - - @cached_property - def webhook(self) -> WebhookResourceWithStreamingResponse: - return WebhookResourceWithStreamingResponse(self._config.webhook) - - -class AsyncConfigResourceWithStreamingResponse: - def __init__(self, config: AsyncConfigResource) -> None: - self._config = config - - @cached_property - def webhook(self) -> AsyncWebhookResourceWithStreamingResponse: - return AsyncWebhookResourceWithStreamingResponse(self._config.webhook) diff --git a/src/blooio/resources/contacts/__init__.py b/src/blooio/resources/contacts/__init__.py new file mode 100644 index 0000000..6c126b0 --- /dev/null +++ b/src/blooio/resources/contacts/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .tags import ( + TagsResource, + AsyncTagsResource, + TagsResourceWithRawResponse, + AsyncTagsResourceWithRawResponse, + TagsResourceWithStreamingResponse, + AsyncTagsResourceWithStreamingResponse, +) +from .contacts import ( + ContactsResource, + AsyncContactsResource, + ContactsResourceWithRawResponse, + AsyncContactsResourceWithRawResponse, + ContactsResourceWithStreamingResponse, + AsyncContactsResourceWithStreamingResponse, +) + +__all__ = [ + "TagsResource", + "AsyncTagsResource", + "TagsResourceWithRawResponse", + "AsyncTagsResourceWithRawResponse", + "TagsResourceWithStreamingResponse", + "AsyncTagsResourceWithStreamingResponse", + "ContactsResource", + "AsyncContactsResource", + "ContactsResourceWithRawResponse", + "AsyncContactsResourceWithRawResponse", + "ContactsResourceWithStreamingResponse", + "AsyncContactsResourceWithStreamingResponse", +] diff --git a/src/blooio/resources/contacts/contacts.py b/src/blooio/resources/contacts/contacts.py new file mode 100644 index 0000000..3eed99f --- /dev/null +++ b/src/blooio/resources/contacts/contacts.py @@ -0,0 +1,673 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Literal + +import httpx + +from .tags import ( + TagsResource, + AsyncTagsResource, + TagsResourceWithRawResponse, + AsyncTagsResourceWithRawResponse, + TagsResourceWithStreamingResponse, + AsyncTagsResourceWithStreamingResponse, +) +from ...types import contact_list_params, contact_create_params, contact_update_params +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._utils import path_template, maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.contact import Contact +from ...types.delete_response import DeleteResponse +from ...types.contact_list_response import ContactListResponse +from ...types.contact_check_capabilities_response import ContactCheckCapabilitiesResponse + +__all__ = ["ContactsResource", "AsyncContactsResource"] + + +class ContactsResource(SyncAPIResource): + """Manage contacts (phone numbers and emails)""" + + @cached_property + def tags(self) -> TagsResource: + """Manage contacts (phone numbers and emails)""" + return TagsResource(self._client) + + @cached_property + def with_raw_response(self) -> ContactsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return ContactsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ContactsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return ContactsResourceWithStreamingResponse(self) + + def create( + self, + *, + identifier: str, + name: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Contact: + """ + Create a new contact with a phone number (E.164 format) or email address. + + Args: + identifier: Phone number (E.164 format, e.g., +15551234567) or email address + + name: Display name for the contact + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/contacts", + body=maybe_transform( + { + "identifier": identifier, + "name": name, + }, + contact_create_params.ContactCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Contact, + ) + + def retrieve( + self, + contact_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Contact: + """ + Get details for a specific contact by phone number or email. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not contact_id: + raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") + return self._get( + path_template("/contacts/{contact_id}", contact_id=contact_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Contact, + ) + + def update( + self, + contact_id: str, + *, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Contact: + """ + Update a contact's name. + + Args: + name: New display name (null to clear) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not contact_id: + raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") + return self._patch( + path_template("/contacts/{contact_id}", contact_id=contact_id), + body=maybe_transform({"name": name}, contact_update_params.ContactUpdateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Contact, + ) + + def list( + self, + *, + limit: int | Omit = omit, + offset: int | Omit = omit, + q: str | Omit = omit, + sort: Literal["recent", "oldest", "name_asc", "name_desc"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ContactListResponse: + """ + List all contacts for the organization with optional search and pagination. + + Args: + limit: Maximum number of items to return (1-200) + + offset: Number of items to skip + + q: Search query (matches identifier or name) + + sort: Sort order + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get( + "/contacts", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + "q": q, + "sort": sort, + }, + contact_list_params.ContactListParams, + ), + ), + cast_to=ContactListResponse, + ) + + def delete( + self, + contact_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> DeleteResponse: + """ + Soft-delete a contact. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not contact_id: + raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") + return self._delete( + path_template("/contacts/{contact_id}", contact_id=contact_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DeleteResponse, + ) + + def check_capabilities( + self, + contact_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ContactCheckCapabilitiesResponse: + """ + Check if a contact supports iMessage and/or SMS. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not contact_id: + raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") + return self._get( + path_template("/contacts/{contact_id}/capabilities", contact_id=contact_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ContactCheckCapabilitiesResponse, + ) + + +class AsyncContactsResource(AsyncAPIResource): + """Manage contacts (phone numbers and emails)""" + + @cached_property + def tags(self) -> AsyncTagsResource: + """Manage contacts (phone numbers and emails)""" + return AsyncTagsResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncContactsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncContactsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncContactsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return AsyncContactsResourceWithStreamingResponse(self) + + async def create( + self, + *, + identifier: str, + name: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Contact: + """ + Create a new contact with a phone number (E.164 format) or email address. + + Args: + identifier: Phone number (E.164 format, e.g., +15551234567) or email address + + name: Display name for the contact + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/contacts", + body=await async_maybe_transform( + { + "identifier": identifier, + "name": name, + }, + contact_create_params.ContactCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Contact, + ) + + async def retrieve( + self, + contact_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Contact: + """ + Get details for a specific contact by phone number or email. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not contact_id: + raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") + return await self._get( + path_template("/contacts/{contact_id}", contact_id=contact_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Contact, + ) + + async def update( + self, + contact_id: str, + *, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Contact: + """ + Update a contact's name. + + Args: + name: New display name (null to clear) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not contact_id: + raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") + return await self._patch( + path_template("/contacts/{contact_id}", contact_id=contact_id), + body=await async_maybe_transform({"name": name}, contact_update_params.ContactUpdateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Contact, + ) + + async def list( + self, + *, + limit: int | Omit = omit, + offset: int | Omit = omit, + q: str | Omit = omit, + sort: Literal["recent", "oldest", "name_asc", "name_desc"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ContactListResponse: + """ + List all contacts for the organization with optional search and pagination. + + Args: + limit: Maximum number of items to return (1-200) + + offset: Number of items to skip + + q: Search query (matches identifier or name) + + sort: Sort order + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._get( + "/contacts", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "limit": limit, + "offset": offset, + "q": q, + "sort": sort, + }, + contact_list_params.ContactListParams, + ), + ), + cast_to=ContactListResponse, + ) + + async def delete( + self, + contact_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> DeleteResponse: + """ + Soft-delete a contact. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not contact_id: + raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") + return await self._delete( + path_template("/contacts/{contact_id}", contact_id=contact_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=DeleteResponse, + ) + + async def check_capabilities( + self, + contact_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ContactCheckCapabilitiesResponse: + """ + Check if a contact supports iMessage and/or SMS. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not contact_id: + raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") + return await self._get( + path_template("/contacts/{contact_id}/capabilities", contact_id=contact_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ContactCheckCapabilitiesResponse, + ) + + +class ContactsResourceWithRawResponse: + def __init__(self, contacts: ContactsResource) -> None: + self._contacts = contacts + + self.create = to_raw_response_wrapper( + contacts.create, + ) + self.retrieve = to_raw_response_wrapper( + contacts.retrieve, + ) + self.update = to_raw_response_wrapper( + contacts.update, + ) + self.list = to_raw_response_wrapper( + contacts.list, + ) + self.delete = to_raw_response_wrapper( + contacts.delete, + ) + self.check_capabilities = to_raw_response_wrapper( + contacts.check_capabilities, + ) + + @cached_property + def tags(self) -> TagsResourceWithRawResponse: + """Manage contacts (phone numbers and emails)""" + return TagsResourceWithRawResponse(self._contacts.tags) + + +class AsyncContactsResourceWithRawResponse: + def __init__(self, contacts: AsyncContactsResource) -> None: + self._contacts = contacts + + self.create = async_to_raw_response_wrapper( + contacts.create, + ) + self.retrieve = async_to_raw_response_wrapper( + contacts.retrieve, + ) + self.update = async_to_raw_response_wrapper( + contacts.update, + ) + self.list = async_to_raw_response_wrapper( + contacts.list, + ) + self.delete = async_to_raw_response_wrapper( + contacts.delete, + ) + self.check_capabilities = async_to_raw_response_wrapper( + contacts.check_capabilities, + ) + + @cached_property + def tags(self) -> AsyncTagsResourceWithRawResponse: + """Manage contacts (phone numbers and emails)""" + return AsyncTagsResourceWithRawResponse(self._contacts.tags) + + +class ContactsResourceWithStreamingResponse: + def __init__(self, contacts: ContactsResource) -> None: + self._contacts = contacts + + self.create = to_streamed_response_wrapper( + contacts.create, + ) + self.retrieve = to_streamed_response_wrapper( + contacts.retrieve, + ) + self.update = to_streamed_response_wrapper( + contacts.update, + ) + self.list = to_streamed_response_wrapper( + contacts.list, + ) + self.delete = to_streamed_response_wrapper( + contacts.delete, + ) + self.check_capabilities = to_streamed_response_wrapper( + contacts.check_capabilities, + ) + + @cached_property + def tags(self) -> TagsResourceWithStreamingResponse: + """Manage contacts (phone numbers and emails)""" + return TagsResourceWithStreamingResponse(self._contacts.tags) + + +class AsyncContactsResourceWithStreamingResponse: + def __init__(self, contacts: AsyncContactsResource) -> None: + self._contacts = contacts + + self.create = async_to_streamed_response_wrapper( + contacts.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + contacts.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + contacts.update, + ) + self.list = async_to_streamed_response_wrapper( + contacts.list, + ) + self.delete = async_to_streamed_response_wrapper( + contacts.delete, + ) + self.check_capabilities = async_to_streamed_response_wrapper( + contacts.check_capabilities, + ) + + @cached_property + def tags(self) -> AsyncTagsResourceWithStreamingResponse: + """Manage contacts (phone numbers and emails)""" + return AsyncTagsResourceWithStreamingResponse(self._contacts.tags) diff --git a/src/blooio/resources/batches.py b/src/blooio/resources/contacts/tags.py similarity index 51% rename from src/blooio/resources/batches.py rename to src/blooio/resources/contacts/tags.py index 1cb733c..d7327b4 100644 --- a/src/blooio/resources/batches.py +++ b/src/blooio/resources/contacts/tags.py @@ -4,42 +4,50 @@ import httpx -from .._types import Body, Query, Headers, NoneType, NotGiven, not_given -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( +from ..._types import Body, Query, Headers, NotGiven, SequenceNotStr, not_given +from ..._utils import path_template, maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( to_raw_response_wrapper, to_streamed_response_wrapper, async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from .._base_client import make_request_options +from ..._base_client import make_request_options +from ...types.contacts import tag_add_params +from ...types.delete_response import DeleteResponse +from ...types.contacts.tag_add_response import TagAddResponse +from ...types.contacts.tag_list_response import TagListResponse -__all__ = ["BatchesResource", "AsyncBatchesResource"] +__all__ = ["TagsResource", "AsyncTagsResource"] -class BatchesResource(SyncAPIResource): +class TagsResource(SyncAPIResource): + """Manage contacts (phone numbers and emails)""" + @cached_property - def with_raw_response(self) -> BatchesResourceWithRawResponse: + def with_raw_response(self) -> TagsResourceWithRawResponse: """ This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers """ - return BatchesResourceWithRawResponse(self) + return TagsResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> BatchesResourceWithStreamingResponse: + def with_streaming_response(self) -> TagsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response """ - return BatchesResourceWithStreamingResponse(self) + return TagsResourceWithStreamingResponse(self) - def create( + def list( self, + contact_id: str, *, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -47,30 +55,9 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> None: - """Create a batch of messages (stub)""" - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return self._post( - "/v1/api/batches", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) - - def retrieve( - self, - batch_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> None: + ) -> TagListResponse: """ - Get batch details (stub) + List all tags assigned to a contact. Args: extra_headers: Send extra headers @@ -81,32 +68,36 @@ def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ - if not batch_id: - raise ValueError(f"Expected a non-empty value for `batch_id` but received {batch_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} + if not contact_id: + raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") return self._get( - f"/v1/api/batches/{batch_id}", + path_template("/contacts/{contact_id}/tags", contact_id=contact_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=NoneType, + cast_to=TagListResponse, ) - def list_messages( + def add( self, - batch_id: str, + contact_id: str, *, + tags: SequenceNotStr[str], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> None: - """ - List messages in a batch (stub) + ) -> TagAddResponse: + """Add one or more tags to a contact. + + If a tag already exists on the contact, it is + re-activated (idempotent). Tags are free-form strings. Args: + tags: Tags to add + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -115,30 +106,33 @@ def list_messages( timeout: Override the client-level default timeout for this request, in seconds """ - if not batch_id: - raise ValueError(f"Expected a non-empty value for `batch_id` but received {batch_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return self._get( - f"/v1/api/batches/{batch_id}/messages", + if not contact_id: + raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") + return self._post( + path_template("/contacts/{contact_id}/tags", contact_id=contact_id), + body=maybe_transform({"tags": tags}, tag_add_params.TagAddParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=NoneType, + cast_to=TagAddResponse, ) - def retrieve_status( + def remove( self, - batch_id: str, + tag: str, *, + contact_id: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> None: - """ - Get batch status (stub) + ) -> DeleteResponse: + """Remove a specific tag from a contact. + + The tag is soft-deleted and can be + re-added later. Args: extra_headers: Send extra headers @@ -149,61 +143,44 @@ def retrieve_status( timeout: Override the client-level default timeout for this request, in seconds """ - if not batch_id: - raise ValueError(f"Expected a non-empty value for `batch_id` but received {batch_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return self._get( - f"/v1/api/batches/{batch_id}/status", + if not contact_id: + raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") + if not tag: + raise ValueError(f"Expected a non-empty value for `tag` but received {tag!r}") + return self._delete( + path_template("/contacts/{contact_id}/tags/{tag}", contact_id=contact_id, tag=tag), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=NoneType, + cast_to=DeleteResponse, ) -class AsyncBatchesResource(AsyncAPIResource): +class AsyncTagsResource(AsyncAPIResource): + """Manage contacts (phone numbers and emails)""" + @cached_property - def with_raw_response(self) -> AsyncBatchesResourceWithRawResponse: + def with_raw_response(self) -> AsyncTagsResourceWithRawResponse: """ This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers """ - return AsyncBatchesResourceWithRawResponse(self) + return AsyncTagsResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> AsyncBatchesResourceWithStreamingResponse: + def with_streaming_response(self) -> AsyncTagsResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response """ - return AsyncBatchesResourceWithStreamingResponse(self) - - async def create( - self, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> None: - """Create a batch of messages (stub)""" - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return await self._post( - "/v1/api/batches", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=NoneType, - ) + return AsyncTagsResourceWithStreamingResponse(self) - async def retrieve( + async def list( self, - batch_id: str, + contact_id: str, *, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -211,9 +188,9 @@ async def retrieve( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> None: + ) -> TagListResponse: """ - Get batch details (stub) + List all tags assigned to a contact. Args: extra_headers: Send extra headers @@ -224,32 +201,36 @@ async def retrieve( timeout: Override the client-level default timeout for this request, in seconds """ - if not batch_id: - raise ValueError(f"Expected a non-empty value for `batch_id` but received {batch_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} + if not contact_id: + raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") return await self._get( - f"/v1/api/batches/{batch_id}", + path_template("/contacts/{contact_id}/tags", contact_id=contact_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=NoneType, + cast_to=TagListResponse, ) - async def list_messages( + async def add( self, - batch_id: str, + contact_id: str, *, + tags: SequenceNotStr[str], # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> None: - """ - List messages in a batch (stub) + ) -> TagAddResponse: + """Add one or more tags to a contact. + + If a tag already exists on the contact, it is + re-activated (idempotent). Tags are free-form strings. Args: + tags: Tags to add + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -258,30 +239,33 @@ async def list_messages( timeout: Override the client-level default timeout for this request, in seconds """ - if not batch_id: - raise ValueError(f"Expected a non-empty value for `batch_id` but received {batch_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return await self._get( - f"/v1/api/batches/{batch_id}/messages", + if not contact_id: + raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") + return await self._post( + path_template("/contacts/{contact_id}/tags", contact_id=contact_id), + body=await async_maybe_transform({"tags": tags}, tag_add_params.TagAddParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=NoneType, + cast_to=TagAddResponse, ) - async def retrieve_status( + async def remove( self, - batch_id: str, + tag: str, *, + contact_id: str, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> None: - """ - Get batch status (stub) + ) -> DeleteResponse: + """Remove a specific tag from a contact. + + The tag is soft-deleted and can be + re-added later. Args: extra_headers: Send extra headers @@ -292,85 +276,74 @@ async def retrieve_status( timeout: Override the client-level default timeout for this request, in seconds """ - if not batch_id: - raise ValueError(f"Expected a non-empty value for `batch_id` but received {batch_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} - return await self._get( - f"/v1/api/batches/{batch_id}/status", + if not contact_id: + raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") + if not tag: + raise ValueError(f"Expected a non-empty value for `tag` but received {tag!r}") + return await self._delete( + path_template("/contacts/{contact_id}/tags/{tag}", contact_id=contact_id, tag=tag), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=NoneType, + cast_to=DeleteResponse, ) -class BatchesResourceWithRawResponse: - def __init__(self, batches: BatchesResource) -> None: - self._batches = batches +class TagsResourceWithRawResponse: + def __init__(self, tags: TagsResource) -> None: + self._tags = tags - self.create = to_raw_response_wrapper( - batches.create, - ) - self.retrieve = to_raw_response_wrapper( - batches.retrieve, + self.list = to_raw_response_wrapper( + tags.list, ) - self.list_messages = to_raw_response_wrapper( - batches.list_messages, + self.add = to_raw_response_wrapper( + tags.add, ) - self.retrieve_status = to_raw_response_wrapper( - batches.retrieve_status, + self.remove = to_raw_response_wrapper( + tags.remove, ) -class AsyncBatchesResourceWithRawResponse: - def __init__(self, batches: AsyncBatchesResource) -> None: - self._batches = batches +class AsyncTagsResourceWithRawResponse: + def __init__(self, tags: AsyncTagsResource) -> None: + self._tags = tags - self.create = async_to_raw_response_wrapper( - batches.create, + self.list = async_to_raw_response_wrapper( + tags.list, ) - self.retrieve = async_to_raw_response_wrapper( - batches.retrieve, + self.add = async_to_raw_response_wrapper( + tags.add, ) - self.list_messages = async_to_raw_response_wrapper( - batches.list_messages, - ) - self.retrieve_status = async_to_raw_response_wrapper( - batches.retrieve_status, + self.remove = async_to_raw_response_wrapper( + tags.remove, ) -class BatchesResourceWithStreamingResponse: - def __init__(self, batches: BatchesResource) -> None: - self._batches = batches +class TagsResourceWithStreamingResponse: + def __init__(self, tags: TagsResource) -> None: + self._tags = tags - self.create = to_streamed_response_wrapper( - batches.create, - ) - self.retrieve = to_streamed_response_wrapper( - batches.retrieve, + self.list = to_streamed_response_wrapper( + tags.list, ) - self.list_messages = to_streamed_response_wrapper( - batches.list_messages, + self.add = to_streamed_response_wrapper( + tags.add, ) - self.retrieve_status = to_streamed_response_wrapper( - batches.retrieve_status, + self.remove = to_streamed_response_wrapper( + tags.remove, ) -class AsyncBatchesResourceWithStreamingResponse: - def __init__(self, batches: AsyncBatchesResource) -> None: - self._batches = batches +class AsyncTagsResourceWithStreamingResponse: + def __init__(self, tags: AsyncTagsResource) -> None: + self._tags = tags - self.create = async_to_streamed_response_wrapper( - batches.create, - ) - self.retrieve = async_to_streamed_response_wrapper( - batches.retrieve, + self.list = async_to_streamed_response_wrapper( + tags.list, ) - self.list_messages = async_to_streamed_response_wrapper( - batches.list_messages, + self.add = async_to_streamed_response_wrapper( + tags.add, ) - self.retrieve_status = async_to_streamed_response_wrapper( - batches.retrieve_status, + self.remove = async_to_streamed_response_wrapper( + tags.remove, ) diff --git a/src/blooio/resources/facetime.py b/src/blooio/resources/facetime.py new file mode 100644 index 0000000..ad8ac80 --- /dev/null +++ b/src/blooio/resources/facetime.py @@ -0,0 +1,183 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..types import facetime_initiate_call_params +from .._types import Body, Query, Headers, NotGiven, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.facetime_initiate_call_response import FacetimeInitiateCallResponse + +__all__ = ["FacetimeResource", "AsyncFacetimeResource"] + + +class FacetimeResource(SyncAPIResource): + """Initiate FaceTime calls""" + + @cached_property + def with_raw_response(self) -> FacetimeResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return FacetimeResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> FacetimeResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return FacetimeResourceWithStreamingResponse(self) + + def initiate_call( + self, + *, + handle: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> FacetimeInitiateCallResponse: + """ + **Coming Soon** -- This endpoint is temporarily disabled while we stabilize the + FaceTime call flow. + + Initiates a FaceTime call to the specified phone number or email address. + Returns a shareable FaceTime link that anyone can use to join the call. The call + will ring the contact and auto-admit the first person who joins via the link. + + Args: + handle: Phone number (E.164) or email address to call + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/facetime/calls", + body=maybe_transform({"handle": handle}, facetime_initiate_call_params.FacetimeInitiateCallParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FacetimeInitiateCallResponse, + ) + + +class AsyncFacetimeResource(AsyncAPIResource): + """Initiate FaceTime calls""" + + @cached_property + def with_raw_response(self) -> AsyncFacetimeResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncFacetimeResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncFacetimeResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return AsyncFacetimeResourceWithStreamingResponse(self) + + async def initiate_call( + self, + *, + handle: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> FacetimeInitiateCallResponse: + """ + **Coming Soon** -- This endpoint is temporarily disabled while we stabilize the + FaceTime call flow. + + Initiates a FaceTime call to the specified phone number or email address. + Returns a shareable FaceTime link that anyone can use to join the call. The call + will ring the contact and auto-admit the first person who joins via the link. + + Args: + handle: Phone number (E.164) or email address to call + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/facetime/calls", + body=await async_maybe_transform( + {"handle": handle}, facetime_initiate_call_params.FacetimeInitiateCallParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FacetimeInitiateCallResponse, + ) + + +class FacetimeResourceWithRawResponse: + def __init__(self, facetime: FacetimeResource) -> None: + self._facetime = facetime + + self.initiate_call = to_raw_response_wrapper( + facetime.initiate_call, + ) + + +class AsyncFacetimeResourceWithRawResponse: + def __init__(self, facetime: AsyncFacetimeResource) -> None: + self._facetime = facetime + + self.initiate_call = async_to_raw_response_wrapper( + facetime.initiate_call, + ) + + +class FacetimeResourceWithStreamingResponse: + def __init__(self, facetime: FacetimeResource) -> None: + self._facetime = facetime + + self.initiate_call = to_streamed_response_wrapper( + facetime.initiate_call, + ) + + +class AsyncFacetimeResourceWithStreamingResponse: + def __init__(self, facetime: AsyncFacetimeResource) -> None: + self._facetime = facetime + + self.initiate_call = async_to_streamed_response_wrapper( + facetime.initiate_call, + ) diff --git a/src/blooio/resources/groups/__init__.py b/src/blooio/resources/groups/__init__.py new file mode 100644 index 0000000..ae900be --- /dev/null +++ b/src/blooio/resources/groups/__init__.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .icon import ( + IconResource, + AsyncIconResource, + IconResourceWithRawResponse, + AsyncIconResourceWithRawResponse, + IconResourceWithStreamingResponse, + AsyncIconResourceWithStreamingResponse, +) +from .groups import ( + GroupsResource, + AsyncGroupsResource, + GroupsResourceWithRawResponse, + AsyncGroupsResourceWithRawResponse, + GroupsResourceWithStreamingResponse, + AsyncGroupsResourceWithStreamingResponse, +) +from .members import ( + MembersResource, + AsyncMembersResource, + MembersResourceWithRawResponse, + AsyncMembersResourceWithRawResponse, + MembersResourceWithStreamingResponse, + AsyncMembersResourceWithStreamingResponse, +) + +__all__ = [ + "MembersResource", + "AsyncMembersResource", + "MembersResourceWithRawResponse", + "AsyncMembersResourceWithRawResponse", + "MembersResourceWithStreamingResponse", + "AsyncMembersResourceWithStreamingResponse", + "IconResource", + "AsyncIconResource", + "IconResourceWithRawResponse", + "AsyncIconResourceWithRawResponse", + "IconResourceWithStreamingResponse", + "AsyncIconResourceWithStreamingResponse", + "GroupsResource", + "AsyncGroupsResource", + "GroupsResourceWithRawResponse", + "AsyncGroupsResourceWithRawResponse", + "GroupsResourceWithStreamingResponse", + "AsyncGroupsResourceWithStreamingResponse", +] diff --git a/src/blooio/resources/groups/groups.py b/src/blooio/resources/groups/groups.py new file mode 100644 index 0000000..5eb4504 --- /dev/null +++ b/src/blooio/resources/groups/groups.py @@ -0,0 +1,681 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from .icon import ( + IconResource, + AsyncIconResource, + IconResourceWithRawResponse, + AsyncIconResourceWithRawResponse, + IconResourceWithStreamingResponse, + AsyncIconResourceWithStreamingResponse, +) +from ...types import group_list_params, group_create_params, group_update_params +from .members import ( + MembersResource, + AsyncMembersResource, + MembersResourceWithRawResponse, + AsyncMembersResourceWithRawResponse, + MembersResourceWithStreamingResponse, + AsyncMembersResourceWithStreamingResponse, +) +from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given +from ..._utils import path_template, maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...types.group import Group +from ..._base_client import make_request_options +from ...types.group_list_response import GroupListResponse +from ...types.group_create_response import GroupCreateResponse +from ...types.group_delete_response import GroupDeleteResponse +from ...types.group_update_response import GroupUpdateResponse + +__all__ = ["GroupsResource", "AsyncGroupsResource"] + + +class GroupsResource(SyncAPIResource): + """Manage contact groups""" + + @cached_property + def members(self) -> MembersResource: + """Manage group membership""" + return MembersResource(self._client) + + @cached_property + def icon(self) -> IconResource: + """Manage contact groups""" + return IconResource(self._client) + + @cached_property + def with_raw_response(self) -> GroupsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return GroupsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> GroupsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return GroupsResourceWithStreamingResponse(self) + + def create( + self, + *, + name: str, + chat_guid: str | Omit = omit, + members: SequenceNotStr[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupCreateResponse: + """Create a new group. + + There are two modes: + + **1. Link to existing iMessage chat:** Provide `chat_guid` to join an existing + group chat that was created outside the API. The `members` list records who is + in the group but does NOT add them to the linked iMessage chat. Multiple groups + can have the same participants if they have different `chat_guid`s. + + **2. Create new group:** Omit `chat_guid` to create a new group. When you send + the first message, a new iMessage chat will be created. Note: iMessage only + allows one chat per unique participant set when created via API. + + Args: + name: Group name (max 255 characters) + + chat_guid: BlueBubbles chat GUID to link this group to an existing iMessage chat. Use this + to join groups created elsewhere. You can get this from the BlueBubbles API or + from inbound message webhooks. + + members: Phone numbers or emails of contacts in the group. When linking via chat_guid, + this is for record-keeping only (members are not added to the linked iMessage + chat). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/groups", + body=maybe_transform( + { + "name": name, + "chat_guid": chat_guid, + "members": members, + }, + group_create_params.GroupCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GroupCreateResponse, + ) + + def retrieve( + self, + group_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Group: + """ + Get details for a specific group. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return self._get( + path_template("/groups/{group_id}", group_id=group_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Group, + ) + + def update( + self, + group_id: str, + *, + name: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupUpdateResponse: + """Update a group's name. + + If the group has a linked `chat_guid`, the display name + will also be updated in the linked iMessage chat. Note: iMessage only allows one + chat per unique participant set, so renaming simply changes the display name on + the existing chat thread. + + Args: + name: New group name + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return self._patch( + path_template("/groups/{group_id}", group_id=group_id), + body=maybe_transform({"name": name}, group_update_params.GroupUpdateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GroupUpdateResponse, + ) + + def list( + self, + *, + limit: int | Omit = omit, + offset: int | Omit = omit, + q: str | Omit = omit, + sort: Literal["recent", "oldest", "name_asc", "name_desc"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupListResponse: + """ + List all groups for the organization with optional search and pagination. + + Args: + limit: Maximum number of items to return (1-200) + + offset: Number of items to skip + + q: Search query (matches group name) + + sort: Sort order + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get( + "/groups", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + "q": q, + "sort": sort, + }, + group_list_params.GroupListParams, + ), + ), + cast_to=GroupListResponse, + ) + + def delete( + self, + group_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupDeleteResponse: + """Soft-delete a group. + + Members are automatically removed. If the group is linked + to an existing iMessage chat, the number also leaves that chat. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return self._delete( + path_template("/groups/{group_id}", group_id=group_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GroupDeleteResponse, + ) + + +class AsyncGroupsResource(AsyncAPIResource): + """Manage contact groups""" + + @cached_property + def members(self) -> AsyncMembersResource: + """Manage group membership""" + return AsyncMembersResource(self._client) + + @cached_property + def icon(self) -> AsyncIconResource: + """Manage contact groups""" + return AsyncIconResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncGroupsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncGroupsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncGroupsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return AsyncGroupsResourceWithStreamingResponse(self) + + async def create( + self, + *, + name: str, + chat_guid: str | Omit = omit, + members: SequenceNotStr[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupCreateResponse: + """Create a new group. + + There are two modes: + + **1. Link to existing iMessage chat:** Provide `chat_guid` to join an existing + group chat that was created outside the API. The `members` list records who is + in the group but does NOT add them to the linked iMessage chat. Multiple groups + can have the same participants if they have different `chat_guid`s. + + **2. Create new group:** Omit `chat_guid` to create a new group. When you send + the first message, a new iMessage chat will be created. Note: iMessage only + allows one chat per unique participant set when created via API. + + Args: + name: Group name (max 255 characters) + + chat_guid: BlueBubbles chat GUID to link this group to an existing iMessage chat. Use this + to join groups created elsewhere. You can get this from the BlueBubbles API or + from inbound message webhooks. + + members: Phone numbers or emails of contacts in the group. When linking via chat_guid, + this is for record-keeping only (members are not added to the linked iMessage + chat). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/groups", + body=await async_maybe_transform( + { + "name": name, + "chat_guid": chat_guid, + "members": members, + }, + group_create_params.GroupCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GroupCreateResponse, + ) + + async def retrieve( + self, + group_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Group: + """ + Get details for a specific group. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return await self._get( + path_template("/groups/{group_id}", group_id=group_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Group, + ) + + async def update( + self, + group_id: str, + *, + name: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupUpdateResponse: + """Update a group's name. + + If the group has a linked `chat_guid`, the display name + will also be updated in the linked iMessage chat. Note: iMessage only allows one + chat per unique participant set, so renaming simply changes the display name on + the existing chat thread. + + Args: + name: New group name + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return await self._patch( + path_template("/groups/{group_id}", group_id=group_id), + body=await async_maybe_transform({"name": name}, group_update_params.GroupUpdateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GroupUpdateResponse, + ) + + async def list( + self, + *, + limit: int | Omit = omit, + offset: int | Omit = omit, + q: str | Omit = omit, + sort: Literal["recent", "oldest", "name_asc", "name_desc"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupListResponse: + """ + List all groups for the organization with optional search and pagination. + + Args: + limit: Maximum number of items to return (1-200) + + offset: Number of items to skip + + q: Search query (matches group name) + + sort: Sort order + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._get( + "/groups", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "limit": limit, + "offset": offset, + "q": q, + "sort": sort, + }, + group_list_params.GroupListParams, + ), + ), + cast_to=GroupListResponse, + ) + + async def delete( + self, + group_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupDeleteResponse: + """Soft-delete a group. + + Members are automatically removed. If the group is linked + to an existing iMessage chat, the number also leaves that chat. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return await self._delete( + path_template("/groups/{group_id}", group_id=group_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GroupDeleteResponse, + ) + + +class GroupsResourceWithRawResponse: + def __init__(self, groups: GroupsResource) -> None: + self._groups = groups + + self.create = to_raw_response_wrapper( + groups.create, + ) + self.retrieve = to_raw_response_wrapper( + groups.retrieve, + ) + self.update = to_raw_response_wrapper( + groups.update, + ) + self.list = to_raw_response_wrapper( + groups.list, + ) + self.delete = to_raw_response_wrapper( + groups.delete, + ) + + @cached_property + def members(self) -> MembersResourceWithRawResponse: + """Manage group membership""" + return MembersResourceWithRawResponse(self._groups.members) + + @cached_property + def icon(self) -> IconResourceWithRawResponse: + """Manage contact groups""" + return IconResourceWithRawResponse(self._groups.icon) + + +class AsyncGroupsResourceWithRawResponse: + def __init__(self, groups: AsyncGroupsResource) -> None: + self._groups = groups + + self.create = async_to_raw_response_wrapper( + groups.create, + ) + self.retrieve = async_to_raw_response_wrapper( + groups.retrieve, + ) + self.update = async_to_raw_response_wrapper( + groups.update, + ) + self.list = async_to_raw_response_wrapper( + groups.list, + ) + self.delete = async_to_raw_response_wrapper( + groups.delete, + ) + + @cached_property + def members(self) -> AsyncMembersResourceWithRawResponse: + """Manage group membership""" + return AsyncMembersResourceWithRawResponse(self._groups.members) + + @cached_property + def icon(self) -> AsyncIconResourceWithRawResponse: + """Manage contact groups""" + return AsyncIconResourceWithRawResponse(self._groups.icon) + + +class GroupsResourceWithStreamingResponse: + def __init__(self, groups: GroupsResource) -> None: + self._groups = groups + + self.create = to_streamed_response_wrapper( + groups.create, + ) + self.retrieve = to_streamed_response_wrapper( + groups.retrieve, + ) + self.update = to_streamed_response_wrapper( + groups.update, + ) + self.list = to_streamed_response_wrapper( + groups.list, + ) + self.delete = to_streamed_response_wrapper( + groups.delete, + ) + + @cached_property + def members(self) -> MembersResourceWithStreamingResponse: + """Manage group membership""" + return MembersResourceWithStreamingResponse(self._groups.members) + + @cached_property + def icon(self) -> IconResourceWithStreamingResponse: + """Manage contact groups""" + return IconResourceWithStreamingResponse(self._groups.icon) + + +class AsyncGroupsResourceWithStreamingResponse: + def __init__(self, groups: AsyncGroupsResource) -> None: + self._groups = groups + + self.create = async_to_streamed_response_wrapper( + groups.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + groups.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + groups.update, + ) + self.list = async_to_streamed_response_wrapper( + groups.list, + ) + self.delete = async_to_streamed_response_wrapper( + groups.delete, + ) + + @cached_property + def members(self) -> AsyncMembersResourceWithStreamingResponse: + """Manage group membership""" + return AsyncMembersResourceWithStreamingResponse(self._groups.members) + + @cached_property + def icon(self) -> AsyncIconResourceWithStreamingResponse: + """Manage contact groups""" + return AsyncIconResourceWithStreamingResponse(self._groups.icon) diff --git a/src/blooio/resources/groups/icon.py b/src/blooio/resources/groups/icon.py new file mode 100644 index 0000000..c4014ae --- /dev/null +++ b/src/blooio/resources/groups/icon.py @@ -0,0 +1,290 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Mapping, cast + +import httpx + +from ..._files import deepcopy_with_paths +from ..._types import Body, Query, Headers, NotGiven, FileTypes, not_given +from ..._utils import extract_files, path_template, maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.groups import icon_set_params +from ...types.groups.group_icon import GroupIcon + +__all__ = ["IconResource", "AsyncIconResource"] + + +class IconResource(SyncAPIResource): + """Manage contact groups""" + + @cached_property + def with_raw_response(self) -> IconResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return IconResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> IconResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return IconResourceWithStreamingResponse(self) + + def remove( + self, + group_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupIcon: + """Remove the group icon/photo. + + Requires the group to have a linked chat_guid. + + The icon is removed from both Blooio storage and the linked iMessage chat before + the request returns. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return self._delete( + path_template("/groups/{group_id}/icon", group_id=group_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GroupIcon, + ) + + def set( + self, + group_id: str, + *, + icon: FileTypes, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupIcon: + """Set the group icon/photo. + + Requires the group to have a linked chat_guid. Uses + multipart/form-data. + + The uploaded image is stored in Blooio storage and synced to the linked iMessage + chat before the request returns. + + Args: + icon: The icon image file to set as the group photo + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + body = deepcopy_with_paths({"icon": icon}, [["icon"]]) + files = extract_files(cast(Mapping[str, object], body), paths=[["icon"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return self._post( + path_template("/groups/{group_id}/icon", group_id=group_id), + body=maybe_transform(body, icon_set_params.IconSetParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GroupIcon, + ) + + +class AsyncIconResource(AsyncAPIResource): + """Manage contact groups""" + + @cached_property + def with_raw_response(self) -> AsyncIconResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncIconResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncIconResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return AsyncIconResourceWithStreamingResponse(self) + + async def remove( + self, + group_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupIcon: + """Remove the group icon/photo. + + Requires the group to have a linked chat_guid. + + The icon is removed from both Blooio storage and the linked iMessage chat before + the request returns. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return await self._delete( + path_template("/groups/{group_id}/icon", group_id=group_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GroupIcon, + ) + + async def set( + self, + group_id: str, + *, + icon: FileTypes, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupIcon: + """Set the group icon/photo. + + Requires the group to have a linked chat_guid. Uses + multipart/form-data. + + The uploaded image is stored in Blooio storage and synced to the linked iMessage + chat before the request returns. + + Args: + icon: The icon image file to set as the group photo + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + body = deepcopy_with_paths({"icon": icon}, [["icon"]]) + files = extract_files(cast(Mapping[str, object], body), paths=[["icon"]]) + # It should be noted that the actual Content-Type header that will be + # sent to the server will contain a `boundary` parameter, e.g. + # multipart/form-data; boundary=---abc-- + extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})} + return await self._post( + path_template("/groups/{group_id}/icon", group_id=group_id), + body=await async_maybe_transform(body, icon_set_params.IconSetParams), + files=files, + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GroupIcon, + ) + + +class IconResourceWithRawResponse: + def __init__(self, icon: IconResource) -> None: + self._icon = icon + + self.remove = to_raw_response_wrapper( + icon.remove, + ) + self.set = to_raw_response_wrapper( + icon.set, + ) + + +class AsyncIconResourceWithRawResponse: + def __init__(self, icon: AsyncIconResource) -> None: + self._icon = icon + + self.remove = async_to_raw_response_wrapper( + icon.remove, + ) + self.set = async_to_raw_response_wrapper( + icon.set, + ) + + +class IconResourceWithStreamingResponse: + def __init__(self, icon: IconResource) -> None: + self._icon = icon + + self.remove = to_streamed_response_wrapper( + icon.remove, + ) + self.set = to_streamed_response_wrapper( + icon.set, + ) + + +class AsyncIconResourceWithStreamingResponse: + def __init__(self, icon: AsyncIconResource) -> None: + self._icon = icon + + self.remove = async_to_streamed_response_wrapper( + icon.remove, + ) + self.set = async_to_streamed_response_wrapper( + icon.set, + ) diff --git a/src/blooio/resources/groups/members.py b/src/blooio/resources/groups/members.py new file mode 100644 index 0000000..c14ef2b --- /dev/null +++ b/src/blooio/resources/groups/members.py @@ -0,0 +1,391 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._utils import path_template, maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.groups import member_add_params, member_list_params +from ...types.groups.member_add_response import MemberAddResponse +from ...types.groups.member_list_response import MemberListResponse +from ...types.groups.member_remove_response import MemberRemoveResponse + +__all__ = ["MembersResource", "AsyncMembersResource"] + + +class MembersResource(SyncAPIResource): + """Manage group membership""" + + @cached_property + def with_raw_response(self) -> MembersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return MembersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> MembersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return MembersResourceWithStreamingResponse(self) + + def list( + self, + group_id: str, + *, + limit: int | Omit = omit, + offset: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MemberListResponse: + """ + List all members of a group. + + Args: + limit: Maximum number of items to return (1-200) + + offset: Number of items to skip + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return self._get( + path_template("/groups/{group_id}/members", group_id=group_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + }, + member_list_params.MemberListParams, + ), + ), + cast_to=MemberListResponse, + ) + + def add( + self, + group_id: str, + *, + contact_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MemberAddResponse: + """ + ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize + this feature. + + Add an existing contact to a group. If the group is linked to an existing + iMessage chat, also adds the participant to that chat. + + Args: + contact_id: Contact identifier (phone number or email) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return self._post( + path_template("/groups/{group_id}/members", group_id=group_id), + body=maybe_transform({"contact_id": contact_id}, member_add_params.MemberAddParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MemberAddResponse, + ) + + def remove( + self, + contact_id: str, + *, + group_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MemberRemoveResponse: + """ + ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize + this feature. + + Remove a contact from a group. If the group is linked to an existing iMessage + chat, also removes the participant from that chat. If the contact being removed + is the organization's own phone number, leaves the group chat instead. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + if not contact_id: + raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") + return self._delete( + path_template("/groups/{group_id}/members/{contact_id}", group_id=group_id, contact_id=contact_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MemberRemoveResponse, + ) + + +class AsyncMembersResource(AsyncAPIResource): + """Manage group membership""" + + @cached_property + def with_raw_response(self) -> AsyncMembersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncMembersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncMembersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return AsyncMembersResourceWithStreamingResponse(self) + + async def list( + self, + group_id: str, + *, + limit: int | Omit = omit, + offset: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MemberListResponse: + """ + List all members of a group. + + Args: + limit: Maximum number of items to return (1-200) + + offset: Number of items to skip + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return await self._get( + path_template("/groups/{group_id}/members", group_id=group_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "limit": limit, + "offset": offset, + }, + member_list_params.MemberListParams, + ), + ), + cast_to=MemberListResponse, + ) + + async def add( + self, + group_id: str, + *, + contact_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MemberAddResponse: + """ + ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize + this feature. + + Add an existing contact to a group. If the group is linked to an existing + iMessage chat, also adds the participant to that chat. + + Args: + contact_id: Contact identifier (phone number or email) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return await self._post( + path_template("/groups/{group_id}/members", group_id=group_id), + body=await async_maybe_transform({"contact_id": contact_id}, member_add_params.MemberAddParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MemberAddResponse, + ) + + async def remove( + self, + contact_id: str, + *, + group_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MemberRemoveResponse: + """ + ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize + this feature. + + Remove a contact from a group. If the group is linked to an existing iMessage + chat, also removes the participant from that chat. If the contact being removed + is the organization's own phone number, leaves the group chat instead. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + if not contact_id: + raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") + return await self._delete( + path_template("/groups/{group_id}/members/{contact_id}", group_id=group_id, contact_id=contact_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MemberRemoveResponse, + ) + + +class MembersResourceWithRawResponse: + def __init__(self, members: MembersResource) -> None: + self._members = members + + self.list = to_raw_response_wrapper( + members.list, + ) + self.add = to_raw_response_wrapper( + members.add, + ) + self.remove = to_raw_response_wrapper( + members.remove, + ) + + +class AsyncMembersResourceWithRawResponse: + def __init__(self, members: AsyncMembersResource) -> None: + self._members = members + + self.list = async_to_raw_response_wrapper( + members.list, + ) + self.add = async_to_raw_response_wrapper( + members.add, + ) + self.remove = async_to_raw_response_wrapper( + members.remove, + ) + + +class MembersResourceWithStreamingResponse: + def __init__(self, members: MembersResource) -> None: + self._members = members + + self.list = to_streamed_response_wrapper( + members.list, + ) + self.add = to_streamed_response_wrapper( + members.add, + ) + self.remove = to_streamed_response_wrapper( + members.remove, + ) + + +class AsyncMembersResourceWithStreamingResponse: + def __init__(self, members: AsyncMembersResource) -> None: + self._members = members + + self.list = async_to_streamed_response_wrapper( + members.list, + ) + self.add = async_to_streamed_response_wrapper( + members.add, + ) + self.remove = async_to_streamed_response_wrapper( + members.remove, + ) diff --git a/src/blooio/resources/location/__init__.py b/src/blooio/resources/location/__init__.py new file mode 100644 index 0000000..f88a761 --- /dev/null +++ b/src/blooio/resources/location/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .contacts import ( + ContactsResource, + AsyncContactsResource, + ContactsResourceWithRawResponse, + AsyncContactsResourceWithRawResponse, + ContactsResourceWithStreamingResponse, + AsyncContactsResourceWithStreamingResponse, +) +from .location import ( + LocationResource, + AsyncLocationResource, + LocationResourceWithRawResponse, + AsyncLocationResourceWithRawResponse, + LocationResourceWithStreamingResponse, + AsyncLocationResourceWithStreamingResponse, +) + +__all__ = [ + "ContactsResource", + "AsyncContactsResource", + "ContactsResourceWithRawResponse", + "AsyncContactsResourceWithRawResponse", + "ContactsResourceWithStreamingResponse", + "AsyncContactsResourceWithStreamingResponse", + "LocationResource", + "AsyncLocationResource", + "LocationResourceWithRawResponse", + "AsyncLocationResourceWithRawResponse", + "LocationResourceWithStreamingResponse", + "AsyncLocationResourceWithStreamingResponse", +] diff --git a/src/blooio/resources/location/contacts.py b/src/blooio/resources/location/contacts.py new file mode 100644 index 0000000..06730d5 --- /dev/null +++ b/src/blooio/resources/location/contacts.py @@ -0,0 +1,288 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..._types import Body, Query, Headers, NotGiven, not_given +from ..._utils import path_template +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.location.contact_location import ContactLocation +from ...types.location.contact_list_response import ContactListResponse +from ...types.location.contact_refresh_response import ContactRefreshResponse + +__all__ = ["ContactsResource", "AsyncContactsResource"] + + +class ContactsResource(SyncAPIResource): + """FindMy contact location tracking""" + + @cached_property + def with_raw_response(self) -> ContactsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return ContactsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ContactsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return ContactsResourceWithStreamingResponse(self) + + def retrieve( + self, + handle: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ContactLocation: + """ + Returns the cached location for a specific contact identified by phone number or + email. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not handle: + raise ValueError(f"Expected a non-empty value for `handle` but received {handle!r}") + return self._get( + path_template("/location/contacts/{handle}", handle=handle), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ContactLocation, + ) + + def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ContactListResponse: + """ + Returns cached FindMy contact locations available through your blooio account. + Each entry includes the contact's handle (phone/email), coordinates, and last + update time. + """ + return self._get( + "/location/contacts", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ContactListResponse, + ) + + def refresh( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ContactRefreshResponse: + """Triggers a refresh of cached FindMy contact locations. + + Updated results may take + 15-20 seconds to appear. + """ + return self._post( + "/location/contacts/refresh", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ContactRefreshResponse, + ) + + +class AsyncContactsResource(AsyncAPIResource): + """FindMy contact location tracking""" + + @cached_property + def with_raw_response(self) -> AsyncContactsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncContactsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncContactsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return AsyncContactsResourceWithStreamingResponse(self) + + async def retrieve( + self, + handle: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ContactLocation: + """ + Returns the cached location for a specific contact identified by phone number or + email. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not handle: + raise ValueError(f"Expected a non-empty value for `handle` but received {handle!r}") + return await self._get( + path_template("/location/contacts/{handle}", handle=handle), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ContactLocation, + ) + + async def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ContactListResponse: + """ + Returns cached FindMy contact locations available through your blooio account. + Each entry includes the contact's handle (phone/email), coordinates, and last + update time. + """ + return await self._get( + "/location/contacts", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ContactListResponse, + ) + + async def refresh( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ContactRefreshResponse: + """Triggers a refresh of cached FindMy contact locations. + + Updated results may take + 15-20 seconds to appear. + """ + return await self._post( + "/location/contacts/refresh", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ContactRefreshResponse, + ) + + +class ContactsResourceWithRawResponse: + def __init__(self, contacts: ContactsResource) -> None: + self._contacts = contacts + + self.retrieve = to_raw_response_wrapper( + contacts.retrieve, + ) + self.list = to_raw_response_wrapper( + contacts.list, + ) + self.refresh = to_raw_response_wrapper( + contacts.refresh, + ) + + +class AsyncContactsResourceWithRawResponse: + def __init__(self, contacts: AsyncContactsResource) -> None: + self._contacts = contacts + + self.retrieve = async_to_raw_response_wrapper( + contacts.retrieve, + ) + self.list = async_to_raw_response_wrapper( + contacts.list, + ) + self.refresh = async_to_raw_response_wrapper( + contacts.refresh, + ) + + +class ContactsResourceWithStreamingResponse: + def __init__(self, contacts: ContactsResource) -> None: + self._contacts = contacts + + self.retrieve = to_streamed_response_wrapper( + contacts.retrieve, + ) + self.list = to_streamed_response_wrapper( + contacts.list, + ) + self.refresh = to_streamed_response_wrapper( + contacts.refresh, + ) + + +class AsyncContactsResourceWithStreamingResponse: + def __init__(self, contacts: AsyncContactsResource) -> None: + self._contacts = contacts + + self.retrieve = async_to_streamed_response_wrapper( + contacts.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + contacts.list, + ) + self.refresh = async_to_streamed_response_wrapper( + contacts.refresh, + ) diff --git a/src/blooio/resources/location/location.py b/src/blooio/resources/location/location.py new file mode 100644 index 0000000..c25a39a --- /dev/null +++ b/src/blooio/resources/location/location.py @@ -0,0 +1,108 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .contacts import ( + ContactsResource, + AsyncContactsResource, + ContactsResourceWithRawResponse, + AsyncContactsResourceWithRawResponse, + ContactsResourceWithStreamingResponse, + AsyncContactsResourceWithStreamingResponse, +) +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource + +__all__ = ["LocationResource", "AsyncLocationResource"] + + +class LocationResource(SyncAPIResource): + @cached_property + def contacts(self) -> ContactsResource: + """FindMy contact location tracking""" + return ContactsResource(self._client) + + @cached_property + def with_raw_response(self) -> LocationResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return LocationResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> LocationResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return LocationResourceWithStreamingResponse(self) + + +class AsyncLocationResource(AsyncAPIResource): + @cached_property + def contacts(self) -> AsyncContactsResource: + """FindMy contact location tracking""" + return AsyncContactsResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncLocationResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncLocationResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncLocationResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return AsyncLocationResourceWithStreamingResponse(self) + + +class LocationResourceWithRawResponse: + def __init__(self, location: LocationResource) -> None: + self._location = location + + @cached_property + def contacts(self) -> ContactsResourceWithRawResponse: + """FindMy contact location tracking""" + return ContactsResourceWithRawResponse(self._location.contacts) + + +class AsyncLocationResourceWithRawResponse: + def __init__(self, location: AsyncLocationResource) -> None: + self._location = location + + @cached_property + def contacts(self) -> AsyncContactsResourceWithRawResponse: + """FindMy contact location tracking""" + return AsyncContactsResourceWithRawResponse(self._location.contacts) + + +class LocationResourceWithStreamingResponse: + def __init__(self, location: LocationResource) -> None: + self._location = location + + @cached_property + def contacts(self) -> ContactsResourceWithStreamingResponse: + """FindMy contact location tracking""" + return ContactsResourceWithStreamingResponse(self._location.contacts) + + +class AsyncLocationResourceWithStreamingResponse: + def __init__(self, location: AsyncLocationResource) -> None: + self._location = location + + @cached_property + def contacts(self) -> AsyncContactsResourceWithStreamingResponse: + """FindMy contact location tracking""" + return AsyncContactsResourceWithStreamingResponse(self._location.contacts) diff --git a/src/blooio/resources/me/__init__.py b/src/blooio/resources/me/__init__.py new file mode 100644 index 0000000..370158e --- /dev/null +++ b/src/blooio/resources/me/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .me import ( + MeResource, + AsyncMeResource, + MeResourceWithRawResponse, + AsyncMeResourceWithRawResponse, + MeResourceWithStreamingResponse, + AsyncMeResourceWithStreamingResponse, +) +from .numbers import ( + NumbersResource, + AsyncNumbersResource, + NumbersResourceWithRawResponse, + AsyncNumbersResourceWithRawResponse, + NumbersResourceWithStreamingResponse, + AsyncNumbersResourceWithStreamingResponse, +) + +__all__ = [ + "NumbersResource", + "AsyncNumbersResource", + "NumbersResourceWithRawResponse", + "AsyncNumbersResourceWithRawResponse", + "NumbersResourceWithStreamingResponse", + "AsyncNumbersResourceWithStreamingResponse", + "MeResource", + "AsyncMeResource", + "MeResourceWithRawResponse", + "AsyncMeResourceWithRawResponse", + "MeResourceWithStreamingResponse", + "AsyncMeResourceWithStreamingResponse", +] diff --git a/src/blooio/resources/me.py b/src/blooio/resources/me/me.py similarity index 66% rename from src/blooio/resources/me.py rename to src/blooio/resources/me/me.py index 6059951..f605ad4 100644 --- a/src/blooio/resources/me.py +++ b/src/blooio/resources/me/me.py @@ -4,22 +4,37 @@ import httpx -from .._types import Body, Query, Headers, NotGiven, not_given -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( +from ..._types import Body, Query, Headers, NotGiven, not_given +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( to_raw_response_wrapper, to_streamed_response_wrapper, async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from .._base_client import make_request_options -from ..types.me_retrieve_response import MeRetrieveResponse +from ..._base_client import make_request_options +from .numbers.numbers import ( + NumbersResource, + AsyncNumbersResource, + NumbersResourceWithRawResponse, + AsyncNumbersResourceWithRawResponse, + NumbersResourceWithStreamingResponse, + AsyncNumbersResourceWithStreamingResponse, +) +from ...types.me_retrieve_response import MeRetrieveResponse __all__ = ["MeResource", "AsyncMeResource"] class MeResource(SyncAPIResource): + """Authentication and account information""" + + @cached_property + def numbers(self) -> NumbersResource: + """Manage phone numbers linked to your account""" + return NumbersResource(self._client) + @cached_property def with_raw_response(self) -> MeResourceWithRawResponse: """ @@ -50,11 +65,11 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> MeRetrieveResponse: """ - Returns information about the authenticated API key including plan, devices, - usage statistics, and integration details. + Returns details about the authenticated API key or dashboard user, including + organization info, devices, and usage statistics. """ return self._get( - "/v1/api/me", + "/me", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -63,6 +78,13 @@ def retrieve( class AsyncMeResource(AsyncAPIResource): + """Authentication and account information""" + + @cached_property + def numbers(self) -> AsyncNumbersResource: + """Manage phone numbers linked to your account""" + return AsyncNumbersResource(self._client) + @cached_property def with_raw_response(self) -> AsyncMeResourceWithRawResponse: """ @@ -93,11 +115,11 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> MeRetrieveResponse: """ - Returns information about the authenticated API key including plan, devices, - usage statistics, and integration details. + Returns details about the authenticated API key or dashboard user, including + organization info, devices, and usage statistics. """ return await self._get( - "/v1/api/me", + "/me", options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), @@ -113,6 +135,11 @@ def __init__(self, me: MeResource) -> None: me.retrieve, ) + @cached_property + def numbers(self) -> NumbersResourceWithRawResponse: + """Manage phone numbers linked to your account""" + return NumbersResourceWithRawResponse(self._me.numbers) + class AsyncMeResourceWithRawResponse: def __init__(self, me: AsyncMeResource) -> None: @@ -122,6 +149,11 @@ def __init__(self, me: AsyncMeResource) -> None: me.retrieve, ) + @cached_property + def numbers(self) -> AsyncNumbersResourceWithRawResponse: + """Manage phone numbers linked to your account""" + return AsyncNumbersResourceWithRawResponse(self._me.numbers) + class MeResourceWithStreamingResponse: def __init__(self, me: MeResource) -> None: @@ -131,6 +163,11 @@ def __init__(self, me: MeResource) -> None: me.retrieve, ) + @cached_property + def numbers(self) -> NumbersResourceWithStreamingResponse: + """Manage phone numbers linked to your account""" + return NumbersResourceWithStreamingResponse(self._me.numbers) + class AsyncMeResourceWithStreamingResponse: def __init__(self, me: AsyncMeResource) -> None: @@ -139,3 +176,8 @@ def __init__(self, me: AsyncMeResource) -> None: self.retrieve = async_to_streamed_response_wrapper( me.retrieve, ) + + @cached_property + def numbers(self) -> AsyncNumbersResourceWithStreamingResponse: + """Manage phone numbers linked to your account""" + return AsyncNumbersResourceWithStreamingResponse(self._me.numbers) diff --git a/src/blooio/resources/me/numbers/__init__.py b/src/blooio/resources/me/numbers/__init__.py new file mode 100644 index 0000000..a0678cc --- /dev/null +++ b/src/blooio/resources/me/numbers/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .numbers import ( + NumbersResource, + AsyncNumbersResource, + NumbersResourceWithRawResponse, + AsyncNumbersResourceWithRawResponse, + NumbersResourceWithStreamingResponse, + AsyncNumbersResourceWithStreamingResponse, +) +from .contact_card import ( + ContactCardResource, + AsyncContactCardResource, + ContactCardResourceWithRawResponse, + AsyncContactCardResourceWithRawResponse, + ContactCardResourceWithStreamingResponse, + AsyncContactCardResourceWithStreamingResponse, +) + +__all__ = [ + "ContactCardResource", + "AsyncContactCardResource", + "ContactCardResourceWithRawResponse", + "AsyncContactCardResourceWithRawResponse", + "ContactCardResourceWithStreamingResponse", + "AsyncContactCardResourceWithStreamingResponse", + "NumbersResource", + "AsyncNumbersResource", + "NumbersResourceWithRawResponse", + "AsyncNumbersResourceWithRawResponse", + "NumbersResourceWithStreamingResponse", + "AsyncNumbersResourceWithStreamingResponse", +] diff --git a/src/blooio/resources/me/numbers/contact_card.py b/src/blooio/resources/me/numbers/contact_card.py new file mode 100644 index 0000000..fa47ab0 --- /dev/null +++ b/src/blooio/resources/me/numbers/contact_card.py @@ -0,0 +1,296 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ...._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ...._utils import path_template, maybe_transform, async_maybe_transform +from ...._compat import cached_property +from ...._resource import SyncAPIResource, AsyncAPIResource +from ...._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...._base_client import make_request_options +from ....types.me.numbers import contact_card_update_params +from ....types.me.numbers.contact_card_update_response import ContactCardUpdateResponse +from ....types.me.numbers.contact_card_retrieve_response import ContactCardRetrieveResponse + +__all__ = ["ContactCardResource", "AsyncContactCardResource"] + + +class ContactCardResource(SyncAPIResource): + """Manage and share your iMessage contact card (Name & Photo)""" + + @cached_property + def with_raw_response(self) -> ContactCardResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return ContactCardResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ContactCardResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return ContactCardResourceWithStreamingResponse(self) + + def retrieve( + self, + number: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ContactCardRetrieveResponse: + """ + ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize + this feature. + + Get the personal contact card (Name & Photo) for the specified phone number. + This is the identity that gets shared with contacts in iMessage. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not number: + raise ValueError(f"Expected a non-empty value for `number` but received {number!r}") + return self._get( + path_template("/me/numbers/{number}/contact-card", number=number), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ContactCardRetrieveResponse, + ) + + def update( + self, + number: str, + *, + avatar: str | Omit = omit, + first_name: str | Omit = omit, + last_name: str | Omit = omit, + sharing: contact_card_update_params.Sharing | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ContactCardUpdateResponse: + """ + Update the personal contact card (Name & Photo) for the specified phone number. + All fields are optional — only provided fields are updated. + + Args: + avatar: Profile photo as base64-encoded JPEG/PNG + + first_name: First name + + last_name: Last name + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not number: + raise ValueError(f"Expected a non-empty value for `number` but received {number!r}") + return self._put( + path_template("/me/numbers/{number}/contact-card", number=number), + body=maybe_transform( + { + "avatar": avatar, + "first_name": first_name, + "last_name": last_name, + "sharing": sharing, + }, + contact_card_update_params.ContactCardUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ContactCardUpdateResponse, + ) + + +class AsyncContactCardResource(AsyncAPIResource): + """Manage and share your iMessage contact card (Name & Photo)""" + + @cached_property + def with_raw_response(self) -> AsyncContactCardResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncContactCardResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncContactCardResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return AsyncContactCardResourceWithStreamingResponse(self) + + async def retrieve( + self, + number: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ContactCardRetrieveResponse: + """ + ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize + this feature. + + Get the personal contact card (Name & Photo) for the specified phone number. + This is the identity that gets shared with contacts in iMessage. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not number: + raise ValueError(f"Expected a non-empty value for `number` but received {number!r}") + return await self._get( + path_template("/me/numbers/{number}/contact-card", number=number), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ContactCardRetrieveResponse, + ) + + async def update( + self, + number: str, + *, + avatar: str | Omit = omit, + first_name: str | Omit = omit, + last_name: str | Omit = omit, + sharing: contact_card_update_params.Sharing | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> ContactCardUpdateResponse: + """ + Update the personal contact card (Name & Photo) for the specified phone number. + All fields are optional — only provided fields are updated. + + Args: + avatar: Profile photo as base64-encoded JPEG/PNG + + first_name: First name + + last_name: Last name + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not number: + raise ValueError(f"Expected a non-empty value for `number` but received {number!r}") + return await self._put( + path_template("/me/numbers/{number}/contact-card", number=number), + body=await async_maybe_transform( + { + "avatar": avatar, + "first_name": first_name, + "last_name": last_name, + "sharing": sharing, + }, + contact_card_update_params.ContactCardUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ContactCardUpdateResponse, + ) + + +class ContactCardResourceWithRawResponse: + def __init__(self, contact_card: ContactCardResource) -> None: + self._contact_card = contact_card + + self.retrieve = to_raw_response_wrapper( + contact_card.retrieve, + ) + self.update = to_raw_response_wrapper( + contact_card.update, + ) + + +class AsyncContactCardResourceWithRawResponse: + def __init__(self, contact_card: AsyncContactCardResource) -> None: + self._contact_card = contact_card + + self.retrieve = async_to_raw_response_wrapper( + contact_card.retrieve, + ) + self.update = async_to_raw_response_wrapper( + contact_card.update, + ) + + +class ContactCardResourceWithStreamingResponse: + def __init__(self, contact_card: ContactCardResource) -> None: + self._contact_card = contact_card + + self.retrieve = to_streamed_response_wrapper( + contact_card.retrieve, + ) + self.update = to_streamed_response_wrapper( + contact_card.update, + ) + + +class AsyncContactCardResourceWithStreamingResponse: + def __init__(self, contact_card: AsyncContactCardResource) -> None: + self._contact_card = contact_card + + self.retrieve = async_to_streamed_response_wrapper( + contact_card.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + contact_card.update, + ) diff --git a/src/blooio/resources/me/numbers/numbers.py b/src/blooio/resources/me/numbers/numbers.py new file mode 100644 index 0000000..4da6fd2 --- /dev/null +++ b/src/blooio/resources/me/numbers/numbers.py @@ -0,0 +1,187 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ...._types import Body, Query, Headers, NotGiven, not_given +from ...._compat import cached_property +from ...._resource import SyncAPIResource, AsyncAPIResource +from ...._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .contact_card import ( + ContactCardResource, + AsyncContactCardResource, + ContactCardResourceWithRawResponse, + AsyncContactCardResourceWithRawResponse, + ContactCardResourceWithStreamingResponse, + AsyncContactCardResourceWithStreamingResponse, +) +from ...._base_client import make_request_options +from ....types.me.number_list_response import NumberListResponse + +__all__ = ["NumbersResource", "AsyncNumbersResource"] + + +class NumbersResource(SyncAPIResource): + """Manage phone numbers linked to your account""" + + @cached_property + def contact_card(self) -> ContactCardResource: + """Manage and share your iMessage contact card (Name & Photo)""" + return ContactCardResource(self._client) + + @cached_property + def with_raw_response(self) -> NumbersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return NumbersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> NumbersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return NumbersResourceWithStreamingResponse(self) + + def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> NumberListResponse: + """List all phone numbers bound to this API key with their availability status. + + Use + the returned phone numbers as the `:number` path parameter for other + `/me/numbers/` endpoints. + """ + return self._get( + "/me/numbers", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NumberListResponse, + ) + + +class AsyncNumbersResource(AsyncAPIResource): + """Manage phone numbers linked to your account""" + + @cached_property + def contact_card(self) -> AsyncContactCardResource: + """Manage and share your iMessage contact card (Name & Photo)""" + return AsyncContactCardResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncNumbersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncNumbersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncNumbersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return AsyncNumbersResourceWithStreamingResponse(self) + + async def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> NumberListResponse: + """List all phone numbers bound to this API key with their availability status. + + Use + the returned phone numbers as the `:number` path parameter for other + `/me/numbers/` endpoints. + """ + return await self._get( + "/me/numbers", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NumberListResponse, + ) + + +class NumbersResourceWithRawResponse: + def __init__(self, numbers: NumbersResource) -> None: + self._numbers = numbers + + self.list = to_raw_response_wrapper( + numbers.list, + ) + + @cached_property + def contact_card(self) -> ContactCardResourceWithRawResponse: + """Manage and share your iMessage contact card (Name & Photo)""" + return ContactCardResourceWithRawResponse(self._numbers.contact_card) + + +class AsyncNumbersResourceWithRawResponse: + def __init__(self, numbers: AsyncNumbersResource) -> None: + self._numbers = numbers + + self.list = async_to_raw_response_wrapper( + numbers.list, + ) + + @cached_property + def contact_card(self) -> AsyncContactCardResourceWithRawResponse: + """Manage and share your iMessage contact card (Name & Photo)""" + return AsyncContactCardResourceWithRawResponse(self._numbers.contact_card) + + +class NumbersResourceWithStreamingResponse: + def __init__(self, numbers: NumbersResource) -> None: + self._numbers = numbers + + self.list = to_streamed_response_wrapper( + numbers.list, + ) + + @cached_property + def contact_card(self) -> ContactCardResourceWithStreamingResponse: + """Manage and share your iMessage contact card (Name & Photo)""" + return ContactCardResourceWithStreamingResponse(self._numbers.contact_card) + + +class AsyncNumbersResourceWithStreamingResponse: + def __init__(self, numbers: AsyncNumbersResource) -> None: + self._numbers = numbers + + self.list = async_to_streamed_response_wrapper( + numbers.list, + ) + + @cached_property + def contact_card(self) -> AsyncContactCardResourceWithStreamingResponse: + """Manage and share your iMessage contact card (Name & Photo)""" + return AsyncContactCardResourceWithStreamingResponse(self._numbers.contact_card) diff --git a/src/blooio/resources/messages.py b/src/blooio/resources/messages.py deleted file mode 100644 index 9025a7c..0000000 --- a/src/blooio/resources/messages.py +++ /dev/null @@ -1,466 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import httpx - -from ..types import message_send_params -from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given -from .._utils import maybe_transform, strip_not_given, async_maybe_transform -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( - to_raw_response_wrapper, - to_streamed_response_wrapper, - async_to_raw_response_wrapper, - async_to_streamed_response_wrapper, -) -from .._base_client import make_request_options -from ..types.message_send_response import MessageSendResponse -from ..types.message_cancel_response import MessageCancelResponse -from ..types.message_retrieve_response import MessageRetrieveResponse -from ..types.message_get_status_response import MessageGetStatusResponse - -__all__ = ["MessagesResource", "AsyncMessagesResource"] - - -class MessagesResource(SyncAPIResource): - @cached_property - def with_raw_response(self) -> MessagesResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers - """ - return MessagesResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> MessagesResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response - """ - return MessagesResourceWithStreamingResponse(self) - - def retrieve( - self, - message_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MessageRetrieveResponse: - """ - Retrieve full message metadata including direction, protocol, text length, - attachments count, timestamps, current status, and original metadata. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not message_id: - raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") - return self._get( - f"/v1/api/messages/{message_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=MessageRetrieveResponse, - ) - - def cancel( - self, - message_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MessageCancelResponse: - """Request cancellation for a queued or scheduled message. - - If the message is - already in a non-cancellable state (e.g., sent), the response indicates the - current status. Note: response may include deprecated `current_status`; use - `status` instead. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not message_id: - raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") - return self._delete( - f"/v1/api/messages/{message_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=MessageCancelResponse, - ) - - def get_status( - self, - message_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MessageGetStatusResponse: - """ - Get current delivery status only - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not message_id: - raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") - return self._get( - f"/v1/api/messages/{message_id}/status", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=MessageGetStatusResponse, - ) - - def send( - self, - *, - to: str, - attachments: SequenceNotStr[str] | Omit = omit, - metadata: object | Omit = omit, - text: str | Omit = omit, - idempotency_key: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MessageSendResponse: - """ - Queues an outbound iMessage/SMS to the specified phone number. - - Supports optional media attachments via public URLs, and arbitrary metadata for - correlation. - - For safe retries, provide an `Idempotency-Key` header. If the same key is used - twice, the original message will be returned with 200; otherwise a new message - is queued with 202. - - Args: - to: Recipient phone number in E.164 format (e.g., +15551234567) - - attachments: Array of publicly accessible URLs to media attachments. - - metadata: Arbitrary key/value pairs to associate with the message. - - text: Text body of the message. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - extra_headers = {**strip_not_given({"Idempotency-Key": idempotency_key}), **(extra_headers or {})} - return self._post( - "/v1/api/messages", - body=maybe_transform( - { - "to": to, - "attachments": attachments, - "metadata": metadata, - "text": text, - }, - message_send_params.MessageSendParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=MessageSendResponse, - ) - - -class AsyncMessagesResource(AsyncAPIResource): - @cached_property - def with_raw_response(self) -> AsyncMessagesResourceWithRawResponse: - """ - This property can be used as a prefix for any HTTP method call to return - the raw response object instead of the parsed content. - - For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers - """ - return AsyncMessagesResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncMessagesResourceWithStreamingResponse: - """ - An alternative to `.with_raw_response` that doesn't eagerly read the response body. - - For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response - """ - return AsyncMessagesResourceWithStreamingResponse(self) - - async def retrieve( - self, - message_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MessageRetrieveResponse: - """ - Retrieve full message metadata including direction, protocol, text length, - attachments count, timestamps, current status, and original metadata. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not message_id: - raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") - return await self._get( - f"/v1/api/messages/{message_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=MessageRetrieveResponse, - ) - - async def cancel( - self, - message_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MessageCancelResponse: - """Request cancellation for a queued or scheduled message. - - If the message is - already in a non-cancellable state (e.g., sent), the response indicates the - current status. Note: response may include deprecated `current_status`; use - `status` instead. - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not message_id: - raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") - return await self._delete( - f"/v1/api/messages/{message_id}", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=MessageCancelResponse, - ) - - async def get_status( - self, - message_id: str, - *, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MessageGetStatusResponse: - """ - Get current delivery status only - - Args: - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - if not message_id: - raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") - return await self._get( - f"/v1/api/messages/{message_id}/status", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=MessageGetStatusResponse, - ) - - async def send( - self, - *, - to: str, - attachments: SequenceNotStr[str] | Omit = omit, - metadata: object | Omit = omit, - text: str | Omit = omit, - idempotency_key: str | Omit = omit, - # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. - # The extra values given here take precedence over values defined on the client or passed to this method. - extra_headers: Headers | None = None, - extra_query: Query | None = None, - extra_body: Body | None = None, - timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MessageSendResponse: - """ - Queues an outbound iMessage/SMS to the specified phone number. - - Supports optional media attachments via public URLs, and arbitrary metadata for - correlation. - - For safe retries, provide an `Idempotency-Key` header. If the same key is used - twice, the original message will be returned with 200; otherwise a new message - is queued with 202. - - Args: - to: Recipient phone number in E.164 format (e.g., +15551234567) - - attachments: Array of publicly accessible URLs to media attachments. - - metadata: Arbitrary key/value pairs to associate with the message. - - text: Text body of the message. - - extra_headers: Send extra headers - - extra_query: Add additional query parameters to the request - - extra_body: Add additional JSON properties to the request - - timeout: Override the client-level default timeout for this request, in seconds - """ - extra_headers = {**strip_not_given({"Idempotency-Key": idempotency_key}), **(extra_headers or {})} - return await self._post( - "/v1/api/messages", - body=await async_maybe_transform( - { - "to": to, - "attachments": attachments, - "metadata": metadata, - "text": text, - }, - message_send_params.MessageSendParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=MessageSendResponse, - ) - - -class MessagesResourceWithRawResponse: - def __init__(self, messages: MessagesResource) -> None: - self._messages = messages - - self.retrieve = to_raw_response_wrapper( - messages.retrieve, - ) - self.cancel = to_raw_response_wrapper( - messages.cancel, - ) - self.get_status = to_raw_response_wrapper( - messages.get_status, - ) - self.send = to_raw_response_wrapper( - messages.send, - ) - - -class AsyncMessagesResourceWithRawResponse: - def __init__(self, messages: AsyncMessagesResource) -> None: - self._messages = messages - - self.retrieve = async_to_raw_response_wrapper( - messages.retrieve, - ) - self.cancel = async_to_raw_response_wrapper( - messages.cancel, - ) - self.get_status = async_to_raw_response_wrapper( - messages.get_status, - ) - self.send = async_to_raw_response_wrapper( - messages.send, - ) - - -class MessagesResourceWithStreamingResponse: - def __init__(self, messages: MessagesResource) -> None: - self._messages = messages - - self.retrieve = to_streamed_response_wrapper( - messages.retrieve, - ) - self.cancel = to_streamed_response_wrapper( - messages.cancel, - ) - self.get_status = to_streamed_response_wrapper( - messages.get_status, - ) - self.send = to_streamed_response_wrapper( - messages.send, - ) - - -class AsyncMessagesResourceWithStreamingResponse: - def __init__(self, messages: AsyncMessagesResource) -> None: - self._messages = messages - - self.retrieve = async_to_streamed_response_wrapper( - messages.retrieve, - ) - self.cancel = async_to_streamed_response_wrapper( - messages.cancel, - ) - self.get_status = async_to_streamed_response_wrapper( - messages.get_status, - ) - self.send = async_to_streamed_response_wrapper( - messages.send, - ) diff --git a/src/blooio/resources/phone_numbers/__init__.py b/src/blooio/resources/phone_numbers/__init__.py new file mode 100644 index 0000000..df2e053 --- /dev/null +++ b/src/blooio/resources/phone_numbers/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .lookup import ( + LookupResource, + AsyncLookupResource, + LookupResourceWithRawResponse, + AsyncLookupResourceWithRawResponse, + LookupResourceWithStreamingResponse, + AsyncLookupResourceWithStreamingResponse, +) +from .phone_numbers import ( + PhoneNumbersResource, + AsyncPhoneNumbersResource, + PhoneNumbersResourceWithRawResponse, + AsyncPhoneNumbersResourceWithRawResponse, + PhoneNumbersResourceWithStreamingResponse, + AsyncPhoneNumbersResourceWithStreamingResponse, +) + +__all__ = [ + "LookupResource", + "AsyncLookupResource", + "LookupResourceWithRawResponse", + "AsyncLookupResourceWithRawResponse", + "LookupResourceWithStreamingResponse", + "AsyncLookupResourceWithStreamingResponse", + "PhoneNumbersResource", + "AsyncPhoneNumbersResource", + "PhoneNumbersResourceWithRawResponse", + "AsyncPhoneNumbersResourceWithRawResponse", + "PhoneNumbersResourceWithStreamingResponse", + "AsyncPhoneNumbersResourceWithStreamingResponse", +] diff --git a/src/blooio/resources/phone_numbers/lookup.py b/src/blooio/resources/phone_numbers/lookup.py new file mode 100644 index 0000000..76f3463 --- /dev/null +++ b/src/blooio/resources/phone_numbers/lookup.py @@ -0,0 +1,283 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..._types import Body, Query, Headers, NotGiven, not_given +from ..._utils import maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.phone_numbers import lookup_create_params, lookup_retrieve_params +from ...types.phone_numbers.phone_number_lookup_result import PhoneNumberLookupResult + +__all__ = ["LookupResource", "AsyncLookupResource"] + + +class LookupResource(SyncAPIResource): + """Phone number validation, formatting, and NANPA geocoding. + + Requires an Enterprise plan (Dedicated Enterprise). + """ + + @cached_property + def with_raw_response(self) -> LookupResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return LookupResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> LookupResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return LookupResourceWithStreamingResponse(self) + + def create( + self, + *, + number: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PhoneNumberLookupResult: + """ + Same as the GET endpoint, but accepts the phone number in the request body. + Useful when the number contains characters that are difficult to URL-encode. + + **Requires an Enterprise plan** (Dedicated Enterprise). + + Args: + number: Phone number to look up + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/phone-numbers/lookup", + body=maybe_transform({"number": number}, lookup_create_params.LookupCreateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PhoneNumberLookupResult, + ) + + def retrieve( + self, + *, + number: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PhoneNumberLookupResult: + """ + Returns detailed information about a phone number including validation, + formatting (E.164, national, international), number type, and NANPA geocoding + (city, state/province) for North American numbers. The geocoding data is sourced + from different database with 240,000+ NPA-NXX entries. + + **Requires an Enterprise plan** (Dedicated Enterprise). Returns 403 if your + organization does not have an active enterprise subscription. + + Args: + number: Phone number to look up. Can be E.164 format (+12125551234), national format + (2125551234), or with formatting ((212) 555-1234). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get( + "/phone-numbers/lookup", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"number": number}, lookup_retrieve_params.LookupRetrieveParams), + ), + cast_to=PhoneNumberLookupResult, + ) + + +class AsyncLookupResource(AsyncAPIResource): + """Phone number validation, formatting, and NANPA geocoding. + + Requires an Enterprise plan (Dedicated Enterprise). + """ + + @cached_property + def with_raw_response(self) -> AsyncLookupResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncLookupResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncLookupResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return AsyncLookupResourceWithStreamingResponse(self) + + async def create( + self, + *, + number: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PhoneNumberLookupResult: + """ + Same as the GET endpoint, but accepts the phone number in the request body. + Useful when the number contains characters that are difficult to URL-encode. + + **Requires an Enterprise plan** (Dedicated Enterprise). + + Args: + number: Phone number to look up + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/phone-numbers/lookup", + body=await async_maybe_transform({"number": number}, lookup_create_params.LookupCreateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PhoneNumberLookupResult, + ) + + async def retrieve( + self, + *, + number: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PhoneNumberLookupResult: + """ + Returns detailed information about a phone number including validation, + formatting (E.164, national, international), number type, and NANPA geocoding + (city, state/province) for North American numbers. The geocoding data is sourced + from different database with 240,000+ NPA-NXX entries. + + **Requires an Enterprise plan** (Dedicated Enterprise). Returns 403 if your + organization does not have an active enterprise subscription. + + Args: + number: Phone number to look up. Can be E.164 format (+12125551234), national format + (2125551234), or with formatting ((212) 555-1234). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._get( + "/phone-numbers/lookup", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform({"number": number}, lookup_retrieve_params.LookupRetrieveParams), + ), + cast_to=PhoneNumberLookupResult, + ) + + +class LookupResourceWithRawResponse: + def __init__(self, lookup: LookupResource) -> None: + self._lookup = lookup + + self.create = to_raw_response_wrapper( + lookup.create, + ) + self.retrieve = to_raw_response_wrapper( + lookup.retrieve, + ) + + +class AsyncLookupResourceWithRawResponse: + def __init__(self, lookup: AsyncLookupResource) -> None: + self._lookup = lookup + + self.create = async_to_raw_response_wrapper( + lookup.create, + ) + self.retrieve = async_to_raw_response_wrapper( + lookup.retrieve, + ) + + +class LookupResourceWithStreamingResponse: + def __init__(self, lookup: LookupResource) -> None: + self._lookup = lookup + + self.create = to_streamed_response_wrapper( + lookup.create, + ) + self.retrieve = to_streamed_response_wrapper( + lookup.retrieve, + ) + + +class AsyncLookupResourceWithStreamingResponse: + def __init__(self, lookup: AsyncLookupResource) -> None: + self._lookup = lookup + + self.create = async_to_streamed_response_wrapper( + lookup.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + lookup.retrieve, + ) diff --git a/src/blooio/resources/phone_numbers/phone_numbers.py b/src/blooio/resources/phone_numbers/phone_numbers.py new file mode 100644 index 0000000..92d442c --- /dev/null +++ b/src/blooio/resources/phone_numbers/phone_numbers.py @@ -0,0 +1,245 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from .lookup import ( + LookupResource, + AsyncLookupResource, + LookupResourceWithRawResponse, + AsyncLookupResourceWithRawResponse, + LookupResourceWithStreamingResponse, + AsyncLookupResourceWithStreamingResponse, +) +from ...types import phone_number_batch_create_params +from ..._types import Body, Query, Headers, NotGiven, SequenceNotStr, not_given +from ..._utils import maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.phone_number_batch_create_response import PhoneNumberBatchCreateResponse + +__all__ = ["PhoneNumbersResource", "AsyncPhoneNumbersResource"] + + +class PhoneNumbersResource(SyncAPIResource): + """Phone number validation, formatting, and NANPA geocoding. + + Requires an Enterprise plan (Dedicated Enterprise). + """ + + @cached_property + def lookup(self) -> LookupResource: + """Phone number validation, formatting, and NANPA geocoding. + + Requires an Enterprise plan (Dedicated Enterprise). + """ + return LookupResource(self._client) + + @cached_property + def with_raw_response(self) -> PhoneNumbersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return PhoneNumbersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> PhoneNumbersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return PhoneNumbersResourceWithStreamingResponse(self) + + def batch_create( + self, + *, + numbers: SequenceNotStr[str], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PhoneNumberBatchCreateResponse: + """Look up multiple phone numbers in a single request. + + Returns the same detailed + information as the single lookup endpoint for each number. Maximum 100 numbers + per request. + + **Requires an Enterprise plan** (Dedicated Enterprise). + + Args: + numbers: Array of phone numbers to look up + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/phone-numbers/batch", + body=maybe_transform({"numbers": numbers}, phone_number_batch_create_params.PhoneNumberBatchCreateParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PhoneNumberBatchCreateResponse, + ) + + +class AsyncPhoneNumbersResource(AsyncAPIResource): + """Phone number validation, formatting, and NANPA geocoding. + + Requires an Enterprise plan (Dedicated Enterprise). + """ + + @cached_property + def lookup(self) -> AsyncLookupResource: + """Phone number validation, formatting, and NANPA geocoding. + + Requires an Enterprise plan (Dedicated Enterprise). + """ + return AsyncLookupResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncPhoneNumbersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncPhoneNumbersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncPhoneNumbersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return AsyncPhoneNumbersResourceWithStreamingResponse(self) + + async def batch_create( + self, + *, + numbers: SequenceNotStr[str], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> PhoneNumberBatchCreateResponse: + """Look up multiple phone numbers in a single request. + + Returns the same detailed + information as the single lookup endpoint for each number. Maximum 100 numbers + per request. + + **Requires an Enterprise plan** (Dedicated Enterprise). + + Args: + numbers: Array of phone numbers to look up + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/phone-numbers/batch", + body=await async_maybe_transform( + {"numbers": numbers}, phone_number_batch_create_params.PhoneNumberBatchCreateParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PhoneNumberBatchCreateResponse, + ) + + +class PhoneNumbersResourceWithRawResponse: + def __init__(self, phone_numbers: PhoneNumbersResource) -> None: + self._phone_numbers = phone_numbers + + self.batch_create = to_raw_response_wrapper( + phone_numbers.batch_create, + ) + + @cached_property + def lookup(self) -> LookupResourceWithRawResponse: + """Phone number validation, formatting, and NANPA geocoding. + + Requires an Enterprise plan (Dedicated Enterprise). + """ + return LookupResourceWithRawResponse(self._phone_numbers.lookup) + + +class AsyncPhoneNumbersResourceWithRawResponse: + def __init__(self, phone_numbers: AsyncPhoneNumbersResource) -> None: + self._phone_numbers = phone_numbers + + self.batch_create = async_to_raw_response_wrapper( + phone_numbers.batch_create, + ) + + @cached_property + def lookup(self) -> AsyncLookupResourceWithRawResponse: + """Phone number validation, formatting, and NANPA geocoding. + + Requires an Enterprise plan (Dedicated Enterprise). + """ + return AsyncLookupResourceWithRawResponse(self._phone_numbers.lookup) + + +class PhoneNumbersResourceWithStreamingResponse: + def __init__(self, phone_numbers: PhoneNumbersResource) -> None: + self._phone_numbers = phone_numbers + + self.batch_create = to_streamed_response_wrapper( + phone_numbers.batch_create, + ) + + @cached_property + def lookup(self) -> LookupResourceWithStreamingResponse: + """Phone number validation, formatting, and NANPA geocoding. + + Requires an Enterprise plan (Dedicated Enterprise). + """ + return LookupResourceWithStreamingResponse(self._phone_numbers.lookup) + + +class AsyncPhoneNumbersResourceWithStreamingResponse: + def __init__(self, phone_numbers: AsyncPhoneNumbersResource) -> None: + self._phone_numbers = phone_numbers + + self.batch_create = async_to_streamed_response_wrapper( + phone_numbers.batch_create, + ) + + @cached_property + def lookup(self) -> AsyncLookupResourceWithStreamingResponse: + """Phone number validation, formatting, and NANPA geocoding. + + Requires an Enterprise plan (Dedicated Enterprise). + """ + return AsyncLookupResourceWithStreamingResponse(self._phone_numbers.lookup) diff --git a/src/blooio/resources/webhooks/__init__.py b/src/blooio/resources/webhooks/__init__.py new file mode 100644 index 0000000..5a63520 --- /dev/null +++ b/src/blooio/resources/webhooks/__init__.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .logs import ( + LogsResource, + AsyncLogsResource, + LogsResourceWithRawResponse, + AsyncLogsResourceWithRawResponse, + LogsResourceWithStreamingResponse, + AsyncLogsResourceWithStreamingResponse, +) +from .secret import ( + SecretResource, + AsyncSecretResource, + SecretResourceWithRawResponse, + AsyncSecretResourceWithRawResponse, + SecretResourceWithStreamingResponse, + AsyncSecretResourceWithStreamingResponse, +) +from .webhooks import ( + WebhooksResource, + AsyncWebhooksResource, + WebhooksResourceWithRawResponse, + AsyncWebhooksResourceWithRawResponse, + WebhooksResourceWithStreamingResponse, + AsyncWebhooksResourceWithStreamingResponse, +) + +__all__ = [ + "SecretResource", + "AsyncSecretResource", + "SecretResourceWithRawResponse", + "AsyncSecretResourceWithRawResponse", + "SecretResourceWithStreamingResponse", + "AsyncSecretResourceWithStreamingResponse", + "LogsResource", + "AsyncLogsResource", + "LogsResourceWithRawResponse", + "AsyncLogsResourceWithRawResponse", + "LogsResourceWithStreamingResponse", + "AsyncLogsResourceWithStreamingResponse", + "WebhooksResource", + "AsyncWebhooksResource", + "WebhooksResourceWithRawResponse", + "AsyncWebhooksResourceWithRawResponse", + "WebhooksResourceWithStreamingResponse", + "AsyncWebhooksResourceWithStreamingResponse", +] diff --git a/src/blooio/resources/webhooks/logs.py b/src/blooio/resources/webhooks/logs.py new file mode 100644 index 0000000..c45621e --- /dev/null +++ b/src/blooio/resources/webhooks/logs.py @@ -0,0 +1,320 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._utils import path_template, maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.webhooks import log_list_params +from ...types.webhooks.log_list_response import LogListResponse +from ...types.webhooks.log_replay_response import LogReplayResponse + +__all__ = ["LogsResource", "AsyncLogsResource"] + + +class LogsResource(SyncAPIResource): + """View and replay webhook deliveries""" + + @cached_property + def with_raw_response(self) -> LogsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return LogsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> LogsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return LogsResourceWithStreamingResponse(self) + + def list( + self, + webhook_id: str, + *, + limit: int | Omit = omit, + max_status: int | Omit = omit, + min_status: int | Omit = omit, + offset: int | Omit = omit, + sort: Literal["asc", "desc"] | Omit = omit, + status: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> LogListResponse: + """ + List delivery logs for a specific webhook. + + Args: + limit: Maximum number of items to return (1-200) + + max_status: Maximum HTTP status code + + min_status: Minimum HTTP status code + + offset: Number of items to skip + + sort: Sort order by attempted time + + status: Filter by exact HTTP status code + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not webhook_id: + raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}") + return self._get( + path_template("/webhooks/{webhook_id}/logs", webhook_id=webhook_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "max_status": max_status, + "min_status": min_status, + "offset": offset, + "sort": sort, + "status": status, + }, + log_list_params.LogListParams, + ), + ), + cast_to=LogListResponse, + ) + + def replay( + self, + event_id: str, + *, + webhook_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> LogReplayResponse: + """ + Re-send a webhook event to the configured URL. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not webhook_id: + raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}") + if not event_id: + raise ValueError(f"Expected a non-empty value for `event_id` but received {event_id!r}") + return self._post( + path_template("/webhooks/{webhook_id}/logs/{event_id}/replay", webhook_id=webhook_id, event_id=event_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=LogReplayResponse, + ) + + +class AsyncLogsResource(AsyncAPIResource): + """View and replay webhook deliveries""" + + @cached_property + def with_raw_response(self) -> AsyncLogsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncLogsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncLogsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return AsyncLogsResourceWithStreamingResponse(self) + + async def list( + self, + webhook_id: str, + *, + limit: int | Omit = omit, + max_status: int | Omit = omit, + min_status: int | Omit = omit, + offset: int | Omit = omit, + sort: Literal["asc", "desc"] | Omit = omit, + status: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> LogListResponse: + """ + List delivery logs for a specific webhook. + + Args: + limit: Maximum number of items to return (1-200) + + max_status: Maximum HTTP status code + + min_status: Minimum HTTP status code + + offset: Number of items to skip + + sort: Sort order by attempted time + + status: Filter by exact HTTP status code + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not webhook_id: + raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}") + return await self._get( + path_template("/webhooks/{webhook_id}/logs", webhook_id=webhook_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "limit": limit, + "max_status": max_status, + "min_status": min_status, + "offset": offset, + "sort": sort, + "status": status, + }, + log_list_params.LogListParams, + ), + ), + cast_to=LogListResponse, + ) + + async def replay( + self, + event_id: str, + *, + webhook_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> LogReplayResponse: + """ + Re-send a webhook event to the configured URL. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not webhook_id: + raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}") + if not event_id: + raise ValueError(f"Expected a non-empty value for `event_id` but received {event_id!r}") + return await self._post( + path_template("/webhooks/{webhook_id}/logs/{event_id}/replay", webhook_id=webhook_id, event_id=event_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=LogReplayResponse, + ) + + +class LogsResourceWithRawResponse: + def __init__(self, logs: LogsResource) -> None: + self._logs = logs + + self.list = to_raw_response_wrapper( + logs.list, + ) + self.replay = to_raw_response_wrapper( + logs.replay, + ) + + +class AsyncLogsResourceWithRawResponse: + def __init__(self, logs: AsyncLogsResource) -> None: + self._logs = logs + + self.list = async_to_raw_response_wrapper( + logs.list, + ) + self.replay = async_to_raw_response_wrapper( + logs.replay, + ) + + +class LogsResourceWithStreamingResponse: + def __init__(self, logs: LogsResource) -> None: + self._logs = logs + + self.list = to_streamed_response_wrapper( + logs.list, + ) + self.replay = to_streamed_response_wrapper( + logs.replay, + ) + + +class AsyncLogsResourceWithStreamingResponse: + def __init__(self, logs: AsyncLogsResource) -> None: + self._logs = logs + + self.list = async_to_streamed_response_wrapper( + logs.list, + ) + self.replay = async_to_streamed_response_wrapper( + logs.replay, + ) diff --git a/src/blooio/resources/contacts.py b/src/blooio/resources/webhooks/secret.py similarity index 52% rename from src/blooio/resources/contacts.py rename to src/blooio/resources/webhooks/secret.py index 741d1fe..caa7194 100644 --- a/src/blooio/resources/contacts.py +++ b/src/blooio/resources/webhooks/secret.py @@ -4,44 +4,47 @@ import httpx -from .._types import Body, Query, Headers, NotGiven, not_given -from .._compat import cached_property -from .._resource import SyncAPIResource, AsyncAPIResource -from .._response import ( +from ..._types import Body, Query, Headers, NotGiven, not_given +from ..._utils import path_template +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( to_raw_response_wrapper, to_streamed_response_wrapper, async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) -from .._base_client import make_request_options -from ..types.contact_check_capabilities_response import ContactCheckCapabilitiesResponse +from ..._base_client import make_request_options +from ...types.webhooks.secret_rotate_response import SecretRotateResponse -__all__ = ["ContactsResource", "AsyncContactsResource"] +__all__ = ["SecretResource", "AsyncSecretResource"] -class ContactsResource(SyncAPIResource): +class SecretResource(SyncAPIResource): + """Manage webhook subscriptions""" + @cached_property - def with_raw_response(self) -> ContactsResourceWithRawResponse: + def with_raw_response(self) -> SecretResourceWithRawResponse: """ This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers """ - return ContactsResourceWithRawResponse(self) + return SecretResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> ContactsResourceWithStreamingResponse: + def with_streaming_response(self) -> SecretResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response """ - return ContactsResourceWithStreamingResponse(self) + return SecretResourceWithStreamingResponse(self) - def check_capabilities( + def rotate( self, - contact: str, + webhook_id: str, *, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -49,10 +52,12 @@ def check_capabilities( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ContactCheckCapabilitiesResponse: - """ - Check if a phone number or email address supports iMessage, SMS, RCS, and other - messaging capabilities. + ) -> SecretRotateResponse: + """Generate a new signing secret for the webhook. + + The new secret is returned only + once in this response - store it securely. The old secret becomes invalid + immediately. Args: extra_headers: Send extra headers @@ -63,40 +68,42 @@ def check_capabilities( timeout: Override the client-level default timeout for this request, in seconds """ - if not contact: - raise ValueError(f"Expected a non-empty value for `contact` but received {contact!r}") - return self._get( - f"/v1/api/contacts/{contact}/capabilities", + if not webhook_id: + raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}") + return self._post( + path_template("/webhooks/{webhook_id}/secret/rotate", webhook_id=webhook_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ContactCheckCapabilitiesResponse, + cast_to=SecretRotateResponse, ) -class AsyncContactsResource(AsyncAPIResource): +class AsyncSecretResource(AsyncAPIResource): + """Manage webhook subscriptions""" + @cached_property - def with_raw_response(self) -> AsyncContactsResourceWithRawResponse: + def with_raw_response(self) -> AsyncSecretResourceWithRawResponse: """ This property can be used as a prefix for any HTTP method call to return the raw response object instead of the parsed content. For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers """ - return AsyncContactsResourceWithRawResponse(self) + return AsyncSecretResourceWithRawResponse(self) @cached_property - def with_streaming_response(self) -> AsyncContactsResourceWithStreamingResponse: + def with_streaming_response(self) -> AsyncSecretResourceWithStreamingResponse: """ An alternative to `.with_raw_response` that doesn't eagerly read the response body. For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response """ - return AsyncContactsResourceWithStreamingResponse(self) + return AsyncSecretResourceWithStreamingResponse(self) - async def check_capabilities( + async def rotate( self, - contact: str, + webhook_id: str, *, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -104,10 +111,12 @@ async def check_capabilities( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> ContactCheckCapabilitiesResponse: - """ - Check if a phone number or email address supports iMessage, SMS, RCS, and other - messaging capabilities. + ) -> SecretRotateResponse: + """Generate a new signing secret for the webhook. + + The new secret is returned only + once in this response - store it securely. The old secret becomes invalid + immediately. Args: extra_headers: Send extra headers @@ -118,48 +127,48 @@ async def check_capabilities( timeout: Override the client-level default timeout for this request, in seconds """ - if not contact: - raise ValueError(f"Expected a non-empty value for `contact` but received {contact!r}") - return await self._get( - f"/v1/api/contacts/{contact}/capabilities", + if not webhook_id: + raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}") + return await self._post( + path_template("/webhooks/{webhook_id}/secret/rotate", webhook_id=webhook_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=ContactCheckCapabilitiesResponse, + cast_to=SecretRotateResponse, ) -class ContactsResourceWithRawResponse: - def __init__(self, contacts: ContactsResource) -> None: - self._contacts = contacts +class SecretResourceWithRawResponse: + def __init__(self, secret: SecretResource) -> None: + self._secret = secret - self.check_capabilities = to_raw_response_wrapper( - contacts.check_capabilities, + self.rotate = to_raw_response_wrapper( + secret.rotate, ) -class AsyncContactsResourceWithRawResponse: - def __init__(self, contacts: AsyncContactsResource) -> None: - self._contacts = contacts +class AsyncSecretResourceWithRawResponse: + def __init__(self, secret: AsyncSecretResource) -> None: + self._secret = secret - self.check_capabilities = async_to_raw_response_wrapper( - contacts.check_capabilities, + self.rotate = async_to_raw_response_wrapper( + secret.rotate, ) -class ContactsResourceWithStreamingResponse: - def __init__(self, contacts: ContactsResource) -> None: - self._contacts = contacts +class SecretResourceWithStreamingResponse: + def __init__(self, secret: SecretResource) -> None: + self._secret = secret - self.check_capabilities = to_streamed_response_wrapper( - contacts.check_capabilities, + self.rotate = to_streamed_response_wrapper( + secret.rotate, ) -class AsyncContactsResourceWithStreamingResponse: - def __init__(self, contacts: AsyncContactsResource) -> None: - self._contacts = contacts +class AsyncSecretResourceWithStreamingResponse: + def __init__(self, secret: AsyncSecretResource) -> None: + self._secret = secret - self.check_capabilities = async_to_streamed_response_wrapper( - contacts.check_capabilities, + self.rotate = async_to_streamed_response_wrapper( + secret.rotate, ) diff --git a/src/blooio/resources/webhooks/webhooks.py b/src/blooio/resources/webhooks/webhooks.py new file mode 100644 index 0000000..471a3a5 --- /dev/null +++ b/src/blooio/resources/webhooks/webhooks.py @@ -0,0 +1,596 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from .logs import ( + LogsResource, + AsyncLogsResource, + LogsResourceWithRawResponse, + AsyncLogsResourceWithRawResponse, + LogsResourceWithStreamingResponse, + AsyncLogsResourceWithStreamingResponse, +) +from .secret import ( + SecretResource, + AsyncSecretResource, + SecretResourceWithRawResponse, + AsyncSecretResourceWithRawResponse, + SecretResourceWithStreamingResponse, + AsyncSecretResourceWithStreamingResponse, +) +from ...types import webhook_create_params, webhook_update_params +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._utils import path_template, maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.webhook import Webhook +from ...types.webhook_list_response import WebhookListResponse +from ...types.webhook_create_response import WebhookCreateResponse +from ...types.webhook_delete_response import WebhookDeleteResponse + +__all__ = ["WebhooksResource", "AsyncWebhooksResource"] + + +class WebhooksResource(SyncAPIResource): + """Manage webhook subscriptions""" + + @cached_property + def secret(self) -> SecretResource: + """Manage webhook subscriptions""" + return SecretResource(self._client) + + @cached_property + def logs(self) -> LogsResource: + """View and replay webhook deliveries""" + return LogsResource(self._client) + + @cached_property + def with_raw_response(self) -> WebhooksResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return WebhooksResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> WebhooksResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return WebhooksResourceWithStreamingResponse(self) + + def create( + self, + *, + webhook_url: str, + valid_until: int | Omit = omit, + webhook_type: Literal["message", "status", "all"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> WebhookCreateResponse: + """ + Create a new webhook subscription. + + Args: + webhook_url: URL to receive webhook events + + valid_until: Expiration timestamp (-1 for no expiration) + + webhook_type: Type of events to receive + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/webhooks", + body=maybe_transform( + { + "webhook_url": webhook_url, + "valid_until": valid_until, + "webhook_type": webhook_type, + }, + webhook_create_params.WebhookCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=WebhookCreateResponse, + ) + + def retrieve( + self, + webhook_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Webhook: + """ + Get details for a specific webhook. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not webhook_id: + raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}") + return self._get( + path_template("/webhooks/{webhook_id}", webhook_id=webhook_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Webhook, + ) + + def update( + self, + webhook_id: str, + *, + deprecate: bool | Omit = omit, + valid_until: int | Omit = omit, + webhook_type: Literal["message", "status", "all"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Webhook: + """ + Update a webhook's configuration. + + Args: + deprecate: Set to true to deprecate, false to undeprecate + + valid_until: Expiration timestamp. Use -1 or null for no expiration. + + webhook_type: Type of events to receive + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not webhook_id: + raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}") + return self._patch( + path_template("/webhooks/{webhook_id}", webhook_id=webhook_id), + body=maybe_transform( + { + "deprecate": deprecate, + "valid_until": valid_until, + "webhook_type": webhook_type, + }, + webhook_update_params.WebhookUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Webhook, + ) + + def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> WebhookListResponse: + """List all webhooks for the organization.""" + return self._get( + "/webhooks", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=WebhookListResponse, + ) + + def delete( + self, + webhook_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> WebhookDeleteResponse: + """ + Permanently delete a webhook. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not webhook_id: + raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}") + return self._delete( + path_template("/webhooks/{webhook_id}", webhook_id=webhook_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=WebhookDeleteResponse, + ) + + +class AsyncWebhooksResource(AsyncAPIResource): + """Manage webhook subscriptions""" + + @cached_property + def secret(self) -> AsyncSecretResource: + """Manage webhook subscriptions""" + return AsyncSecretResource(self._client) + + @cached_property + def logs(self) -> AsyncLogsResource: + """View and replay webhook deliveries""" + return AsyncLogsResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncWebhooksResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#accessing-raw-response-data-eg-headers + """ + return AsyncWebhooksResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncWebhooksResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Blooio/blooio-python-sdk#with_streaming_response + """ + return AsyncWebhooksResourceWithStreamingResponse(self) + + async def create( + self, + *, + webhook_url: str, + valid_until: int | Omit = omit, + webhook_type: Literal["message", "status", "all"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> WebhookCreateResponse: + """ + Create a new webhook subscription. + + Args: + webhook_url: URL to receive webhook events + + valid_until: Expiration timestamp (-1 for no expiration) + + webhook_type: Type of events to receive + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/webhooks", + body=await async_maybe_transform( + { + "webhook_url": webhook_url, + "valid_until": valid_until, + "webhook_type": webhook_type, + }, + webhook_create_params.WebhookCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=WebhookCreateResponse, + ) + + async def retrieve( + self, + webhook_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Webhook: + """ + Get details for a specific webhook. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not webhook_id: + raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}") + return await self._get( + path_template("/webhooks/{webhook_id}", webhook_id=webhook_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Webhook, + ) + + async def update( + self, + webhook_id: str, + *, + deprecate: bool | Omit = omit, + valid_until: int | Omit = omit, + webhook_type: Literal["message", "status", "all"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Webhook: + """ + Update a webhook's configuration. + + Args: + deprecate: Set to true to deprecate, false to undeprecate + + valid_until: Expiration timestamp. Use -1 or null for no expiration. + + webhook_type: Type of events to receive + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not webhook_id: + raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}") + return await self._patch( + path_template("/webhooks/{webhook_id}", webhook_id=webhook_id), + body=await async_maybe_transform( + { + "deprecate": deprecate, + "valid_until": valid_until, + "webhook_type": webhook_type, + }, + webhook_update_params.WebhookUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Webhook, + ) + + async def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> WebhookListResponse: + """List all webhooks for the organization.""" + return await self._get( + "/webhooks", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=WebhookListResponse, + ) + + async def delete( + self, + webhook_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> WebhookDeleteResponse: + """ + Permanently delete a webhook. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not webhook_id: + raise ValueError(f"Expected a non-empty value for `webhook_id` but received {webhook_id!r}") + return await self._delete( + path_template("/webhooks/{webhook_id}", webhook_id=webhook_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=WebhookDeleteResponse, + ) + + +class WebhooksResourceWithRawResponse: + def __init__(self, webhooks: WebhooksResource) -> None: + self._webhooks = webhooks + + self.create = to_raw_response_wrapper( + webhooks.create, + ) + self.retrieve = to_raw_response_wrapper( + webhooks.retrieve, + ) + self.update = to_raw_response_wrapper( + webhooks.update, + ) + self.list = to_raw_response_wrapper( + webhooks.list, + ) + self.delete = to_raw_response_wrapper( + webhooks.delete, + ) + + @cached_property + def secret(self) -> SecretResourceWithRawResponse: + """Manage webhook subscriptions""" + return SecretResourceWithRawResponse(self._webhooks.secret) + + @cached_property + def logs(self) -> LogsResourceWithRawResponse: + """View and replay webhook deliveries""" + return LogsResourceWithRawResponse(self._webhooks.logs) + + +class AsyncWebhooksResourceWithRawResponse: + def __init__(self, webhooks: AsyncWebhooksResource) -> None: + self._webhooks = webhooks + + self.create = async_to_raw_response_wrapper( + webhooks.create, + ) + self.retrieve = async_to_raw_response_wrapper( + webhooks.retrieve, + ) + self.update = async_to_raw_response_wrapper( + webhooks.update, + ) + self.list = async_to_raw_response_wrapper( + webhooks.list, + ) + self.delete = async_to_raw_response_wrapper( + webhooks.delete, + ) + + @cached_property + def secret(self) -> AsyncSecretResourceWithRawResponse: + """Manage webhook subscriptions""" + return AsyncSecretResourceWithRawResponse(self._webhooks.secret) + + @cached_property + def logs(self) -> AsyncLogsResourceWithRawResponse: + """View and replay webhook deliveries""" + return AsyncLogsResourceWithRawResponse(self._webhooks.logs) + + +class WebhooksResourceWithStreamingResponse: + def __init__(self, webhooks: WebhooksResource) -> None: + self._webhooks = webhooks + + self.create = to_streamed_response_wrapper( + webhooks.create, + ) + self.retrieve = to_streamed_response_wrapper( + webhooks.retrieve, + ) + self.update = to_streamed_response_wrapper( + webhooks.update, + ) + self.list = to_streamed_response_wrapper( + webhooks.list, + ) + self.delete = to_streamed_response_wrapper( + webhooks.delete, + ) + + @cached_property + def secret(self) -> SecretResourceWithStreamingResponse: + """Manage webhook subscriptions""" + return SecretResourceWithStreamingResponse(self._webhooks.secret) + + @cached_property + def logs(self) -> LogsResourceWithStreamingResponse: + """View and replay webhook deliveries""" + return LogsResourceWithStreamingResponse(self._webhooks.logs) + + +class AsyncWebhooksResourceWithStreamingResponse: + def __init__(self, webhooks: AsyncWebhooksResource) -> None: + self._webhooks = webhooks + + self.create = async_to_streamed_response_wrapper( + webhooks.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + webhooks.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + webhooks.update, + ) + self.list = async_to_streamed_response_wrapper( + webhooks.list, + ) + self.delete = async_to_streamed_response_wrapper( + webhooks.delete, + ) + + @cached_property + def secret(self) -> AsyncSecretResourceWithStreamingResponse: + """Manage webhook subscriptions""" + return AsyncSecretResourceWithStreamingResponse(self._webhooks.secret) + + @cached_property + def logs(self) -> AsyncLogsResourceWithStreamingResponse: + """View and replay webhook deliveries""" + return AsyncLogsResourceWithStreamingResponse(self._webhooks.logs) diff --git a/src/blooio/types/__init__.py b/src/blooio/types/__init__.py index a0463d1..dad7cd9 100644 --- a/src/blooio/types/__init__.py +++ b/src/blooio/types/__init__.py @@ -2,10 +2,36 @@ from __future__ import annotations -from .message_send_params import MessageSendParams as MessageSendParams +from .group import Group as Group +from .contact import Contact as Contact +from .webhook import Webhook as Webhook +from .pagination import Pagination as Pagination +from .last_message import LastMessage as LastMessage +from .delete_response import DeleteResponse as DeleteResponse +from .chat_list_params import ChatListParams as ChatListParams +from .group_list_params import GroupListParams as GroupListParams +from .chat_list_response import ChatListResponse as ChatListResponse +from .contact_list_params import ContactListParams as ContactListParams +from .group_create_params import GroupCreateParams as GroupCreateParams +from .group_list_response import GroupListResponse as GroupListResponse +from .group_update_params import GroupUpdateParams as GroupUpdateParams from .me_retrieve_response import MeRetrieveResponse as MeRetrieveResponse -from .message_send_response import MessageSendResponse as MessageSendResponse -from .message_cancel_response import MessageCancelResponse as MessageCancelResponse -from .message_retrieve_response import MessageRetrieveResponse as MessageRetrieveResponse -from .message_get_status_response import MessageGetStatusResponse as MessageGetStatusResponse +from .contact_create_params import ContactCreateParams as ContactCreateParams +from .contact_list_response import ContactListResponse as ContactListResponse +from .contact_update_params import ContactUpdateParams as ContactUpdateParams +from .group_create_response import GroupCreateResponse as GroupCreateResponse +from .group_delete_response import GroupDeleteResponse as GroupDeleteResponse +from .group_update_response import GroupUpdateResponse as GroupUpdateResponse +from .webhook_create_params import WebhookCreateParams as WebhookCreateParams +from .webhook_list_response import WebhookListResponse as WebhookListResponse +from .webhook_update_params import WebhookUpdateParams as WebhookUpdateParams +from .chat_retrieve_response import ChatRetrieveResponse as ChatRetrieveResponse +from .webhook_create_response import WebhookCreateResponse as WebhookCreateResponse +from .webhook_delete_response import WebhookDeleteResponse as WebhookDeleteResponse +from .chat_mark_as_read_response import ChatMarkAsReadResponse as ChatMarkAsReadResponse +from .facetime_initiate_call_params import FacetimeInitiateCallParams as FacetimeInitiateCallParams +from .facetime_initiate_call_response import FacetimeInitiateCallResponse as FacetimeInitiateCallResponse +from .chat_share_contact_card_response import ChatShareContactCardResponse as ChatShareContactCardResponse +from .phone_number_batch_create_params import PhoneNumberBatchCreateParams as PhoneNumberBatchCreateParams +from .phone_number_batch_create_response import PhoneNumberBatchCreateResponse as PhoneNumberBatchCreateResponse from .contact_check_capabilities_response import ContactCheckCapabilitiesResponse as ContactCheckCapabilitiesResponse diff --git a/src/blooio/types/chat_list_params.py b/src/blooio/types/chat_list_params.py new file mode 100644 index 0000000..3a6dbab --- /dev/null +++ b/src/blooio/types/chat_list_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["ChatListParams"] + + +class ChatListParams(TypedDict, total=False): + limit: int + """Maximum number of items to return (1-200)""" + + offset: int + """Number of items to skip""" + + q: str + """Search query (matches phone/email or contact name)""" + + sort: Literal["recent", "oldest"] + """Sort order""" diff --git a/src/blooio/types/chat_list_response.py b/src/blooio/types/chat_list_response.py new file mode 100644 index 0000000..6ec4a82 --- /dev/null +++ b/src/blooio/types/chat_list_response.py @@ -0,0 +1,62 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from .._models import BaseModel +from .pagination import Pagination +from .last_message import LastMessage + +__all__ = ["ChatListResponse", "Chat", "ChatContact"] + + +class ChatContact(BaseModel): + """Contact info (only for non-group chats)""" + + contact_id: Optional[str] = None + + identifier: Optional[str] = None + + name: Optional[str] = None + + +class Chat(BaseModel): + id: Optional[str] = None + """Chat identifier (phone number, email, or group ID)""" + + contact: Optional[ChatContact] = None + """Contact info (only for non-group chats)""" + + group_id: Optional[str] = None + """Group ID (only for group chats)""" + + group_name: Optional[str] = None + """Group name (only for group chats)""" + + inbound_count: Optional[int] = None + + is_group: Optional[bool] = None + """Whether this is a group chat""" + + last_inbound_time: Optional[int] = None + + last_message: Optional[LastMessage] = None + + last_message_time: Optional[int] = None + + last_outbound_time: Optional[int] = None + + member_count: Optional[int] = None + """Number of members (only for group chats)""" + + message_count: Optional[int] = None + + outbound_count: Optional[int] = None + + type: Optional[Literal["phone", "email", "group"]] = None + + +class ChatListResponse(BaseModel): + chats: Optional[List[Chat]] = None + + pagination: Optional[Pagination] = None diff --git a/src/blooio/types/chat_mark_as_read_response.py b/src/blooio/types/chat_mark_as_read_response.py new file mode 100644 index 0000000..595077a --- /dev/null +++ b/src/blooio/types/chat_mark_as_read_response.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["ChatMarkAsReadResponse"] + + +class ChatMarkAsReadResponse(BaseModel): + chat_id: Optional[str] = None + """Chat identifier""" + + marked_at: Optional[int] = None + """Timestamp when marked as read""" + + status: Optional[Literal["read"]] = None + """Read status""" diff --git a/src/blooio/types/chat_retrieve_response.py b/src/blooio/types/chat_retrieve_response.py new file mode 100644 index 0000000..14f98a5 --- /dev/null +++ b/src/blooio/types/chat_retrieve_response.py @@ -0,0 +1,57 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel +from .last_message import LastMessage + +__all__ = ["ChatRetrieveResponse", "Contact"] + + +class Contact(BaseModel): + """Contact info (only for non-group chats)""" + + contact_id: Optional[str] = None + + identifier: Optional[str] = None + + name: Optional[str] = None + + +class ChatRetrieveResponse(BaseModel): + id: Optional[str] = None + """Chat identifier (phone number, email, or group ID)""" + + contact: Optional[Contact] = None + """Contact info (only for non-group chats)""" + + first_message_time: Optional[int] = None + + group_id: Optional[str] = None + """Group ID (only for group chats)""" + + group_name: Optional[str] = None + """Group name (only for group chats)""" + + inbound_count: Optional[int] = None + + is_group: Optional[bool] = None + """Whether this is a group chat""" + + last_inbound_time: Optional[int] = None + + last_message: Optional[LastMessage] = None + + last_message_time: Optional[int] = None + + last_outbound_time: Optional[int] = None + + member_count: Optional[int] = None + """Number of members (only for group chats)""" + + message_count: Optional[int] = None + + outbound_count: Optional[int] = None + + type: Optional[Literal["phone", "email", "group"]] = None diff --git a/src/blooio/types/chat_share_contact_card_response.py b/src/blooio/types/chat_share_contact_card_response.py new file mode 100644 index 0000000..f1c109d --- /dev/null +++ b/src/blooio/types/chat_share_contact_card_response.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["ChatShareContactCardResponse"] + + +class ChatShareContactCardResponse(BaseModel): + chat_id: Optional[str] = None + """Normalized chat identifier""" + + message: Optional[str] = None + + success: Optional[bool] = None diff --git a/src/blooio/types/chats/__init__.py b/src/blooio/types/chats/__init__.py new file mode 100644 index 0000000..fcb9a74 --- /dev/null +++ b/src/blooio/types/chats/__init__.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .reaction import Reaction as Reaction +from .typing_response import TypingResponse as TypingResponse +from .poll_send_params import PollSendParams as PollSendParams +from .link_preview_param import LinkPreviewParam as LinkPreviewParam +from .poll_send_response import PollSendResponse as PollSendResponse +from .message_list_params import MessageListParams as MessageListParams +from .message_send_params import MessageSendParams as MessageSendParams +from .message_react_params import MessageReactParams as MessageReactParams +from .background_set_params import BackgroundSetParams as BackgroundSetParams +from .message_list_response import MessageListResponse as MessageListResponse +from .message_send_response import MessageSendResponse as MessageSendResponse +from .message_react_response import MessageReactResponse as MessageReactResponse +from .chat_background_response import ChatBackgroundResponse as ChatBackgroundResponse +from .message_retrieve_response import MessageRetrieveResponse as MessageRetrieveResponse +from .poll_get_results_response import PollGetResultsResponse as PollGetResultsResponse +from .message_get_status_response import MessageGetStatusResponse as MessageGetStatusResponse diff --git a/src/blooio/types/chats/background_set_params.py b/src/blooio/types/chats/background_set_params.py new file mode 100644 index 0000000..315fc10 --- /dev/null +++ b/src/blooio/types/chats/background_set_params.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from ..._types import FileTypes + +__all__ = ["BackgroundSetParams"] + + +class BackgroundSetParams(TypedDict, total=False): + background: Required[FileTypes] + """The image file to set as the chat background""" diff --git a/src/blooio/types/chats/chat_background_response.py b/src/blooio/types/chats/chat_background_response.py new file mode 100644 index 0000000..48c1348 --- /dev/null +++ b/src/blooio/types/chats/chat_background_response.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["ChatBackgroundResponse"] + + +class ChatBackgroundResponse(BaseModel): + """Response for chat background operations""" + + background_id: Optional[str] = None + """Unique identifier for the current background, or null if none""" + + background_version: Optional[int] = None + """Version number of the background (for cache invalidation)""" + + changed: Optional[bool] = None + """Whether the background was changed by this operation (only present on PUT)""" + + chat_id: Optional[str] = None + """Normalized chat identifier (phone number, email, or group ID)""" + + has_background: Optional[bool] = None + """Whether the chat currently has a background set""" diff --git a/src/blooio/types/chats/link_preview_param.py b/src/blooio/types/chats/link_preview_param.py new file mode 100644 index 0000000..8e80daf --- /dev/null +++ b/src/blooio/types/chats/link_preview_param.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["LinkPreviewParam"] + + +class LinkPreviewParam(TypedDict, total=False): + """Rich-link-preview overrides for URL messages (iMessage URL balloon). + + All fields are optional. Only applies when the message text (or the concatenated part text) is exactly a single http(s) URL. If omitted but the text is a URL, Blooio auto-fetches the page's Open Graph metadata to generate a preview. If the image download fails, the send still succeeds — Blooio silently falls back to the auto-generated preview. + """ + + image_url: str + """HTTPS URL to an image (png, jpg, webp, gif). + + Blooio downloads the image server-side and attaches it as the rich-link hero. + Max 16 MB. If the download fails or returns a non-image MIME, the send falls + back to auto-fetched OG metadata. + """ + + title: str + """Bold title line rendered in the iMessage bubble. + + Overrides the page's ``. + """ diff --git a/src/blooio/types/message_get_status_response.py b/src/blooio/types/chats/message_get_status_response.py similarity index 50% rename from src/blooio/types/message_get_status_response.py rename to src/blooio/types/chats/message_get_status_response.py index 9da685b..da637b4 100644 --- a/src/blooio/types/message_get_status_response.py +++ b/src/blooio/types/chats/message_get_status_response.py @@ -3,14 +3,26 @@ from typing import Optional from typing_extensions import Literal -from .._models import BaseModel +from ..._models import BaseModel __all__ = ["MessageGetStatusResponse"] class MessageGetStatusResponse(BaseModel): + chat_id: Optional[str] = None + + direction: Optional[Literal["inbound", "outbound"]] = None + + error: Optional[str] = None + message_id: Optional[str] = None + protocol: Optional[Literal["imessage", "sms", "rcs", "non-imessage"]] = None + status: Optional[ - Literal["pending", "queued", "sent", "delivered", "failed", "cancelled", "cancellation_requested"] + Literal["pending", "queued", "sent", "delivered", "failed", "cancellation_requested", "cancelled"] ] = None + + time_delivered: Optional[int] = None + + time_sent: Optional[int] = None diff --git a/src/blooio/types/chats/message_list_params.py b/src/blooio/types/chats/message_list_params.py new file mode 100644 index 0000000..0e35f39 --- /dev/null +++ b/src/blooio/types/chats/message_list_params.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["MessageListParams"] + + +class MessageListParams(TypedDict, total=False): + direction: Literal["inbound", "outbound"] + """Filter by message direction""" + + limit: int + """Maximum number of items to return (1-200)""" + + offset: int + """Number of items to skip""" + + since: int + """Only messages sent after this timestamp (ms)""" + + sort: Literal["asc", "desc"] + """Sort order by time""" + + until: int + """Only messages sent before this timestamp (ms)""" diff --git a/src/blooio/types/chats/message_list_response.py b/src/blooio/types/chats/message_list_response.py new file mode 100644 index 0000000..88001a7 --- /dev/null +++ b/src/blooio/types/chats/message_list_response.py @@ -0,0 +1,55 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from .reaction import Reaction +from ..._models import BaseModel +from ..pagination import Pagination + +__all__ = ["MessageListResponse", "Message"] + + +class Message(BaseModel): + attachments: Optional[List[object]] = None + + direction: Optional[Literal["inbound", "outbound"]] = None + + error: Optional[str] = None + + external_id: Optional[str] = None + """Phone number or email of the contact, or group ID for group messages""" + + internal_id: Optional[str] = None + """Organization phone number (from-number) used for this message""" + + message_id: Optional[str] = None + + protocol: Optional[Literal["imessage", "sms", "rcs", "non-imessage"]] = None + + reactions: Optional[List[Reaction]] = None + """Reactions on this message (tapbacks and emoji reactions)""" + + sender: Optional[str] = None + """Sender's phone number or email for inbound group messages. + + Null for outbound messages and 1-1 chats. + """ + + status: Optional[ + Literal["pending", "queued", "sent", "delivered", "failed", "cancellation_requested", "cancelled"] + ] = None + + text: Optional[str] = None + + time_delivered: Optional[int] = None + + time_sent: Optional[int] = None + + +class MessageListResponse(BaseModel): + chat_id: Optional[str] = None + + messages: Optional[List[Message]] = None + + pagination: Optional[Pagination] = None diff --git a/src/blooio/types/chats/message_react_params.py b/src/blooio/types/chats/message_react_params.py new file mode 100644 index 0000000..9c6a6ed --- /dev/null +++ b/src/blooio/types/chats/message_react_params.py @@ -0,0 +1,32 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, Annotated, TypedDict + +from ..._utils import PropertyInfo + +__all__ = ["MessageReactParams"] + + +class MessageReactParams(TypedDict, total=False): + chat_id: Required[Annotated[str, PropertyInfo(alias="chatId")]] + + reaction: Required[str] + """The reaction to add or remove. + + Must be prefixed with `+` to add or `-` to remove. + + **Classic tapbacks:** `+love`, `-love`, `+like`, `-like`, `+dislike`, + `-dislike`, `+laugh`, `-laugh`, `+emphasize`, `-emphasize`, `+question`, + `-question` + + **Emoji reactions:** Any emoji prefixed with `+` or `-` (e.g. `+😂`, `-😂`, + `+👍`, `-🔥`). Emoji reactions require macOS 14 (Sonoma) or later on the device. + """ + + direction: Literal["inbound", "outbound"] + """ + Filter by message direction (only used when messageId is a relative index like + -1, -2) + """ diff --git a/src/blooio/types/chats/message_react_response.py b/src/blooio/types/chats/message_react_response.py new file mode 100644 index 0000000..06dcd4c --- /dev/null +++ b/src/blooio/types/chats/message_react_response.py @@ -0,0 +1,26 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["MessageReactResponse"] + + +class MessageReactResponse(BaseModel): + action: Optional[Literal["add", "remove"]] = None + """The action that was performed""" + + message_id: Optional[str] = None + """The ID of the message that was reacted to""" + + reaction: Optional[str] = None + """The reaction that was added or removed. + + For classic tapbacks: love, like, dislike, laugh, emphasize, question. For emoji + reactions: the emoji character (e.g. 😂, 👍, 🔥). + """ + + success: Optional[bool] = None + """Whether the reaction was sent successfully""" diff --git a/src/blooio/types/chats/message_retrieve_response.py b/src/blooio/types/chats/message_retrieve_response.py new file mode 100644 index 0000000..a2c61ad --- /dev/null +++ b/src/blooio/types/chats/message_retrieve_response.py @@ -0,0 +1,56 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from .reaction import Reaction +from ..._models import BaseModel + +__all__ = ["MessageRetrieveResponse", "Contact"] + + +class Contact(BaseModel): + contact_id: Optional[str] = None + + identifier: Optional[str] = None + """The contact's phone number or email""" + + name: Optional[str] = None + + +class MessageRetrieveResponse(BaseModel): + attachments: Optional[List[object]] = None + + chat_id: Optional[str] = None + + contact: Optional[Contact] = None + + direction: Optional[Literal["inbound", "outbound"]] = None + + error: Optional[str] = None + + internal_id: Optional[str] = None + """Organization phone number (from-number) used for this message""" + + message_id: Optional[str] = None + + protocol: Optional[Literal["imessage", "sms", "rcs", "non-imessage"]] = None + + reactions: Optional[List[Reaction]] = None + """Reactions on this message (tapbacks and emoji reactions)""" + + sender: Optional[str] = None + """Sender's phone number or email for inbound group messages. + + Null for outbound messages and 1-1 chats. + """ + + status: Optional[ + Literal["pending", "queued", "sent", "delivered", "failed", "cancellation_requested", "cancelled"] + ] = None + + text: Optional[str] = None + + time_delivered: Optional[int] = None + + time_sent: Optional[int] = None diff --git a/src/blooio/types/chats/message_send_params.py b/src/blooio/types/chats/message_send_params.py new file mode 100644 index 0000000..7268fce --- /dev/null +++ b/src/blooio/types/chats/message_send_params.py @@ -0,0 +1,100 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Iterable, Optional +from typing_extensions import Required, Annotated, TypeAlias, TypedDict + +from ..._types import SequenceNotStr +from ..._utils import PropertyInfo +from .link_preview_param import LinkPreviewParam + +__all__ = ["MessageSendParams", "Attachment", "AttachmentUnionMember1", "Part"] + + +class MessageSendParams(TypedDict, total=False): + attachments: SequenceNotStr[Attachment] + """Array of attachment URLs or objects with url/name""" + + from_number: str + """E.164 phone number to send from. + + For Twilio API keys, this is optional — if omitted, the first assigned Twilio + number is auto-selected. For Blooio (iMessage) API keys, this selects a specific + number from your pool. Must be a number assigned to your API key. + """ + + link_preview: Optional[LinkPreviewParam] + """Rich-link-preview overrides for URL messages (iMessage URL balloon). + + All fields are optional. Only applies when the message text (or the concatenated + part text) is exactly a single http(s) URL. If omitted but the text is a URL, + Blooio auto-fetches the page's Open Graph metadata to generate a preview. If the + image download fails, the send still succeeds — Blooio silently falls back to + the auto-generated preview. + """ + + parts: Iterable[Part] + """Ordered array of message parts. Two modes: + + 1. **Multipart mode** — parts sent as a single unified iMessage bubble (mix of + text and attachment parts). This is the default. + 2. **URL-balloon batch mode** — triggered when any part has a `link_preview` + object. Each part becomes its own rich-link-preview iMessage; parts are sent + sequentially in array order. In batch mode every part must be text-only with + `text` being a single http(s) URL. Response contains `message_ids[]` + + `count` instead of `message_id`. + """ + + share_contact: bool + """If true, the contact card (Name & Photo) will be shared with this message. + + The contact card is piggybacked onto the outgoing message. Defaults to false. + """ + + text: Union[str, SequenceNotStr[str]] + """Message text. + + Can be a single string or array of strings (each becomes a separate message) + """ + + use_typing_indicator: bool + """Whether to show typing indicator before sending. Defaults to org preference.""" + + idempotency_key: Annotated[str, PropertyInfo(alias="Idempotency-Key")] + + +class AttachmentUnionMember1(TypedDict, total=False): + url: Required[str] + + name: str + + +Attachment: TypeAlias = Union[str, AttachmentUnionMember1] + + +class Part(TypedDict, total=False): + link_preview: Optional[LinkPreviewParam] + """Rich-link-preview overrides for URL messages (iMessage URL balloon). + + All fields are optional. Only applies when the message text (or the concatenated + part text) is exactly a single http(s) URL. If omitted but the text is a URL, + Blooio auto-fetches the page's Open Graph metadata to generate a preview. If the + image download fails, the send still succeeds — Blooio silently falls back to + the auto-generated preview. + """ + + mention: str + """Participant phone number or email to @-mention. + + Only valid with 'text'. The entire text of the part is rendered as the mention. + """ + + name: str + """Filename for the attachment. Only valid with 'url'.""" + + text: str + """Text content for this part. Mutually exclusive with 'url'.""" + + url: str + """URL to an attachment for this part. Mutually exclusive with 'text'.""" diff --git a/src/blooio/types/chats/message_send_response.py b/src/blooio/types/chats/message_send_response.py new file mode 100644 index 0000000..385d693 --- /dev/null +++ b/src/blooio/types/chats/message_send_response.py @@ -0,0 +1,37 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["MessageSendResponse"] + + +class MessageSendResponse(BaseModel): + """Response after sending a message""" + + count: Optional[int] = None + """Number of messages sent. Only present in URL-balloon batch mode.""" + + group_created: Optional[bool] = None + """True if a new unnamed group was created for this multi-recipient message""" + + group_id: Optional[str] = None + """Group ID when sending to multi-recipient (new or existing)""" + + message_id: Optional[str] = None + """ID of the sent message (single-message sends)""" + + message_ids: Optional[List[str]] = None + """IDs of sent messages. + + Present when `text` is an array or when `parts` uses per-part `link_preview` + (URL-balloon batch mode). + """ + + participants: Optional[List[str]] = None + """List of participants (present for multi-recipient)""" + + status: Optional[Literal["queued", "failed"]] = None + """Initial status of the message(s)""" diff --git a/src/blooio/types/chats/poll_get_results_response.py b/src/blooio/types/chats/poll_get_results_response.py new file mode 100644 index 0000000..3d3c52e --- /dev/null +++ b/src/blooio/types/chats/poll_get_results_response.py @@ -0,0 +1,25 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel + +__all__ = ["PollGetResultsResponse", "Option"] + + +class Option(BaseModel): + text: Optional[str] = None + + votes: Optional[int] = None + + +class PollGetResultsResponse(BaseModel): + chat_id: Optional[str] = None + + options: Optional[List[Option]] = None + + poll_id: Optional[str] = None + + title: Optional[str] = None + + total_votes: Optional[int] = None diff --git a/src/blooio/types/chats/poll_send_params.py b/src/blooio/types/chats/poll_send_params.py new file mode 100644 index 0000000..6d468f8 --- /dev/null +++ b/src/blooio/types/chats/poll_send_params.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from ..._types import SequenceNotStr + +__all__ = ["PollSendParams"] + + +class PollSendParams(TypedDict, total=False): + options: Required[SequenceNotStr[str]] + """Array of 2-10 option strings for the poll""" + + title: str + """Poll question or title (optional)""" diff --git a/src/blooio/types/chats/poll_send_response.py b/src/blooio/types/chats/poll_send_response.py new file mode 100644 index 0000000..1a48141 --- /dev/null +++ b/src/blooio/types/chats/poll_send_response.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel + +__all__ = ["PollSendResponse", "Poll"] + + +class Poll(BaseModel): + options: Optional[List[str]] = None + + title: Optional[str] = None + + +class PollSendResponse(BaseModel): + chat_id: Optional[str] = None + + poll: Optional[Poll] = None + + poll_id: Optional[str] = None + """Unique identifier for the poll""" + + sent_at: Optional[float] = None diff --git a/src/blooio/types/chats/reaction.py b/src/blooio/types/chats/reaction.py new file mode 100644 index 0000000..530f398 --- /dev/null +++ b/src/blooio/types/chats/reaction.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["Reaction"] + + +class Reaction(BaseModel): + is_added: Optional[bool] = None + """Whether the reaction is currently active (true) or was removed (false)""" + + reaction: Optional[str] = None + """The reaction value. + + Classic tapbacks: love, like, dislike, laugh, emphasize, question. Emoji + reactions: the emoji character (e.g. 😂, 👍). + """ + + sender: Optional[str] = None + """Phone number or email of who sent the reaction. + + Null when the reaction was sent by you (outbound). + """ + + time_sent: Optional[int] = None + """Timestamp when the reaction was sent (ms)""" diff --git a/src/blooio/types/chats/typing_response.py b/src/blooio/types/chats/typing_response.py new file mode 100644 index 0000000..3350bee --- /dev/null +++ b/src/blooio/types/chats/typing_response.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["TypingResponse"] + + +class TypingResponse(BaseModel): + chat_id: Optional[str] = None + """Chat identifier""" + + started_at: Optional[int] = None + """Timestamp when typing started (only for start)""" + + stopped_at: Optional[int] = None + """Timestamp when typing stopped (only for stop)""" + + typing: Optional[bool] = None + """Whether typing indicator is active""" diff --git a/src/blooio/types/config/__init__.py b/src/blooio/types/config/__init__.py deleted file mode 100644 index dd88238..0000000 --- a/src/blooio/types/config/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from .webhook_update_params import WebhookUpdateParams as WebhookUpdateParams -from .webhook_update_response import WebhookUpdateResponse as WebhookUpdateResponse -from .webhook_retrieve_response import WebhookRetrieveResponse as WebhookRetrieveResponse diff --git a/src/blooio/types/config/webhook_retrieve_response.py b/src/blooio/types/config/webhook_retrieve_response.py deleted file mode 100644 index 77e692c..0000000 --- a/src/blooio/types/config/webhook_retrieve_response.py +++ /dev/null @@ -1,15 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from ..._models import BaseModel - -__all__ = ["WebhookRetrieveResponse"] - - -class WebhookRetrieveResponse(BaseModel): - updated_at: Optional[int] = None - """Unix timestamp (ms) when the webhook URL was last updated.""" - - webhook_url: Optional[str] = None - """The current webhook URL or null if not set.""" diff --git a/src/blooio/types/contact.py b/src/blooio/types/contact.py new file mode 100644 index 0000000..0e5c5ea --- /dev/null +++ b/src/blooio/types/contact.py @@ -0,0 +1,29 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["Contact"] + + +class Contact(BaseModel): + id: Optional[str] = None + """Contact identifier (phone or email)""" + + contact_id: Optional[str] = None + """Internal contact ID""" + + created_at: Optional[int] = None + + identifier: Optional[str] = None + """Phone number (E.164) or email""" + + last_message_time: Optional[int] = None + + name: Optional[str] = None + + tags: Optional[List[str]] = None + + type: Optional[Literal["phone", "email"]] = None diff --git a/src/blooio/types/contact_check_capabilities_response.py b/src/blooio/types/contact_check_capabilities_response.py index 3a68ed5..ffd24f9 100644 --- a/src/blooio/types/contact_check_capabilities_response.py +++ b/src/blooio/types/contact_check_capabilities_response.py @@ -1,38 +1,31 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Optional -from datetime import datetime from typing_extensions import Literal -from pydantic import Field as FieldInfo - from .._models import BaseModel __all__ = ["ContactCheckCapabilitiesResponse", "Capabilities"] class Capabilities(BaseModel): - """Messaging capabilities for this contact.""" + facetime: Optional[bool] = None + """Whether FaceTime is available""" imessage: Optional[bool] = None - """Whether this contact supports iMessage.""" + """Whether iMessage is available""" sms: Optional[bool] = None - """ - Whether this contact supports SMS (always true for phone numbers, false for - emails). - """ + """Whether SMS is available (phone only)""" class ContactCheckCapabilitiesResponse(BaseModel): capabilities: Optional[Capabilities] = None - """Messaging capabilities for this contact.""" contact: Optional[str] = None - """The contact identifier (phone number or email).""" + """Normalized contact identifier""" - last_checked: Optional[datetime] = FieldInfo(alias="lastChecked", default=None) - """ISO 8601 timestamp of when capabilities were last checked.""" + last_checked: Optional[int] = None + """Timestamp when capabilities were checked""" type: Optional[Literal["phone", "email"]] = None - """Type of contact identifier.""" diff --git a/src/blooio/types/contact_create_params.py b/src/blooio/types/contact_create_params.py new file mode 100644 index 0000000..3cf5bee --- /dev/null +++ b/src/blooio/types/contact_create_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["ContactCreateParams"] + + +class ContactCreateParams(TypedDict, total=False): + identifier: Required[str] + """Phone number (E.164 format, e.g., +15551234567) or email address""" + + name: str + """Display name for the contact""" diff --git a/src/blooio/types/contact_list_params.py b/src/blooio/types/contact_list_params.py new file mode 100644 index 0000000..6c109ff --- /dev/null +++ b/src/blooio/types/contact_list_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["ContactListParams"] + + +class ContactListParams(TypedDict, total=False): + limit: int + """Maximum number of items to return (1-200)""" + + offset: int + """Number of items to skip""" + + q: str + """Search query (matches identifier or name)""" + + sort: Literal["recent", "oldest", "name_asc", "name_desc"] + """Sort order""" diff --git a/src/blooio/types/contact_list_response.py b/src/blooio/types/contact_list_response.py new file mode 100644 index 0000000..fdd20e4 --- /dev/null +++ b/src/blooio/types/contact_list_response.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .contact import Contact +from .._models import BaseModel +from .pagination import Pagination + +__all__ = ["ContactListResponse"] + + +class ContactListResponse(BaseModel): + contacts: Optional[List[Contact]] = None + + pagination: Optional[Pagination] = None diff --git a/src/blooio/types/contact_update_params.py b/src/blooio/types/contact_update_params.py new file mode 100644 index 0000000..c01667d --- /dev/null +++ b/src/blooio/types/contact_update_params.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import TypedDict + +__all__ = ["ContactUpdateParams"] + + +class ContactUpdateParams(TypedDict, total=False): + name: Optional[str] + """New display name (null to clear)""" diff --git a/src/blooio/types/contacts/__init__.py b/src/blooio/types/contacts/__init__.py new file mode 100644 index 0000000..0724771 --- /dev/null +++ b/src/blooio/types/contacts/__init__.py @@ -0,0 +1,7 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .tag_add_params import TagAddParams as TagAddParams +from .tag_add_response import TagAddResponse as TagAddResponse +from .tag_list_response import TagListResponse as TagListResponse diff --git a/src/blooio/types/contacts/tag_add_params.py b/src/blooio/types/contacts/tag_add_params.py new file mode 100644 index 0000000..b2210d9 --- /dev/null +++ b/src/blooio/types/contacts/tag_add_params.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from ..._types import SequenceNotStr + +__all__ = ["TagAddParams"] + + +class TagAddParams(TypedDict, total=False): + tags: Required[SequenceNotStr[str]] + """Tags to add""" diff --git a/src/blooio/types/contacts/tag_add_response.py b/src/blooio/types/contacts/tag_add_response.py new file mode 100644 index 0000000..90423f7 --- /dev/null +++ b/src/blooio/types/contacts/tag_add_response.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel + +__all__ = ["TagAddResponse"] + + +class TagAddResponse(BaseModel): + success: Optional[bool] = None + + tags_added: Optional[List[str]] = None + """Tags that were added""" diff --git a/src/blooio/types/contacts/tag_list_response.py b/src/blooio/types/contacts/tag_list_response.py new file mode 100644 index 0000000..3461936 --- /dev/null +++ b/src/blooio/types/contacts/tag_list_response.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel + +__all__ = ["TagListResponse", "Tag"] + + +class Tag(BaseModel): + created_at: Optional[int] = None + """Timestamp when the tag was added (ms since epoch)""" + + tag: Optional[str] = None + """The tag value""" + + +class TagListResponse(BaseModel): + tags: Optional[List[Tag]] = None diff --git a/src/blooio/types/message_send_response.py b/src/blooio/types/delete_response.py similarity index 51% rename from src/blooio/types/message_send_response.py rename to src/blooio/types/delete_response.py index 47b99d4..b6e0312 100644 --- a/src/blooio/types/message_send_response.py +++ b/src/blooio/types/delete_response.py @@ -4,10 +4,10 @@ from .._models import BaseModel -__all__ = ["MessageSendResponse"] +__all__ = ["DeleteResponse"] -class MessageSendResponse(BaseModel): - message_id: Optional[str] = None +class DeleteResponse(BaseModel): + deleted_at: Optional[int] = None - status: Optional[str] = None + success: Optional[bool] = None diff --git a/src/blooio/types/facetime_initiate_call_params.py b/src/blooio/types/facetime_initiate_call_params.py new file mode 100644 index 0000000..aa4b526 --- /dev/null +++ b/src/blooio/types/facetime_initiate_call_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["FacetimeInitiateCallParams"] + + +class FacetimeInitiateCallParams(TypedDict, total=False): + handle: Required[str] + """Phone number (E.164) or email address to call""" diff --git a/src/blooio/types/facetime_initiate_call_response.py b/src/blooio/types/facetime_initiate_call_response.py new file mode 100644 index 0000000..7b1d8f8 --- /dev/null +++ b/src/blooio/types/facetime_initiate_call_response.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["FacetimeInitiateCallResponse"] + + +class FacetimeInitiateCallResponse(BaseModel): + handle: Optional[str] = None + """The handle that was called""" + + link: Optional[str] = None + """Shareable FaceTime link""" + + success: Optional[bool] = None diff --git a/src/blooio/types/group.py b/src/blooio/types/group.py new file mode 100644 index 0000000..e43ba83 --- /dev/null +++ b/src/blooio/types/group.py @@ -0,0 +1,37 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["Group"] + + +class Group(BaseModel): + chat_guid: Optional[str] = None + """BlueBubbles chat GUID if linked to a device group chat""" + + created_at: Optional[int] = None + + group_id: Optional[str] = None + + icon_url: Optional[str] = None + """URL of the group icon/photo""" + + last_message_direction: Optional[Literal["inbound", "outbound"]] = None + """Direction of the most recent message""" + + last_message_text: Optional[str] = None + """Text of the most recent message in the group""" + + last_message_time: Optional[int] = None + """Timestamp of the most recent message""" + + member_count: Optional[int] = None + + message_count: Optional[int] = None + """Total number of messages in this group""" + + name: Optional[str] = None + """Group name. Null for unnamed groups.""" diff --git a/src/blooio/types/group_create_params.py b/src/blooio/types/group_create_params.py new file mode 100644 index 0000000..9cb3443 --- /dev/null +++ b/src/blooio/types/group_create_params.py @@ -0,0 +1,28 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from .._types import SequenceNotStr + +__all__ = ["GroupCreateParams"] + + +class GroupCreateParams(TypedDict, total=False): + name: Required[str] + """Group name (max 255 characters)""" + + chat_guid: str + """BlueBubbles chat GUID to link this group to an existing iMessage chat. + + Use this to join groups created elsewhere. You can get this from the BlueBubbles + API or from inbound message webhooks. + """ + + members: SequenceNotStr[str] + """Phone numbers or emails of contacts in the group. + + When linking via chat_guid, this is for record-keeping only (members are not + added to the linked iMessage chat). + """ diff --git a/src/blooio/types/group_create_response.py b/src/blooio/types/group_create_response.py new file mode 100644 index 0000000..2a5266d --- /dev/null +++ b/src/blooio/types/group_create_response.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .group import Group + +__all__ = ["GroupCreateResponse"] + + +class GroupCreateResponse(Group): + added_members: Optional[List[str]] = None + """List of member identifiers that were added to the group""" + + created_contacts: Optional[List[str]] = None + """List of contacts that were auto-created""" diff --git a/src/blooio/types/group_delete_response.py b/src/blooio/types/group_delete_response.py new file mode 100644 index 0000000..63a26ad --- /dev/null +++ b/src/blooio/types/group_delete_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["GroupDeleteResponse"] + + +class GroupDeleteResponse(BaseModel): + deleted_at: Optional[int] = None + + success: Optional[bool] = None diff --git a/src/blooio/types/group_list_params.py b/src/blooio/types/group_list_params.py new file mode 100644 index 0000000..7628e4e --- /dev/null +++ b/src/blooio/types/group_list_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["GroupListParams"] + + +class GroupListParams(TypedDict, total=False): + limit: int + """Maximum number of items to return (1-200)""" + + offset: int + """Number of items to skip""" + + q: str + """Search query (matches group name)""" + + sort: Literal["recent", "oldest", "name_asc", "name_desc"] + """Sort order""" diff --git a/src/blooio/types/group_list_response.py b/src/blooio/types/group_list_response.py new file mode 100644 index 0000000..f787e8a --- /dev/null +++ b/src/blooio/types/group_list_response.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .group import Group +from .._models import BaseModel +from .pagination import Pagination + +__all__ = ["GroupListResponse"] + + +class GroupListResponse(BaseModel): + groups: Optional[List[Group]] = None + + pagination: Optional[Pagination] = None diff --git a/src/blooio/types/group_update_params.py b/src/blooio/types/group_update_params.py new file mode 100644 index 0000000..afb2969 --- /dev/null +++ b/src/blooio/types/group_update_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["GroupUpdateParams"] + + +class GroupUpdateParams(TypedDict, total=False): + name: str + """New group name""" diff --git a/src/blooio/types/group_update_response.py b/src/blooio/types/group_update_response.py new file mode 100644 index 0000000..d053bbf --- /dev/null +++ b/src/blooio/types/group_update_response.py @@ -0,0 +1,30 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .group import Group +from .._models import BaseModel + +__all__ = ["GroupUpdateResponse", "GroupUpdateResponseDeviceSync"] + + +class GroupUpdateResponseDeviceSync(BaseModel): + """Result of syncing the operation to a linked iMessage chat""" + + action: Optional[Literal["add_participant", "remove_participant", "leave"]] = None + """The action that was performed for the linked chat""" + + chat_guid: Optional[str] = None + """The linked iMessage chat GUID""" + + error: Optional[str] = None + """Error message if sync failed""" + + synced: Optional[bool] = None + """Whether the sync was successful""" + + +class GroupUpdateResponse(Group): + device_sync: Optional[GroupUpdateResponseDeviceSync] = None + """Result of syncing the operation to a linked iMessage chat""" diff --git a/src/blooio/types/groups/__init__.py b/src/blooio/types/groups/__init__.py new file mode 100644 index 0000000..caf4ab2 --- /dev/null +++ b/src/blooio/types/groups/__init__.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .group_icon import GroupIcon as GroupIcon +from .group_member import GroupMember as GroupMember +from .icon_set_params import IconSetParams as IconSetParams +from .member_add_params import MemberAddParams as MemberAddParams +from .member_list_params import MemberListParams as MemberListParams +from .member_add_response import MemberAddResponse as MemberAddResponse +from .member_list_response import MemberListResponse as MemberListResponse +from .member_remove_response import MemberRemoveResponse as MemberRemoveResponse diff --git a/src/blooio/types/groups/group_icon.py b/src/blooio/types/groups/group_icon.py new file mode 100644 index 0000000..6cc20fb --- /dev/null +++ b/src/blooio/types/groups/group_icon.py @@ -0,0 +1,41 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["GroupIcon", "DeviceSync"] + + +class DeviceSync(BaseModel): + """Linked chat sync status""" + + chat_guid: Optional[str] = None + + message: Optional[str] = None + """Status message about linked chat sync""" + + synced: Optional[bool] = None + """Whether the icon change was synced to the linked iMessage chat. + + This will be true on successful set/remove operations. + """ + + +class GroupIcon(BaseModel): + """Response for group icon operations""" + + chat_guid: Optional[str] = None + """The BlueBubbles chat GUID""" + + device_sync: Optional[DeviceSync] = None + """Linked chat sync status""" + + group_id: Optional[str] = None + + icon_url: Optional[str] = None + """URL of the uploaded icon (only present on set)""" + + message: Optional[str] = None + + success: Optional[bool] = None diff --git a/src/blooio/types/groups/group_member.py b/src/blooio/types/groups/group_member.py new file mode 100644 index 0000000..9701afc --- /dev/null +++ b/src/blooio/types/groups/group_member.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["GroupMember"] + + +class GroupMember(BaseModel): + id: Optional[str] = None + """Contact identifier (phone or email)""" + + added_at: Optional[int] = None + + contact_id: Optional[str] = None + + identifier: Optional[str] = None + + name: Optional[str] = None diff --git a/src/blooio/types/groups/icon_set_params.py b/src/blooio/types/groups/icon_set_params.py new file mode 100644 index 0000000..828c823 --- /dev/null +++ b/src/blooio/types/groups/icon_set_params.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from ..._types import FileTypes + +__all__ = ["IconSetParams"] + + +class IconSetParams(TypedDict, total=False): + icon: Required[FileTypes] + """The icon image file to set as the group photo""" diff --git a/src/blooio/types/groups/member_add_params.py b/src/blooio/types/groups/member_add_params.py new file mode 100644 index 0000000..a06cffc --- /dev/null +++ b/src/blooio/types/groups/member_add_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["MemberAddParams"] + + +class MemberAddParams(TypedDict, total=False): + contact_id: Required[str] + """Contact identifier (phone number or email)""" diff --git a/src/blooio/types/config/webhook_update_response.py b/src/blooio/types/groups/member_add_response.py similarity index 55% rename from src/blooio/types/config/webhook_update_response.py rename to src/blooio/types/groups/member_add_response.py index c8b8cce..606e03e 100644 --- a/src/blooio/types/config/webhook_update_response.py +++ b/src/blooio/types/groups/member_add_response.py @@ -3,11 +3,12 @@ from typing import Optional from ..._models import BaseModel +from .group_member import GroupMember -__all__ = ["WebhookUpdateResponse"] +__all__ = ["MemberAddResponse"] -class WebhookUpdateResponse(BaseModel): - message: Optional[str] = None +class MemberAddResponse(BaseModel): + member: Optional[GroupMember] = None - status: Optional[str] = None + message: Optional[str] = None diff --git a/src/blooio/types/groups/member_list_params.py b/src/blooio/types/groups/member_list_params.py new file mode 100644 index 0000000..75b4ade --- /dev/null +++ b/src/blooio/types/groups/member_list_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["MemberListParams"] + + +class MemberListParams(TypedDict, total=False): + limit: int + """Maximum number of items to return (1-200)""" + + offset: int + """Number of items to skip""" diff --git a/src/blooio/types/groups/member_list_response.py b/src/blooio/types/groups/member_list_response.py new file mode 100644 index 0000000..77ae951 --- /dev/null +++ b/src/blooio/types/groups/member_list_response.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel +from ..pagination import Pagination +from .group_member import GroupMember + +__all__ = ["MemberListResponse"] + + +class MemberListResponse(BaseModel): + group_id: Optional[str] = None + """The group ID""" + + group_name: Optional[str] = None + """The group name""" + + icon_url: Optional[str] = None + """URL of the group icon/photo""" + + members: Optional[List[GroupMember]] = None + + pagination: Optional[Pagination] = None diff --git a/src/blooio/types/groups/member_remove_response.py b/src/blooio/types/groups/member_remove_response.py new file mode 100644 index 0000000..e1cfe9c --- /dev/null +++ b/src/blooio/types/groups/member_remove_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["MemberRemoveResponse"] + + +class MemberRemoveResponse(BaseModel): + removed_at: Optional[int] = None + + success: Optional[bool] = None diff --git a/src/blooio/types/last_message.py b/src/blooio/types/last_message.py new file mode 100644 index 0000000..37a186a --- /dev/null +++ b/src/blooio/types/last_message.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["LastMessage"] + + +class LastMessage(BaseModel): + direction: Optional[Literal["inbound", "outbound"]] = None + + message_id: Optional[str] = None + + text: Optional[str] = None + + time_sent: Optional[int] = None diff --git a/src/blooio/types/location/__init__.py b/src/blooio/types/location/__init__.py new file mode 100644 index 0000000..c364c8d --- /dev/null +++ b/src/blooio/types/location/__init__.py @@ -0,0 +1,7 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .contact_location import ContactLocation as ContactLocation +from .contact_list_response import ContactListResponse as ContactListResponse +from .contact_refresh_response import ContactRefreshResponse as ContactRefreshResponse diff --git a/src/blooio/types/location/contact_list_response.py b/src/blooio/types/location/contact_list_response.py new file mode 100644 index 0000000..3b238fc --- /dev/null +++ b/src/blooio/types/location/contact_list_response.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel +from .contact_location import ContactLocation + +__all__ = ["ContactListResponse"] + + +class ContactListResponse(BaseModel): + friends: Optional[List[ContactLocation]] = None diff --git a/src/blooio/types/location/contact_location.py b/src/blooio/types/location/contact_location.py new file mode 100644 index 0000000..4b436a1 --- /dev/null +++ b/src/blooio/types/location/contact_location.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel + +__all__ = ["ContactLocation"] + + +class ContactLocation(BaseModel): + coordinates: Optional[List[float]] = None + """GPS coordinates [latitude, longitude]""" + + handle: Optional[str] = None + """Contact's phone number or email""" + + last_updated: Optional[int] = None + """Timestamp of last location update (epoch ms)""" + + status: Optional[str] = None + """Location status (e.g., 'live', 'shallow', 'legacy')""" diff --git a/src/blooio/types/location/contact_refresh_response.py b/src/blooio/types/location/contact_refresh_response.py new file mode 100644 index 0000000..f788094 --- /dev/null +++ b/src/blooio/types/location/contact_refresh_response.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from ..._models import BaseModel +from .contact_location import ContactLocation + +__all__ = ["ContactRefreshResponse"] + + +class ContactRefreshResponse(BaseModel): + friends: Optional[List[ContactLocation]] = None + + success: Optional[bool] = None diff --git a/src/blooio/types/me/__init__.py b/src/blooio/types/me/__init__.py new file mode 100644 index 0000000..e51f508 --- /dev/null +++ b/src/blooio/types/me/__init__.py @@ -0,0 +1,5 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .number_list_response import NumberListResponse as NumberListResponse diff --git a/src/blooio/types/me/number_list_response.py b/src/blooio/types/me/number_list_response.py new file mode 100644 index 0000000..48deca9 --- /dev/null +++ b/src/blooio/types/me/number_list_response.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import datetime + +from ..._models import BaseModel + +__all__ = ["NumberListResponse", "Number"] + + +class Number(BaseModel): + is_active: Optional[bool] = None + + last_active: Optional[datetime] = None + + phone_number: Optional[str] = None + + +class NumberListResponse(BaseModel): + numbers: Optional[List[Number]] = None diff --git a/src/blooio/types/me/numbers/__init__.py b/src/blooio/types/me/numbers/__init__.py new file mode 100644 index 0000000..306f28d --- /dev/null +++ b/src/blooio/types/me/numbers/__init__.py @@ -0,0 +1,7 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .contact_card_update_params import ContactCardUpdateParams as ContactCardUpdateParams +from .contact_card_update_response import ContactCardUpdateResponse as ContactCardUpdateResponse +from .contact_card_retrieve_response import ContactCardRetrieveResponse as ContactCardRetrieveResponse diff --git a/src/blooio/types/me/numbers/contact_card_retrieve_response.py b/src/blooio/types/me/numbers/contact_card_retrieve_response.py new file mode 100644 index 0000000..72e6efe --- /dev/null +++ b/src/blooio/types/me/numbers/contact_card_retrieve_response.py @@ -0,0 +1,36 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ...._models import BaseModel + +__all__ = ["ContactCardRetrieveResponse", "Sharing"] + + +class Sharing(BaseModel): + audience: Optional[int] = None + """0 = Contacts Only, 1 = Always Ask""" + + enabled: Optional[bool] = None + """Whether Name & Photo sharing is enabled""" + + name_format: Optional[int] = None + """0 = First & Last, 1 = First Only""" + + +class ContactCardRetrieveResponse(BaseModel): + avatar: Optional[str] = None + """Base64-encoded JPEG/PNG image""" + + first_name: Optional[str] = None + + has_wallpaper: Optional[bool] = None + + last_name: Optional[str] = None + + name: Optional[str] = None + """Display name""" + + phone_number: Optional[str] = None + + sharing: Optional[Sharing] = None diff --git a/src/blooio/types/me/numbers/contact_card_update_params.py b/src/blooio/types/me/numbers/contact_card_update_params.py new file mode 100644 index 0000000..7aa27ed --- /dev/null +++ b/src/blooio/types/me/numbers/contact_card_update_params.py @@ -0,0 +1,31 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["ContactCardUpdateParams", "Sharing"] + + +class ContactCardUpdateParams(TypedDict, total=False): + avatar: str + """Profile photo as base64-encoded JPEG/PNG""" + + first_name: str + """First name""" + + last_name: str + """Last name""" + + sharing: Sharing + + +class Sharing(TypedDict, total=False): + audience: int + """0 = Contacts Only, 1 = Always Ask""" + + enabled: bool + """Enable/disable Name & Photo sharing""" + + name_format: int + """0 = First & Last, 1 = First Only""" diff --git a/src/blooio/types/me/numbers/contact_card_update_response.py b/src/blooio/types/me/numbers/contact_card_update_response.py new file mode 100644 index 0000000..7210198 --- /dev/null +++ b/src/blooio/types/me/numbers/contact_card_update_response.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ...._models import BaseModel + +__all__ = ["ContactCardUpdateResponse"] + + +class ContactCardUpdateResponse(BaseModel): + first_name: Optional[str] = None + + last_name: Optional[str] = None + + phone_number: Optional[str] = None + + success: Optional[bool] = None diff --git a/src/blooio/types/me_retrieve_response.py b/src/blooio/types/me_retrieve_response.py index a42e8ff..aedcf64 100644 --- a/src/blooio/types/me_retrieve_response.py +++ b/src/blooio/types/me_retrieve_response.py @@ -1,67 +1,76 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import List, Optional +from typing_extensions import Literal from .._models import BaseModel -__all__ = ["MeRetrieveResponse", "Device", "IntegrationDetails", "Usage"] +__all__ = ["MeRetrieveResponse", "Device", "Organization", "Usage"] class Device(BaseModel): - device_hash: Optional[str] = None - """Hashed device identifier.""" - is_active: Optional[bool] = None - """Whether the device is currently active.""" last_active: Optional[int] = None - """Unix timestamp (ms) of last device activity.""" + phone_number: Optional[str] = None + """Phone number assigned to this device (E.164 format)""" -class IntegrationDetails(BaseModel): - """Integration-specific details (GHL or API integration).""" - customer_webhook_url: Optional[str] = None - """Webhook URL for API integrations.""" +class Organization(BaseModel): + country_code: Optional[str] = None - metadata: Optional[object] = None - """Integration-specific metadata.""" + created_at: Optional[int] = None name: Optional[str] = None - """Name of the integration (GHL only).""" + + organization_id: Optional[str] = None class Usage(BaseModel): - """Usage statistics for this API key.""" + """Usage statistics (only for api_key auth)""" inbound_messages: Optional[int] = None - """Total number of inbound messages.""" last_message_sent: Optional[int] = None - """Unix timestamp (ms) of the last message sent.""" outbound_messages: Optional[int] = None - """Total number of outbound messages.""" class MeRetrieveResponse(BaseModel): + """Response depends on auth_type. + + For 'api_key': includes full API key details. For 'dashboard': includes user_id and organization info only. + """ + api_key: Optional[str] = None - """The API key used for authentication.""" + """The API key (only for api_key auth)""" + + auth_type: Optional[Literal["api_key", "dashboard"]] = None + """Type of authentication used""" devices: Optional[List[Device]] = None - """List of devices associated with this API key.""" + """List of devices associated with this API key (only for api_key auth)""" - integration_details: Optional[IntegrationDetails] = None - """Integration-specific details (GHL or API integration).""" + integration_details: Optional[object] = None + """ + Integration details if the API key is associated with an integration (only for + api_key auth) + """ metadata: Optional[object] = None - """Custom metadata associated with the API key.""" + """API key metadata (only for api_key auth)""" - plan: Optional[str] = None - """The plan associated with this API key.""" + organization: Optional[Organization] = None + + organization_id: Optional[str] = None + """Organization ID (only for api_key auth)""" usage: Optional[Usage] = None - """Usage statistics for this API key.""" + """Usage statistics (only for api_key auth)""" + + user_id: Optional[str] = None + """User ID (only for dashboard auth)""" valid: Optional[bool] = None - """Whether the API key is valid.""" + """Whether the API key is valid (only for api_key auth)""" diff --git a/src/blooio/types/message_cancel_response.py b/src/blooio/types/message_cancel_response.py deleted file mode 100644 index 747276a..0000000 --- a/src/blooio/types/message_cancel_response.py +++ /dev/null @@ -1,23 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from .._models import BaseModel - -__all__ = ["MessageCancelResponse"] - - -class MessageCancelResponse(BaseModel): - cancelled: Optional[bool] = None - """True if cancellation was successful, false otherwise.""" - - current_status: Optional[str] = None - """The current status if cancellation failed (deprecated, use 'status' instead).""" - - message: Optional[str] = None - """Human-readable message about the cancellation result.""" - - message_id: Optional[str] = None - - status: Optional[str] = None - """The current status of the message.""" diff --git a/src/blooio/types/message_retrieve_response.py b/src/blooio/types/message_retrieve_response.py deleted file mode 100644 index 9993f5a..0000000 --- a/src/blooio/types/message_retrieve_response.py +++ /dev/null @@ -1,37 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional -from typing_extensions import Literal - -from .._models import BaseModel - -__all__ = ["MessageRetrieveResponse"] - - -class MessageRetrieveResponse(BaseModel): - api_key: Optional[str] = None - - attachments_count: Optional[int] = None - - direction: Optional[Literal["outbound", "inbound"]] = None - - external_id: Optional[str] = None - """Recipient phone number.""" - - message_id: Optional[str] = None - - metadata: Optional[object] = None - """Original metadata provided plus system generated fields.""" - - protocol: Optional[str] = None - """The protocol used to send the message (e.g., imessage, rcs, sms).""" - - status: Optional[ - Literal["pending", "queued", "sent", "delivered", "failed", "cancelled", "cancellation_requested"] - ] = None - """Current delivery status.""" - - text_length: Optional[int] = None - - time_sent: Optional[int] = None - """Unix timestamp (ms) when the message was queued/sent.""" diff --git a/src/blooio/types/message_send_params.py b/src/blooio/types/message_send_params.py deleted file mode 100644 index 36ff3cd..0000000 --- a/src/blooio/types/message_send_params.py +++ /dev/null @@ -1,26 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import Required, Annotated, TypedDict - -from .._types import SequenceNotStr -from .._utils import PropertyInfo - -__all__ = ["MessageSendParams"] - - -class MessageSendParams(TypedDict, total=False): - to: Required[str] - """Recipient phone number in E.164 format (e.g., +15551234567)""" - - attachments: SequenceNotStr[str] - """Array of publicly accessible URLs to media attachments.""" - - metadata: object - """Arbitrary key/value pairs to associate with the message.""" - - text: str - """Text body of the message.""" - - idempotency_key: Annotated[str, PropertyInfo(alias="Idempotency-Key")] diff --git a/src/blooio/types/pagination.py b/src/blooio/types/pagination.py new file mode 100644 index 0000000..8587fe5 --- /dev/null +++ b/src/blooio/types/pagination.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["Pagination"] + + +class Pagination(BaseModel): + limit: Optional[int] = None + + offset: Optional[int] = None + + total: Optional[int] = None diff --git a/src/blooio/types/phone_number_batch_create_params.py b/src/blooio/types/phone_number_batch_create_params.py new file mode 100644 index 0000000..d56a6d2 --- /dev/null +++ b/src/blooio/types/phone_number_batch_create_params.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from .._types import SequenceNotStr + +__all__ = ["PhoneNumberBatchCreateParams"] + + +class PhoneNumberBatchCreateParams(TypedDict, total=False): + numbers: Required[SequenceNotStr[str]] + """Array of phone numbers to look up""" diff --git a/src/blooio/types/phone_number_batch_create_response.py b/src/blooio/types/phone_number_batch_create_response.py new file mode 100644 index 0000000..467165e --- /dev/null +++ b/src/blooio/types/phone_number_batch_create_response.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel +from .phone_numbers.phone_number_lookup_result import PhoneNumberLookupResult + +__all__ = ["PhoneNumberBatchCreateResponse"] + + +class PhoneNumberBatchCreateResponse(BaseModel): + results: Optional[List[PhoneNumberLookupResult]] = None diff --git a/src/blooio/types/phone_numbers/__init__.py b/src/blooio/types/phone_numbers/__init__.py new file mode 100644 index 0000000..aae359e --- /dev/null +++ b/src/blooio/types/phone_numbers/__init__.py @@ -0,0 +1,7 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .lookup_create_params import LookupCreateParams as LookupCreateParams +from .lookup_retrieve_params import LookupRetrieveParams as LookupRetrieveParams +from .phone_number_lookup_result import PhoneNumberLookupResult as PhoneNumberLookupResult diff --git a/src/blooio/types/config/webhook_update_params.py b/src/blooio/types/phone_numbers/lookup_create_params.py similarity index 55% rename from src/blooio/types/config/webhook_update_params.py rename to src/blooio/types/phone_numbers/lookup_create_params.py index f8a173a..84c0848 100644 --- a/src/blooio/types/config/webhook_update_params.py +++ b/src/blooio/types/phone_numbers/lookup_create_params.py @@ -4,8 +4,9 @@ from typing_extensions import Required, TypedDict -__all__ = ["WebhookUpdateParams"] +__all__ = ["LookupCreateParams"] -class WebhookUpdateParams(TypedDict, total=False): - webhook_url: Required[str] +class LookupCreateParams(TypedDict, total=False): + number: Required[str] + """Phone number to look up""" diff --git a/src/blooio/types/phone_numbers/lookup_retrieve_params.py b/src/blooio/types/phone_numbers/lookup_retrieve_params.py new file mode 100644 index 0000000..024b1ae --- /dev/null +++ b/src/blooio/types/phone_numbers/lookup_retrieve_params.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["LookupRetrieveParams"] + + +class LookupRetrieveParams(TypedDict, total=False): + number: Required[str] + """Phone number to look up. + + Can be E.164 format (+12125551234), national format (2125551234), or with + formatting ((212) 555-1234). + """ diff --git a/src/blooio/types/phone_numbers/phone_number_lookup_result.py b/src/blooio/types/phone_numbers/phone_number_lookup_result.py new file mode 100644 index 0000000..608fdd5 --- /dev/null +++ b/src/blooio/types/phone_numbers/phone_number_lookup_result.py @@ -0,0 +1,79 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["PhoneNumberLookupResult", "Location"] + + +class Location(BaseModel): + """NANPA geocoding location (only for North American numbers with country code 1)""" + + city: Optional[str] = None + """City name""" + + region: Optional[str] = None + """State/province abbreviation""" + + region_name: Optional[str] = None + """Full state/province name""" + + +class PhoneNumberLookupResult(BaseModel): + area_code: Optional[str] = None + """NPA area code (first 3 digits of national number, only for NANP numbers)""" + + area_code_region: Optional[str] = None + """General region for the area code (most common city, only for NANP numbers)""" + + country: Optional[str] = None + """ISO 3166-1 alpha-2 country code""" + + country_calling_code: Optional[str] = None + """Country calling code without +""" + + e164: Optional[str] = None + """E.164 formatted number""" + + exchange: Optional[str] = None + """NXX exchange code (digits 4-6 of national number, only for NANP numbers)""" + + input: Optional[str] = None + """The original input string""" + + international: Optional[str] = None + """International formatted number""" + + location: Optional[Location] = None + """NANPA geocoding location (only for North American numbers with country code 1)""" + + national: Optional[str] = None + """National formatted number""" + + national_number: Optional[str] = None + """National number without country code""" + + possible: Optional[bool] = None + """Whether the phone number is a possible number (less strict than valid)""" + + type: Optional[ + Literal[ + "FIXED_LINE", + "MOBILE", + "FIXED_LINE_OR_MOBILE", + "TOLL_FREE", + "PREMIUM_RATE", + "SHARED_COST", + "VOIP", + "PERSONAL_NUMBER", + "PAGER", + "UAN", + "VOICEMAIL", + ] + ] = None + """Number type detected by libphonenumber""" + + valid: Optional[bool] = None + """Whether the phone number is valid""" diff --git a/src/blooio/types/webhook.py b/src/blooio/types/webhook.py new file mode 100644 index 0000000..6d7c9c2 --- /dev/null +++ b/src/blooio/types/webhook.py @@ -0,0 +1,38 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["Webhook"] + + +class Webhook(BaseModel): + api_key_name: Optional[str] = None + """Name of the API key (if scope is api_key)""" + + created_at: Optional[int] = None + + deprecated_at: Optional[int] = None + + failure_count: Optional[int] = None + + integration_name: Optional[str] = None + """Name of the integration (if scope is integration)""" + + is_active: Optional[bool] = None + """Whether the webhook is active (not deprecated)""" + + last_triggered: Optional[int] = None + + scope: Optional[Literal["api_key", "organization", "integration"]] = None + + valid_until: Optional[int] = None + """-1 means no expiration""" + + webhook_id: Optional[str] = None + + webhook_type: Optional[Literal["message", "status", "all"]] = None + + webhook_url: Optional[str] = None diff --git a/src/blooio/types/webhook_create_params.py b/src/blooio/types/webhook_create_params.py new file mode 100644 index 0000000..99c36b1 --- /dev/null +++ b/src/blooio/types/webhook_create_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["WebhookCreateParams"] + + +class WebhookCreateParams(TypedDict, total=False): + webhook_url: Required[str] + """URL to receive webhook events""" + + valid_until: int + """Expiration timestamp (-1 for no expiration)""" + + webhook_type: Literal["message", "status", "all"] + """Type of events to receive""" diff --git a/src/blooio/types/webhook_create_response.py b/src/blooio/types/webhook_create_response.py new file mode 100644 index 0000000..82a6eda --- /dev/null +++ b/src/blooio/types/webhook_create_response.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["WebhookCreateResponse"] + + +class WebhookCreateResponse(BaseModel): + message: Optional[str] = None + + scope: Optional[Literal["api_key", "organization"]] = None + + webhook_id: Optional[str] = None + + webhook_url: Optional[str] = None diff --git a/src/blooio/types/webhook_delete_response.py b/src/blooio/types/webhook_delete_response.py new file mode 100644 index 0000000..dd330b1 --- /dev/null +++ b/src/blooio/types/webhook_delete_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["WebhookDeleteResponse"] + + +class WebhookDeleteResponse(BaseModel): + message: Optional[str] = None + + success: Optional[bool] = None diff --git a/src/blooio/types/webhook_list_response.py b/src/blooio/types/webhook_list_response.py new file mode 100644 index 0000000..5ec884c --- /dev/null +++ b/src/blooio/types/webhook_list_response.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .webhook import Webhook +from .._models import BaseModel + +__all__ = ["WebhookListResponse"] + + +class WebhookListResponse(BaseModel): + webhooks: Optional[List[Webhook]] = None diff --git a/src/blooio/types/webhook_update_params.py b/src/blooio/types/webhook_update_params.py new file mode 100644 index 0000000..20f0de3 --- /dev/null +++ b/src/blooio/types/webhook_update_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["WebhookUpdateParams"] + + +class WebhookUpdateParams(TypedDict, total=False): + deprecate: bool + """Set to true to deprecate, false to undeprecate""" + + valid_until: int + """Expiration timestamp. Use -1 or null for no expiration.""" + + webhook_type: Literal["message", "status", "all"] + """Type of events to receive""" diff --git a/src/blooio/types/webhooks/__init__.py b/src/blooio/types/webhooks/__init__.py new file mode 100644 index 0000000..a9920ad --- /dev/null +++ b/src/blooio/types/webhooks/__init__.py @@ -0,0 +1,8 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .log_list_params import LogListParams as LogListParams +from .log_list_response import LogListResponse as LogListResponse +from .log_replay_response import LogReplayResponse as LogReplayResponse +from .secret_rotate_response import SecretRotateResponse as SecretRotateResponse diff --git a/src/blooio/types/webhooks/log_list_params.py b/src/blooio/types/webhooks/log_list_params.py new file mode 100644 index 0000000..d8aeaaa --- /dev/null +++ b/src/blooio/types/webhooks/log_list_params.py @@ -0,0 +1,27 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["LogListParams"] + + +class LogListParams(TypedDict, total=False): + limit: int + """Maximum number of items to return (1-200)""" + + max_status: int + """Maximum HTTP status code""" + + min_status: int + """Minimum HTTP status code""" + + offset: int + """Number of items to skip""" + + sort: Literal["asc", "desc"] + """Sort order by attempted time""" + + status: int + """Filter by exact HTTP status code""" diff --git a/src/blooio/types/webhooks/log_list_response.py b/src/blooio/types/webhooks/log_list_response.py new file mode 100644 index 0000000..386c9d6 --- /dev/null +++ b/src/blooio/types/webhooks/log_list_response.py @@ -0,0 +1,162 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = [ + "LogListResponse", + "Log", + "LogEventBody", + "LogEventBodyAttachment", + "LogEventBodyParticipant", + "LogMetadata", + "Pagination", +] + + +class LogEventBodyAttachment(BaseModel): + name: Optional[str] = None + + url: Optional[str] = None + + +class LogEventBodyParticipant(BaseModel): + contact_id: Optional[str] = None + + identifier: Optional[str] = None + + name: Optional[str] = None + + +class LogEventBody(BaseModel): + """Webhook event payload. + + Structure varies by event type. All message events include group information when applicable. + """ + + attachments: Optional[List[LogEventBodyAttachment]] = None + """Array of attachment objects""" + + delivered_at: Optional[int] = None + """Timestamp when message was delivered (for message.delivered events)""" + + error_code: Optional[str] = None + """Error code (for message.failed events)""" + + error_message: Optional[str] = None + """Error description (for message.failed events)""" + + event: Optional[str] = None + """ + Event type (e.g., message.received, message.sent, message.delivered, + message.failed, message.read) + """ + + external_id: Optional[str] = None + """Recipient identifier (phone number, email, or group ID)""" + + group_id: Optional[str] = None + """Group ID (only present when is_group=true)""" + + group_name: Optional[str] = None + """Group display name (only present when is_group=true)""" + + internal_id: Optional[str] = None + """Phone number that sent/received the message""" + + is_group: Optional[bool] = None + """Whether this message is from/to a group chat. Always present.""" + + message_id: Optional[str] = None + """Unique message identifier""" + + participants: Optional[List[LogEventBodyParticipant]] = None + """Array of group participants (only present when is_group=true)""" + + protocol: Optional[Literal["imessage", "sms", "rcs", "non-imessage"]] = None + """Message protocol""" + + read_at: Optional[int] = None + """Timestamp when message was read (for message.read events)""" + + sender: Optional[str] = None + """Sender identifier (for inbound messages)""" + + sent_at: Optional[int] = None + """Timestamp when message was sent (for message.sent events)""" + + status: Optional[Literal["queued", "pending", "sent", "delivered", "failed", "read", "received"]] = None + """Message status""" + + text: Optional[str] = None + """Message text content""" + + timestamp: Optional[int] = None + """Event timestamp in milliseconds""" + + +class LogMetadata(BaseModel): + """Additional metadata about the webhook delivery""" + + duration_ms: Optional[int] = None + + event_name: Optional[str] = None + + is_replay: Optional[bool] = None + + message_id: Optional[str] = None + + organization_id: Optional[str] = None + + original_event_id: Optional[str] = None + + +class Log(BaseModel): + attempted_time: Optional[int] = None + + event_body: Optional[LogEventBody] = None + """Webhook event payload. + + Structure varies by event type. All message events include group information + when applicable. + """ + + event_id: Optional[str] = None + + metadata: Optional[LogMetadata] = None + """Additional metadata about the webhook delivery""" + + response_json: Optional[object] = None + """Response body from the webhook endpoint (if JSON)""" + + response_received_at: Optional[int] = None + + response_status: Optional[int] = None + """HTTP status code received from the webhook endpoint""" + + scope: Optional[Literal["api", "integration", "org"]] = None + + webhook_url: Optional[str] = None + + +class Pagination(BaseModel): + has_more: Optional[bool] = None + """Whether there are more logs to fetch""" + + limit: Optional[int] = None + + offset: Optional[int] = None + + returned: Optional[int] = None + """Number of logs returned in this response""" + + total: Optional[int] = None + """Total number of matching logs""" + + +class LogListResponse(BaseModel): + logs: Optional[List[Log]] = None + + pagination: Optional[Pagination] = None diff --git a/src/blooio/types/webhooks/log_replay_response.py b/src/blooio/types/webhooks/log_replay_response.py new file mode 100644 index 0000000..14690bb --- /dev/null +++ b/src/blooio/types/webhooks/log_replay_response.py @@ -0,0 +1,52 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from pydantic import Field as FieldInfo + +from ..._models import BaseModel + +__all__ = ["LogReplayResponse", "ResponseData"] + + +class ResponseData(BaseModel): + """Response details from the replay attempt""" + + body: Optional[object] = None + """Response body (if parseable)""" + + content_type: Optional[str] = FieldInfo(alias="contentType", default=None) + + duration: Optional[int] = None + + error: Optional[str] = None + + error_type: Optional[str] = FieldInfo(alias="errorType", default=None) + + headers: Optional[object] = None + + size: Optional[int] = None + + +class LogReplayResponse(BaseModel): + duration_ms: Optional[int] = None + """Time taken for the replay request in milliseconds""" + + original_event_id: Optional[str] = None + """The original event ID that was replayed""" + + replay_event_id: Optional[str] = None + """New event ID for this replay attempt""" + + response_data: Optional[ResponseData] = None + """Response details from the replay attempt""" + + response_status: Optional[int] = None + """HTTP status code from replay attempt""" + + success: Optional[bool] = None + """Whether the replay received a 2xx response""" + + webhook_id: Optional[str] = None + + webhook_url: Optional[str] = None diff --git a/src/blooio/types/webhooks/secret_rotate_response.py b/src/blooio/types/webhooks/secret_rotate_response.py new file mode 100644 index 0000000..6e89653 --- /dev/null +++ b/src/blooio/types/webhooks/secret_rotate_response.py @@ -0,0 +1,23 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["SecretRotateResponse"] + + +class SecretRotateResponse(BaseModel): + rotated_at: Optional[int] = None + """Timestamp when the secret was rotated""" + + rotated_by: Optional[str] = None + """Identifier of who rotated the secret""" + + rotation_count: Optional[int] = None + """Total number of times this secret has been rotated""" + + signing_secret: Optional[str] = None + """The new signing secret. Store this securely - it will not be shown again.""" + + webhook_id: Optional[str] = None diff --git a/tests/api_resources/config/__init__.py b/tests/api_resources/chats/__init__.py similarity index 100% rename from tests/api_resources/config/__init__.py rename to tests/api_resources/chats/__init__.py diff --git a/tests/api_resources/chats/test_background.py b/tests/api_resources/chats/test_background.py new file mode 100644 index 0000000..4772183 --- /dev/null +++ b/tests/api_resources/chats/test_background.py @@ -0,0 +1,284 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from blooio import Blooio, AsyncBlooio +from tests.utils import assert_matches_type +from blooio.types.chats import ChatBackgroundResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestBackground: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: Blooio) -> None: + background = client.chats.background.retrieve( + "chatId", + ) + assert_matches_type(ChatBackgroundResponse, background, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Blooio) -> None: + response = client.chats.background.with_raw_response.retrieve( + "chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + background = response.parse() + assert_matches_type(ChatBackgroundResponse, background, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Blooio) -> None: + with client.chats.background.with_streaming_response.retrieve( + "chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + background = response.parse() + assert_matches_type(ChatBackgroundResponse, background, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.chats.background.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_remove(self, client: Blooio) -> None: + background = client.chats.background.remove( + "chatId", + ) + assert_matches_type(ChatBackgroundResponse, background, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_remove(self, client: Blooio) -> None: + response = client.chats.background.with_raw_response.remove( + "chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + background = response.parse() + assert_matches_type(ChatBackgroundResponse, background, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_remove(self, client: Blooio) -> None: + with client.chats.background.with_streaming_response.remove( + "chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + background = response.parse() + assert_matches_type(ChatBackgroundResponse, background, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_remove(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.chats.background.with_raw_response.remove( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_set(self, client: Blooio) -> None: + background = client.chats.background.set( + chat_id="chatId", + background=b"Example data", + ) + assert_matches_type(ChatBackgroundResponse, background, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_set(self, client: Blooio) -> None: + response = client.chats.background.with_raw_response.set( + chat_id="chatId", + background=b"Example data", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + background = response.parse() + assert_matches_type(ChatBackgroundResponse, background, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_set(self, client: Blooio) -> None: + with client.chats.background.with_streaming_response.set( + chat_id="chatId", + background=b"Example data", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + background = response.parse() + assert_matches_type(ChatBackgroundResponse, background, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_set(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.chats.background.with_raw_response.set( + chat_id="", + background=b"Example data", + ) + + +class TestAsyncBackground: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncBlooio) -> None: + background = await async_client.chats.background.retrieve( + "chatId", + ) + assert_matches_type(ChatBackgroundResponse, background, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncBlooio) -> None: + response = await async_client.chats.background.with_raw_response.retrieve( + "chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + background = await response.parse() + assert_matches_type(ChatBackgroundResponse, background, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncBlooio) -> None: + async with async_client.chats.background.with_streaming_response.retrieve( + "chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + background = await response.parse() + assert_matches_type(ChatBackgroundResponse, background, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.chats.background.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_remove(self, async_client: AsyncBlooio) -> None: + background = await async_client.chats.background.remove( + "chatId", + ) + assert_matches_type(ChatBackgroundResponse, background, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_remove(self, async_client: AsyncBlooio) -> None: + response = await async_client.chats.background.with_raw_response.remove( + "chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + background = await response.parse() + assert_matches_type(ChatBackgroundResponse, background, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_remove(self, async_client: AsyncBlooio) -> None: + async with async_client.chats.background.with_streaming_response.remove( + "chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + background = await response.parse() + assert_matches_type(ChatBackgroundResponse, background, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_remove(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.chats.background.with_raw_response.remove( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_set(self, async_client: AsyncBlooio) -> None: + background = await async_client.chats.background.set( + chat_id="chatId", + background=b"Example data", + ) + assert_matches_type(ChatBackgroundResponse, background, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_set(self, async_client: AsyncBlooio) -> None: + response = await async_client.chats.background.with_raw_response.set( + chat_id="chatId", + background=b"Example data", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + background = await response.parse() + assert_matches_type(ChatBackgroundResponse, background, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_set(self, async_client: AsyncBlooio) -> None: + async with async_client.chats.background.with_streaming_response.set( + chat_id="chatId", + background=b"Example data", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + background = await response.parse() + assert_matches_type(ChatBackgroundResponse, background, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_set(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.chats.background.with_raw_response.set( + chat_id="", + background=b"Example data", + ) diff --git a/tests/api_resources/chats/test_messages.py b/tests/api_resources/chats/test_messages.py new file mode 100644 index 0000000..9034d44 --- /dev/null +++ b/tests/api_resources/chats/test_messages.py @@ -0,0 +1,630 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from blooio import Blooio, AsyncBlooio +from tests.utils import assert_matches_type +from blooio.types.chats import ( + MessageListResponse, + MessageSendResponse, + MessageReactResponse, + MessageRetrieveResponse, + MessageGetStatusResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestMessages: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: Blooio) -> None: + message = client.chats.messages.retrieve( + message_id="msg_abc123def456", + chat_id="chatId", + ) + assert_matches_type(MessageRetrieveResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Blooio) -> None: + response = client.chats.messages.with_raw_response.retrieve( + message_id="msg_abc123def456", + chat_id="chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = response.parse() + assert_matches_type(MessageRetrieveResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Blooio) -> None: + with client.chats.messages.with_streaming_response.retrieve( + message_id="msg_abc123def456", + chat_id="chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = response.parse() + assert_matches_type(MessageRetrieveResponse, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.chats.messages.with_raw_response.retrieve( + message_id="msg_abc123def456", + chat_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `message_id` but received ''"): + client.chats.messages.with_raw_response.retrieve( + message_id="", + chat_id="chatId", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: Blooio) -> None: + message = client.chats.messages.list( + chat_id="chatId", + ) + assert_matches_type(MessageListResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Blooio) -> None: + message = client.chats.messages.list( + chat_id="chatId", + direction="inbound", + limit=1, + offset=0, + since=0, + sort="asc", + until=0, + ) + assert_matches_type(MessageListResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: Blooio) -> None: + response = client.chats.messages.with_raw_response.list( + chat_id="chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = response.parse() + assert_matches_type(MessageListResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Blooio) -> None: + with client.chats.messages.with_streaming_response.list( + chat_id="chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = response.parse() + assert_matches_type(MessageListResponse, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_list(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.chats.messages.with_raw_response.list( + chat_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_get_status(self, client: Blooio) -> None: + message = client.chats.messages.get_status( + message_id="msg_abc123def456", + chat_id="chatId", + ) + assert_matches_type(MessageGetStatusResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_get_status(self, client: Blooio) -> None: + response = client.chats.messages.with_raw_response.get_status( + message_id="msg_abc123def456", + chat_id="chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = response.parse() + assert_matches_type(MessageGetStatusResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_get_status(self, client: Blooio) -> None: + with client.chats.messages.with_streaming_response.get_status( + message_id="msg_abc123def456", + chat_id="chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = response.parse() + assert_matches_type(MessageGetStatusResponse, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_get_status(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.chats.messages.with_raw_response.get_status( + message_id="msg_abc123def456", + chat_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `message_id` but received ''"): + client.chats.messages.with_raw_response.get_status( + message_id="", + chat_id="chatId", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_react(self, client: Blooio) -> None: + message = client.chats.messages.react( + message_id="messageId", + chat_id="chatId", + reaction="+love", + ) + assert_matches_type(MessageReactResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_react_with_all_params(self, client: Blooio) -> None: + message = client.chats.messages.react( + message_id="messageId", + chat_id="chatId", + reaction="+love", + direction="inbound", + ) + assert_matches_type(MessageReactResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_react(self, client: Blooio) -> None: + response = client.chats.messages.with_raw_response.react( + message_id="messageId", + chat_id="chatId", + reaction="+love", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = response.parse() + assert_matches_type(MessageReactResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_react(self, client: Blooio) -> None: + with client.chats.messages.with_streaming_response.react( + message_id="messageId", + chat_id="chatId", + reaction="+love", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = response.parse() + assert_matches_type(MessageReactResponse, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_react(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.chats.messages.with_raw_response.react( + message_id="messageId", + chat_id="", + reaction="+love", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `message_id` but received ''"): + client.chats.messages.with_raw_response.react( + message_id="", + chat_id="chatId", + reaction="+love", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_send(self, client: Blooio) -> None: + message = client.chats.messages.send( + chat_id="chatId", + ) + assert_matches_type(MessageSendResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_send_with_all_params(self, client: Blooio) -> None: + message = client.chats.messages.send( + chat_id="chatId", + attachments=["string"], + from_number="from_number", + link_preview={ + "image_url": "https://example.com", + "title": "title", + }, + parts=[ + { + "link_preview": { + "image_url": "https://example.com", + "title": "title", + }, + "mention": "mention", + "name": "name", + "text": "text", + "url": "url", + } + ], + share_contact=True, + text="string", + use_typing_indicator=True, + idempotency_key="Idempotency-Key", + ) + assert_matches_type(MessageSendResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_send(self, client: Blooio) -> None: + response = client.chats.messages.with_raw_response.send( + chat_id="chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = response.parse() + assert_matches_type(MessageSendResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_send(self, client: Blooio) -> None: + with client.chats.messages.with_streaming_response.send( + chat_id="chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = response.parse() + assert_matches_type(MessageSendResponse, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_send(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.chats.messages.with_raw_response.send( + chat_id="", + ) + + +class TestAsyncMessages: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncBlooio) -> None: + message = await async_client.chats.messages.retrieve( + message_id="msg_abc123def456", + chat_id="chatId", + ) + assert_matches_type(MessageRetrieveResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncBlooio) -> None: + response = await async_client.chats.messages.with_raw_response.retrieve( + message_id="msg_abc123def456", + chat_id="chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = await response.parse() + assert_matches_type(MessageRetrieveResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncBlooio) -> None: + async with async_client.chats.messages.with_streaming_response.retrieve( + message_id="msg_abc123def456", + chat_id="chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = await response.parse() + assert_matches_type(MessageRetrieveResponse, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.chats.messages.with_raw_response.retrieve( + message_id="msg_abc123def456", + chat_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `message_id` but received ''"): + await async_client.chats.messages.with_raw_response.retrieve( + message_id="", + chat_id="chatId", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncBlooio) -> None: + message = await async_client.chats.messages.list( + chat_id="chatId", + ) + assert_matches_type(MessageListResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncBlooio) -> None: + message = await async_client.chats.messages.list( + chat_id="chatId", + direction="inbound", + limit=1, + offset=0, + since=0, + sort="asc", + until=0, + ) + assert_matches_type(MessageListResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncBlooio) -> None: + response = await async_client.chats.messages.with_raw_response.list( + chat_id="chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = await response.parse() + assert_matches_type(MessageListResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncBlooio) -> None: + async with async_client.chats.messages.with_streaming_response.list( + chat_id="chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = await response.parse() + assert_matches_type(MessageListResponse, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_list(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.chats.messages.with_raw_response.list( + chat_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_get_status(self, async_client: AsyncBlooio) -> None: + message = await async_client.chats.messages.get_status( + message_id="msg_abc123def456", + chat_id="chatId", + ) + assert_matches_type(MessageGetStatusResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_get_status(self, async_client: AsyncBlooio) -> None: + response = await async_client.chats.messages.with_raw_response.get_status( + message_id="msg_abc123def456", + chat_id="chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = await response.parse() + assert_matches_type(MessageGetStatusResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_get_status(self, async_client: AsyncBlooio) -> None: + async with async_client.chats.messages.with_streaming_response.get_status( + message_id="msg_abc123def456", + chat_id="chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = await response.parse() + assert_matches_type(MessageGetStatusResponse, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_get_status(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.chats.messages.with_raw_response.get_status( + message_id="msg_abc123def456", + chat_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `message_id` but received ''"): + await async_client.chats.messages.with_raw_response.get_status( + message_id="", + chat_id="chatId", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_react(self, async_client: AsyncBlooio) -> None: + message = await async_client.chats.messages.react( + message_id="messageId", + chat_id="chatId", + reaction="+love", + ) + assert_matches_type(MessageReactResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_react_with_all_params(self, async_client: AsyncBlooio) -> None: + message = await async_client.chats.messages.react( + message_id="messageId", + chat_id="chatId", + reaction="+love", + direction="inbound", + ) + assert_matches_type(MessageReactResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_react(self, async_client: AsyncBlooio) -> None: + response = await async_client.chats.messages.with_raw_response.react( + message_id="messageId", + chat_id="chatId", + reaction="+love", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = await response.parse() + assert_matches_type(MessageReactResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_react(self, async_client: AsyncBlooio) -> None: + async with async_client.chats.messages.with_streaming_response.react( + message_id="messageId", + chat_id="chatId", + reaction="+love", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = await response.parse() + assert_matches_type(MessageReactResponse, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_react(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.chats.messages.with_raw_response.react( + message_id="messageId", + chat_id="", + reaction="+love", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `message_id` but received ''"): + await async_client.chats.messages.with_raw_response.react( + message_id="", + chat_id="chatId", + reaction="+love", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_send(self, async_client: AsyncBlooio) -> None: + message = await async_client.chats.messages.send( + chat_id="chatId", + ) + assert_matches_type(MessageSendResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_send_with_all_params(self, async_client: AsyncBlooio) -> None: + message = await async_client.chats.messages.send( + chat_id="chatId", + attachments=["string"], + from_number="from_number", + link_preview={ + "image_url": "https://example.com", + "title": "title", + }, + parts=[ + { + "link_preview": { + "image_url": "https://example.com", + "title": "title", + }, + "mention": "mention", + "name": "name", + "text": "text", + "url": "url", + } + ], + share_contact=True, + text="string", + use_typing_indicator=True, + idempotency_key="Idempotency-Key", + ) + assert_matches_type(MessageSendResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_send(self, async_client: AsyncBlooio) -> None: + response = await async_client.chats.messages.with_raw_response.send( + chat_id="chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = await response.parse() + assert_matches_type(MessageSendResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_send(self, async_client: AsyncBlooio) -> None: + async with async_client.chats.messages.with_streaming_response.send( + chat_id="chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = await response.parse() + assert_matches_type(MessageSendResponse, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_send(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.chats.messages.with_raw_response.send( + chat_id="", + ) diff --git a/tests/api_resources/chats/test_polls.py b/tests/api_resources/chats/test_polls.py new file mode 100644 index 0000000..aa5cd9d --- /dev/null +++ b/tests/api_resources/chats/test_polls.py @@ -0,0 +1,240 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from blooio import Blooio, AsyncBlooio +from tests.utils import assert_matches_type +from blooio.types.chats import PollSendResponse, PollGetResultsResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestPolls: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_get_results(self, client: Blooio) -> None: + poll = client.chats.polls.get_results( + poll_id="pollId", + chat_id="chatId", + ) + assert_matches_type(PollGetResultsResponse, poll, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_get_results(self, client: Blooio) -> None: + response = client.chats.polls.with_raw_response.get_results( + poll_id="pollId", + chat_id="chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + poll = response.parse() + assert_matches_type(PollGetResultsResponse, poll, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_get_results(self, client: Blooio) -> None: + with client.chats.polls.with_streaming_response.get_results( + poll_id="pollId", + chat_id="chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + poll = response.parse() + assert_matches_type(PollGetResultsResponse, poll, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_get_results(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.chats.polls.with_raw_response.get_results( + poll_id="pollId", + chat_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `poll_id` but received ''"): + client.chats.polls.with_raw_response.get_results( + poll_id="", + chat_id="chatId", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_send(self, client: Blooio) -> None: + poll = client.chats.polls.send( + chat_id="chatId", + options=["string", "string"], + ) + assert_matches_type(PollSendResponse, poll, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_send_with_all_params(self, client: Blooio) -> None: + poll = client.chats.polls.send( + chat_id="chatId", + options=["string", "string"], + title="title", + ) + assert_matches_type(PollSendResponse, poll, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_send(self, client: Blooio) -> None: + response = client.chats.polls.with_raw_response.send( + chat_id="chatId", + options=["string", "string"], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + poll = response.parse() + assert_matches_type(PollSendResponse, poll, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_send(self, client: Blooio) -> None: + with client.chats.polls.with_streaming_response.send( + chat_id="chatId", + options=["string", "string"], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + poll = response.parse() + assert_matches_type(PollSendResponse, poll, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_send(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.chats.polls.with_raw_response.send( + chat_id="", + options=["string", "string"], + ) + + +class TestAsyncPolls: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_get_results(self, async_client: AsyncBlooio) -> None: + poll = await async_client.chats.polls.get_results( + poll_id="pollId", + chat_id="chatId", + ) + assert_matches_type(PollGetResultsResponse, poll, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_get_results(self, async_client: AsyncBlooio) -> None: + response = await async_client.chats.polls.with_raw_response.get_results( + poll_id="pollId", + chat_id="chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + poll = await response.parse() + assert_matches_type(PollGetResultsResponse, poll, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_get_results(self, async_client: AsyncBlooio) -> None: + async with async_client.chats.polls.with_streaming_response.get_results( + poll_id="pollId", + chat_id="chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + poll = await response.parse() + assert_matches_type(PollGetResultsResponse, poll, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_get_results(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.chats.polls.with_raw_response.get_results( + poll_id="pollId", + chat_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `poll_id` but received ''"): + await async_client.chats.polls.with_raw_response.get_results( + poll_id="", + chat_id="chatId", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_send(self, async_client: AsyncBlooio) -> None: + poll = await async_client.chats.polls.send( + chat_id="chatId", + options=["string", "string"], + ) + assert_matches_type(PollSendResponse, poll, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_send_with_all_params(self, async_client: AsyncBlooio) -> None: + poll = await async_client.chats.polls.send( + chat_id="chatId", + options=["string", "string"], + title="title", + ) + assert_matches_type(PollSendResponse, poll, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_send(self, async_client: AsyncBlooio) -> None: + response = await async_client.chats.polls.with_raw_response.send( + chat_id="chatId", + options=["string", "string"], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + poll = await response.parse() + assert_matches_type(PollSendResponse, poll, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_send(self, async_client: AsyncBlooio) -> None: + async with async_client.chats.polls.with_streaming_response.send( + chat_id="chatId", + options=["string", "string"], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + poll = await response.parse() + assert_matches_type(PollSendResponse, poll, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_send(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.chats.polls.with_raw_response.send( + chat_id="", + options=["string", "string"], + ) diff --git a/tests/api_resources/chats/test_typing.py b/tests/api_resources/chats/test_typing.py new file mode 100644 index 0000000..38f1577 --- /dev/null +++ b/tests/api_resources/chats/test_typing.py @@ -0,0 +1,192 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from blooio import Blooio, AsyncBlooio +from tests.utils import assert_matches_type +from blooio.types.chats import TypingResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestTyping: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_start(self, client: Blooio) -> None: + typing = client.chats.typing.start( + "chatId", + ) + assert_matches_type(TypingResponse, typing, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_start(self, client: Blooio) -> None: + response = client.chats.typing.with_raw_response.start( + "chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + typing = response.parse() + assert_matches_type(TypingResponse, typing, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_start(self, client: Blooio) -> None: + with client.chats.typing.with_streaming_response.start( + "chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + typing = response.parse() + assert_matches_type(TypingResponse, typing, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_start(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.chats.typing.with_raw_response.start( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_stop(self, client: Blooio) -> None: + typing = client.chats.typing.stop( + "chatId", + ) + assert_matches_type(TypingResponse, typing, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_stop(self, client: Blooio) -> None: + response = client.chats.typing.with_raw_response.stop( + "chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + typing = response.parse() + assert_matches_type(TypingResponse, typing, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_stop(self, client: Blooio) -> None: + with client.chats.typing.with_streaming_response.stop( + "chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + typing = response.parse() + assert_matches_type(TypingResponse, typing, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_stop(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.chats.typing.with_raw_response.stop( + "", + ) + + +class TestAsyncTyping: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_start(self, async_client: AsyncBlooio) -> None: + typing = await async_client.chats.typing.start( + "chatId", + ) + assert_matches_type(TypingResponse, typing, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_start(self, async_client: AsyncBlooio) -> None: + response = await async_client.chats.typing.with_raw_response.start( + "chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + typing = await response.parse() + assert_matches_type(TypingResponse, typing, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_start(self, async_client: AsyncBlooio) -> None: + async with async_client.chats.typing.with_streaming_response.start( + "chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + typing = await response.parse() + assert_matches_type(TypingResponse, typing, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_start(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.chats.typing.with_raw_response.start( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_stop(self, async_client: AsyncBlooio) -> None: + typing = await async_client.chats.typing.stop( + "chatId", + ) + assert_matches_type(TypingResponse, typing, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_stop(self, async_client: AsyncBlooio) -> None: + response = await async_client.chats.typing.with_raw_response.stop( + "chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + typing = await response.parse() + assert_matches_type(TypingResponse, typing, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_stop(self, async_client: AsyncBlooio) -> None: + async with async_client.chats.typing.with_streaming_response.stop( + "chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + typing = await response.parse() + assert_matches_type(TypingResponse, typing, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_stop(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.chats.typing.with_raw_response.stop( + "", + ) diff --git a/tests/api_resources/config/test_webhook.py b/tests/api_resources/config/test_webhook.py deleted file mode 100644 index 64632f9..0000000 --- a/tests/api_resources/config/test_webhook.py +++ /dev/null @@ -1,148 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from blooio import Blooio, AsyncBlooio -from tests.utils import assert_matches_type -from blooio.types.config import WebhookUpdateResponse, WebhookRetrieveResponse - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestWebhook: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_retrieve(self, client: Blooio) -> None: - webhook = client.config.webhook.retrieve() - assert_matches_type(WebhookRetrieveResponse, webhook, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_retrieve(self, client: Blooio) -> None: - response = client.config.webhook.with_raw_response.retrieve() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - webhook = response.parse() - assert_matches_type(WebhookRetrieveResponse, webhook, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_retrieve(self, client: Blooio) -> None: - with client.config.webhook.with_streaming_response.retrieve() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - webhook = response.parse() - assert_matches_type(WebhookRetrieveResponse, webhook, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_update(self, client: Blooio) -> None: - webhook = client.config.webhook.update( - webhook_url="https://example.com/mywebhook", - ) - assert_matches_type(WebhookUpdateResponse, webhook, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_update(self, client: Blooio) -> None: - response = client.config.webhook.with_raw_response.update( - webhook_url="https://example.com/mywebhook", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - webhook = response.parse() - assert_matches_type(WebhookUpdateResponse, webhook, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_update(self, client: Blooio) -> None: - with client.config.webhook.with_streaming_response.update( - webhook_url="https://example.com/mywebhook", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - webhook = response.parse() - assert_matches_type(WebhookUpdateResponse, webhook, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncWebhook: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_retrieve(self, async_client: AsyncBlooio) -> None: - webhook = await async_client.config.webhook.retrieve() - assert_matches_type(WebhookRetrieveResponse, webhook, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncBlooio) -> None: - response = await async_client.config.webhook.with_raw_response.retrieve() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - webhook = await response.parse() - assert_matches_type(WebhookRetrieveResponse, webhook, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncBlooio) -> None: - async with async_client.config.webhook.with_streaming_response.retrieve() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - webhook = await response.parse() - assert_matches_type(WebhookRetrieveResponse, webhook, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_update(self, async_client: AsyncBlooio) -> None: - webhook = await async_client.config.webhook.update( - webhook_url="https://example.com/mywebhook", - ) - assert_matches_type(WebhookUpdateResponse, webhook, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_update(self, async_client: AsyncBlooio) -> None: - response = await async_client.config.webhook.with_raw_response.update( - webhook_url="https://example.com/mywebhook", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - webhook = await response.parse() - assert_matches_type(WebhookUpdateResponse, webhook, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_update(self, async_client: AsyncBlooio) -> None: - async with async_client.config.webhook.with_streaming_response.update( - webhook_url="https://example.com/mywebhook", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - webhook = await response.parse() - assert_matches_type(WebhookUpdateResponse, webhook, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/contacts/__init__.py b/tests/api_resources/contacts/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/contacts/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/contacts/test_tags.py b/tests/api_resources/contacts/test_tags.py new file mode 100644 index 0000000..e90924b --- /dev/null +++ b/tests/api_resources/contacts/test_tags.py @@ -0,0 +1,305 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from blooio import Blooio, AsyncBlooio +from tests.utils import assert_matches_type +from blooio.types import DeleteResponse +from blooio.types.contacts import TagAddResponse, TagListResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestTags: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: Blooio) -> None: + tag = client.contacts.tags.list( + "%2B15551234567", + ) + assert_matches_type(TagListResponse, tag, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: Blooio) -> None: + response = client.contacts.tags.with_raw_response.list( + "%2B15551234567", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + tag = response.parse() + assert_matches_type(TagListResponse, tag, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Blooio) -> None: + with client.contacts.tags.with_streaming_response.list( + "%2B15551234567", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + tag = response.parse() + assert_matches_type(TagListResponse, tag, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_list(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `contact_id` but received ''"): + client.contacts.tags.with_raw_response.list( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_add(self, client: Blooio) -> None: + tag = client.contacts.tags.add( + contact_id="%2B15551234567", + tags=["vip", "priority"], + ) + assert_matches_type(TagAddResponse, tag, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_add(self, client: Blooio) -> None: + response = client.contacts.tags.with_raw_response.add( + contact_id="%2B15551234567", + tags=["vip", "priority"], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + tag = response.parse() + assert_matches_type(TagAddResponse, tag, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_add(self, client: Blooio) -> None: + with client.contacts.tags.with_streaming_response.add( + contact_id="%2B15551234567", + tags=["vip", "priority"], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + tag = response.parse() + assert_matches_type(TagAddResponse, tag, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_add(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `contact_id` but received ''"): + client.contacts.tags.with_raw_response.add( + contact_id="", + tags=["vip", "priority"], + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_remove(self, client: Blooio) -> None: + tag = client.contacts.tags.remove( + tag="vip", + contact_id="%2B15551234567", + ) + assert_matches_type(DeleteResponse, tag, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_remove(self, client: Blooio) -> None: + response = client.contacts.tags.with_raw_response.remove( + tag="vip", + contact_id="%2B15551234567", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + tag = response.parse() + assert_matches_type(DeleteResponse, tag, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_remove(self, client: Blooio) -> None: + with client.contacts.tags.with_streaming_response.remove( + tag="vip", + contact_id="%2B15551234567", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + tag = response.parse() + assert_matches_type(DeleteResponse, tag, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_remove(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `contact_id` but received ''"): + client.contacts.tags.with_raw_response.remove( + tag="vip", + contact_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `tag` but received ''"): + client.contacts.tags.with_raw_response.remove( + tag="", + contact_id="%2B15551234567", + ) + + +class TestAsyncTags: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncBlooio) -> None: + tag = await async_client.contacts.tags.list( + "%2B15551234567", + ) + assert_matches_type(TagListResponse, tag, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncBlooio) -> None: + response = await async_client.contacts.tags.with_raw_response.list( + "%2B15551234567", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + tag = await response.parse() + assert_matches_type(TagListResponse, tag, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncBlooio) -> None: + async with async_client.contacts.tags.with_streaming_response.list( + "%2B15551234567", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + tag = await response.parse() + assert_matches_type(TagListResponse, tag, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_list(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `contact_id` but received ''"): + await async_client.contacts.tags.with_raw_response.list( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_add(self, async_client: AsyncBlooio) -> None: + tag = await async_client.contacts.tags.add( + contact_id="%2B15551234567", + tags=["vip", "priority"], + ) + assert_matches_type(TagAddResponse, tag, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_add(self, async_client: AsyncBlooio) -> None: + response = await async_client.contacts.tags.with_raw_response.add( + contact_id="%2B15551234567", + tags=["vip", "priority"], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + tag = await response.parse() + assert_matches_type(TagAddResponse, tag, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_add(self, async_client: AsyncBlooio) -> None: + async with async_client.contacts.tags.with_streaming_response.add( + contact_id="%2B15551234567", + tags=["vip", "priority"], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + tag = await response.parse() + assert_matches_type(TagAddResponse, tag, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_add(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `contact_id` but received ''"): + await async_client.contacts.tags.with_raw_response.add( + contact_id="", + tags=["vip", "priority"], + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_remove(self, async_client: AsyncBlooio) -> None: + tag = await async_client.contacts.tags.remove( + tag="vip", + contact_id="%2B15551234567", + ) + assert_matches_type(DeleteResponse, tag, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_remove(self, async_client: AsyncBlooio) -> None: + response = await async_client.contacts.tags.with_raw_response.remove( + tag="vip", + contact_id="%2B15551234567", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + tag = await response.parse() + assert_matches_type(DeleteResponse, tag, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_remove(self, async_client: AsyncBlooio) -> None: + async with async_client.contacts.tags.with_streaming_response.remove( + tag="vip", + contact_id="%2B15551234567", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + tag = await response.parse() + assert_matches_type(DeleteResponse, tag, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_remove(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `contact_id` but received ''"): + await async_client.contacts.tags.with_raw_response.remove( + tag="vip", + contact_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `tag` but received ''"): + await async_client.contacts.tags.with_raw_response.remove( + tag="", + contact_id="%2B15551234567", + ) diff --git a/tests/api_resources/groups/__init__.py b/tests/api_resources/groups/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/groups/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/groups/test_icon.py b/tests/api_resources/groups/test_icon.py new file mode 100644 index 0000000..39b057d --- /dev/null +++ b/tests/api_resources/groups/test_icon.py @@ -0,0 +1,200 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from blooio import Blooio, AsyncBlooio +from tests.utils import assert_matches_type +from blooio.types.groups import GroupIcon + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestIcon: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_remove(self, client: Blooio) -> None: + icon = client.groups.icon.remove( + "grp_abc123def456", + ) + assert_matches_type(GroupIcon, icon, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_remove(self, client: Blooio) -> None: + response = client.groups.icon.with_raw_response.remove( + "grp_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + icon = response.parse() + assert_matches_type(GroupIcon, icon, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_remove(self, client: Blooio) -> None: + with client.groups.icon.with_streaming_response.remove( + "grp_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + icon = response.parse() + assert_matches_type(GroupIcon, icon, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_remove(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + client.groups.icon.with_raw_response.remove( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_set(self, client: Blooio) -> None: + icon = client.groups.icon.set( + group_id="grp_abc123def456", + icon=b"Example data", + ) + assert_matches_type(GroupIcon, icon, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_set(self, client: Blooio) -> None: + response = client.groups.icon.with_raw_response.set( + group_id="grp_abc123def456", + icon=b"Example data", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + icon = response.parse() + assert_matches_type(GroupIcon, icon, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_set(self, client: Blooio) -> None: + with client.groups.icon.with_streaming_response.set( + group_id="grp_abc123def456", + icon=b"Example data", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + icon = response.parse() + assert_matches_type(GroupIcon, icon, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_set(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + client.groups.icon.with_raw_response.set( + group_id="", + icon=b"Example data", + ) + + +class TestAsyncIcon: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_remove(self, async_client: AsyncBlooio) -> None: + icon = await async_client.groups.icon.remove( + "grp_abc123def456", + ) + assert_matches_type(GroupIcon, icon, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_remove(self, async_client: AsyncBlooio) -> None: + response = await async_client.groups.icon.with_raw_response.remove( + "grp_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + icon = await response.parse() + assert_matches_type(GroupIcon, icon, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_remove(self, async_client: AsyncBlooio) -> None: + async with async_client.groups.icon.with_streaming_response.remove( + "grp_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + icon = await response.parse() + assert_matches_type(GroupIcon, icon, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_remove(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + await async_client.groups.icon.with_raw_response.remove( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_set(self, async_client: AsyncBlooio) -> None: + icon = await async_client.groups.icon.set( + group_id="grp_abc123def456", + icon=b"Example data", + ) + assert_matches_type(GroupIcon, icon, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_set(self, async_client: AsyncBlooio) -> None: + response = await async_client.groups.icon.with_raw_response.set( + group_id="grp_abc123def456", + icon=b"Example data", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + icon = await response.parse() + assert_matches_type(GroupIcon, icon, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_set(self, async_client: AsyncBlooio) -> None: + async with async_client.groups.icon.with_streaming_response.set( + group_id="grp_abc123def456", + icon=b"Example data", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + icon = await response.parse() + assert_matches_type(GroupIcon, icon, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_set(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + await async_client.groups.icon.with_raw_response.set( + group_id="", + icon=b"Example data", + ) diff --git a/tests/api_resources/groups/test_members.py b/tests/api_resources/groups/test_members.py new file mode 100644 index 0000000..8ccc6ba --- /dev/null +++ b/tests/api_resources/groups/test_members.py @@ -0,0 +1,328 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from blooio import Blooio, AsyncBlooio +from tests.utils import assert_matches_type +from blooio.types.groups import ( + MemberAddResponse, + MemberListResponse, + MemberRemoveResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestMembers: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: Blooio) -> None: + member = client.groups.members.list( + group_id="grp_abc123def456", + ) + assert_matches_type(MemberListResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Blooio) -> None: + member = client.groups.members.list( + group_id="grp_abc123def456", + limit=1, + offset=0, + ) + assert_matches_type(MemberListResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: Blooio) -> None: + response = client.groups.members.with_raw_response.list( + group_id="grp_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = response.parse() + assert_matches_type(MemberListResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Blooio) -> None: + with client.groups.members.with_streaming_response.list( + group_id="grp_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = response.parse() + assert_matches_type(MemberListResponse, member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_list(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + client.groups.members.with_raw_response.list( + group_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_add(self, client: Blooio) -> None: + member = client.groups.members.add( + group_id="grp_abc123def456", + contact_id="+15551234567", + ) + assert_matches_type(MemberAddResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_add(self, client: Blooio) -> None: + response = client.groups.members.with_raw_response.add( + group_id="grp_abc123def456", + contact_id="+15551234567", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = response.parse() + assert_matches_type(MemberAddResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_add(self, client: Blooio) -> None: + with client.groups.members.with_streaming_response.add( + group_id="grp_abc123def456", + contact_id="+15551234567", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = response.parse() + assert_matches_type(MemberAddResponse, member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_add(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + client.groups.members.with_raw_response.add( + group_id="", + contact_id="+15551234567", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_remove(self, client: Blooio) -> None: + member = client.groups.members.remove( + contact_id="%2B15551234567", + group_id="grp_abc123def456", + ) + assert_matches_type(MemberRemoveResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_remove(self, client: Blooio) -> None: + response = client.groups.members.with_raw_response.remove( + contact_id="%2B15551234567", + group_id="grp_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = response.parse() + assert_matches_type(MemberRemoveResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_remove(self, client: Blooio) -> None: + with client.groups.members.with_streaming_response.remove( + contact_id="%2B15551234567", + group_id="grp_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = response.parse() + assert_matches_type(MemberRemoveResponse, member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_remove(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + client.groups.members.with_raw_response.remove( + contact_id="%2B15551234567", + group_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `contact_id` but received ''"): + client.groups.members.with_raw_response.remove( + contact_id="", + group_id="grp_abc123def456", + ) + + +class TestAsyncMembers: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncBlooio) -> None: + member = await async_client.groups.members.list( + group_id="grp_abc123def456", + ) + assert_matches_type(MemberListResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncBlooio) -> None: + member = await async_client.groups.members.list( + group_id="grp_abc123def456", + limit=1, + offset=0, + ) + assert_matches_type(MemberListResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncBlooio) -> None: + response = await async_client.groups.members.with_raw_response.list( + group_id="grp_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = await response.parse() + assert_matches_type(MemberListResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncBlooio) -> None: + async with async_client.groups.members.with_streaming_response.list( + group_id="grp_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = await response.parse() + assert_matches_type(MemberListResponse, member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_list(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + await async_client.groups.members.with_raw_response.list( + group_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_add(self, async_client: AsyncBlooio) -> None: + member = await async_client.groups.members.add( + group_id="grp_abc123def456", + contact_id="+15551234567", + ) + assert_matches_type(MemberAddResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_add(self, async_client: AsyncBlooio) -> None: + response = await async_client.groups.members.with_raw_response.add( + group_id="grp_abc123def456", + contact_id="+15551234567", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = await response.parse() + assert_matches_type(MemberAddResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_add(self, async_client: AsyncBlooio) -> None: + async with async_client.groups.members.with_streaming_response.add( + group_id="grp_abc123def456", + contact_id="+15551234567", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = await response.parse() + assert_matches_type(MemberAddResponse, member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_add(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + await async_client.groups.members.with_raw_response.add( + group_id="", + contact_id="+15551234567", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_remove(self, async_client: AsyncBlooio) -> None: + member = await async_client.groups.members.remove( + contact_id="%2B15551234567", + group_id="grp_abc123def456", + ) + assert_matches_type(MemberRemoveResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_remove(self, async_client: AsyncBlooio) -> None: + response = await async_client.groups.members.with_raw_response.remove( + contact_id="%2B15551234567", + group_id="grp_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = await response.parse() + assert_matches_type(MemberRemoveResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_remove(self, async_client: AsyncBlooio) -> None: + async with async_client.groups.members.with_streaming_response.remove( + contact_id="%2B15551234567", + group_id="grp_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = await response.parse() + assert_matches_type(MemberRemoveResponse, member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_remove(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + await async_client.groups.members.with_raw_response.remove( + contact_id="%2B15551234567", + group_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `contact_id` but received ''"): + await async_client.groups.members.with_raw_response.remove( + contact_id="", + group_id="grp_abc123def456", + ) diff --git a/tests/api_resources/location/__init__.py b/tests/api_resources/location/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/location/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/location/test_contacts.py b/tests/api_resources/location/test_contacts.py new file mode 100644 index 0000000..d6afc79 --- /dev/null +++ b/tests/api_resources/location/test_contacts.py @@ -0,0 +1,220 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from blooio import Blooio, AsyncBlooio +from tests.utils import assert_matches_type +from blooio.types.location import ContactLocation, ContactListResponse, ContactRefreshResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestContacts: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: Blooio) -> None: + contact = client.location.contacts.retrieve( + "handle", + ) + assert_matches_type(ContactLocation, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Blooio) -> None: + response = client.location.contacts.with_raw_response.retrieve( + "handle", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact = response.parse() + assert_matches_type(ContactLocation, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Blooio) -> None: + with client.location.contacts.with_streaming_response.retrieve( + "handle", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact = response.parse() + assert_matches_type(ContactLocation, contact, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `handle` but received ''"): + client.location.contacts.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: Blooio) -> None: + contact = client.location.contacts.list() + assert_matches_type(ContactListResponse, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: Blooio) -> None: + response = client.location.contacts.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact = response.parse() + assert_matches_type(ContactListResponse, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Blooio) -> None: + with client.location.contacts.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact = response.parse() + assert_matches_type(ContactListResponse, contact, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_refresh(self, client: Blooio) -> None: + contact = client.location.contacts.refresh() + assert_matches_type(ContactRefreshResponse, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_refresh(self, client: Blooio) -> None: + response = client.location.contacts.with_raw_response.refresh() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact = response.parse() + assert_matches_type(ContactRefreshResponse, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_refresh(self, client: Blooio) -> None: + with client.location.contacts.with_streaming_response.refresh() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact = response.parse() + assert_matches_type(ContactRefreshResponse, contact, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncContacts: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncBlooio) -> None: + contact = await async_client.location.contacts.retrieve( + "handle", + ) + assert_matches_type(ContactLocation, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncBlooio) -> None: + response = await async_client.location.contacts.with_raw_response.retrieve( + "handle", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact = await response.parse() + assert_matches_type(ContactLocation, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncBlooio) -> None: + async with async_client.location.contacts.with_streaming_response.retrieve( + "handle", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact = await response.parse() + assert_matches_type(ContactLocation, contact, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `handle` but received ''"): + await async_client.location.contacts.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncBlooio) -> None: + contact = await async_client.location.contacts.list() + assert_matches_type(ContactListResponse, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncBlooio) -> None: + response = await async_client.location.contacts.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact = await response.parse() + assert_matches_type(ContactListResponse, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncBlooio) -> None: + async with async_client.location.contacts.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact = await response.parse() + assert_matches_type(ContactListResponse, contact, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_refresh(self, async_client: AsyncBlooio) -> None: + contact = await async_client.location.contacts.refresh() + assert_matches_type(ContactRefreshResponse, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_refresh(self, async_client: AsyncBlooio) -> None: + response = await async_client.location.contacts.with_raw_response.refresh() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact = await response.parse() + assert_matches_type(ContactRefreshResponse, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_refresh(self, async_client: AsyncBlooio) -> None: + async with async_client.location.contacts.with_streaming_response.refresh() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact = await response.parse() + assert_matches_type(ContactRefreshResponse, contact, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/me/__init__.py b/tests/api_resources/me/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/me/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/me/numbers/__init__.py b/tests/api_resources/me/numbers/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/me/numbers/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/me/numbers/test_contact_card.py b/tests/api_resources/me/numbers/test_contact_card.py new file mode 100644 index 0000000..1c5557e --- /dev/null +++ b/tests/api_resources/me/numbers/test_contact_card.py @@ -0,0 +1,224 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from blooio import Blooio, AsyncBlooio +from tests.utils import assert_matches_type +from blooio.types.me.numbers import ContactCardUpdateResponse, ContactCardRetrieveResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestContactCard: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: Blooio) -> None: + contact_card = client.me.numbers.contact_card.retrieve( + "number", + ) + assert_matches_type(ContactCardRetrieveResponse, contact_card, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Blooio) -> None: + response = client.me.numbers.contact_card.with_raw_response.retrieve( + "number", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact_card = response.parse() + assert_matches_type(ContactCardRetrieveResponse, contact_card, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Blooio) -> None: + with client.me.numbers.contact_card.with_streaming_response.retrieve( + "number", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact_card = response.parse() + assert_matches_type(ContactCardRetrieveResponse, contact_card, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `number` but received ''"): + client.me.numbers.contact_card.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update(self, client: Blooio) -> None: + contact_card = client.me.numbers.contact_card.update( + number="number", + ) + assert_matches_type(ContactCardUpdateResponse, contact_card, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: Blooio) -> None: + contact_card = client.me.numbers.contact_card.update( + number="number", + avatar="avatar", + first_name="first_name", + last_name="last_name", + sharing={ + "audience": 0, + "enabled": True, + "name_format": 0, + }, + ) + assert_matches_type(ContactCardUpdateResponse, contact_card, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update(self, client: Blooio) -> None: + response = client.me.numbers.contact_card.with_raw_response.update( + number="number", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact_card = response.parse() + assert_matches_type(ContactCardUpdateResponse, contact_card, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update(self, client: Blooio) -> None: + with client.me.numbers.contact_card.with_streaming_response.update( + number="number", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact_card = response.parse() + assert_matches_type(ContactCardUpdateResponse, contact_card, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_update(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `number` but received ''"): + client.me.numbers.contact_card.with_raw_response.update( + number="", + ) + + +class TestAsyncContactCard: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncBlooio) -> None: + contact_card = await async_client.me.numbers.contact_card.retrieve( + "number", + ) + assert_matches_type(ContactCardRetrieveResponse, contact_card, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncBlooio) -> None: + response = await async_client.me.numbers.contact_card.with_raw_response.retrieve( + "number", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact_card = await response.parse() + assert_matches_type(ContactCardRetrieveResponse, contact_card, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncBlooio) -> None: + async with async_client.me.numbers.contact_card.with_streaming_response.retrieve( + "number", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact_card = await response.parse() + assert_matches_type(ContactCardRetrieveResponse, contact_card, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `number` but received ''"): + await async_client.me.numbers.contact_card.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncBlooio) -> None: + contact_card = await async_client.me.numbers.contact_card.update( + number="number", + ) + assert_matches_type(ContactCardUpdateResponse, contact_card, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncBlooio) -> None: + contact_card = await async_client.me.numbers.contact_card.update( + number="number", + avatar="avatar", + first_name="first_name", + last_name="last_name", + sharing={ + "audience": 0, + "enabled": True, + "name_format": 0, + }, + ) + assert_matches_type(ContactCardUpdateResponse, contact_card, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncBlooio) -> None: + response = await async_client.me.numbers.contact_card.with_raw_response.update( + number="number", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact_card = await response.parse() + assert_matches_type(ContactCardUpdateResponse, contact_card, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncBlooio) -> None: + async with async_client.me.numbers.contact_card.with_streaming_response.update( + number="number", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact_card = await response.parse() + assert_matches_type(ContactCardUpdateResponse, contact_card, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_update(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `number` but received ''"): + await async_client.me.numbers.contact_card.with_raw_response.update( + number="", + ) diff --git a/tests/api_resources/me/test_numbers.py b/tests/api_resources/me/test_numbers.py new file mode 100644 index 0000000..79ee262 --- /dev/null +++ b/tests/api_resources/me/test_numbers.py @@ -0,0 +1,80 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from blooio import Blooio, AsyncBlooio +from tests.utils import assert_matches_type +from blooio.types.me import NumberListResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestNumbers: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: Blooio) -> None: + number = client.me.numbers.list() + assert_matches_type(NumberListResponse, number, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: Blooio) -> None: + response = client.me.numbers.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + number = response.parse() + assert_matches_type(NumberListResponse, number, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Blooio) -> None: + with client.me.numbers.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + number = response.parse() + assert_matches_type(NumberListResponse, number, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncNumbers: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncBlooio) -> None: + number = await async_client.me.numbers.list() + assert_matches_type(NumberListResponse, number, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncBlooio) -> None: + response = await async_client.me.numbers.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + number = await response.parse() + assert_matches_type(NumberListResponse, number, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncBlooio) -> None: + async with async_client.me.numbers.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + number = await response.parse() + assert_matches_type(NumberListResponse, number, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/phone_numbers/__init__.py b/tests/api_resources/phone_numbers/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/phone_numbers/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/phone_numbers/test_lookup.py b/tests/api_resources/phone_numbers/test_lookup.py new file mode 100644 index 0000000..2f91740 --- /dev/null +++ b/tests/api_resources/phone_numbers/test_lookup.py @@ -0,0 +1,160 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from blooio import Blooio, AsyncBlooio +from tests.utils import assert_matches_type +from blooio.types.phone_numbers import PhoneNumberLookupResult + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestLookup: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create(self, client: Blooio) -> None: + lookup = client.phone_numbers.lookup.create( + number="+12125551234", + ) + assert_matches_type(PhoneNumberLookupResult, lookup, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create(self, client: Blooio) -> None: + response = client.phone_numbers.lookup.with_raw_response.create( + number="+12125551234", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + lookup = response.parse() + assert_matches_type(PhoneNumberLookupResult, lookup, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create(self, client: Blooio) -> None: + with client.phone_numbers.lookup.with_streaming_response.create( + number="+12125551234", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + lookup = response.parse() + assert_matches_type(PhoneNumberLookupResult, lookup, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: Blooio) -> None: + lookup = client.phone_numbers.lookup.retrieve( + number="+12125551234", + ) + assert_matches_type(PhoneNumberLookupResult, lookup, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Blooio) -> None: + response = client.phone_numbers.lookup.with_raw_response.retrieve( + number="+12125551234", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + lookup = response.parse() + assert_matches_type(PhoneNumberLookupResult, lookup, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Blooio) -> None: + with client.phone_numbers.lookup.with_streaming_response.retrieve( + number="+12125551234", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + lookup = response.parse() + assert_matches_type(PhoneNumberLookupResult, lookup, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncLookup: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncBlooio) -> None: + lookup = await async_client.phone_numbers.lookup.create( + number="+12125551234", + ) + assert_matches_type(PhoneNumberLookupResult, lookup, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncBlooio) -> None: + response = await async_client.phone_numbers.lookup.with_raw_response.create( + number="+12125551234", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + lookup = await response.parse() + assert_matches_type(PhoneNumberLookupResult, lookup, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncBlooio) -> None: + async with async_client.phone_numbers.lookup.with_streaming_response.create( + number="+12125551234", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + lookup = await response.parse() + assert_matches_type(PhoneNumberLookupResult, lookup, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncBlooio) -> None: + lookup = await async_client.phone_numbers.lookup.retrieve( + number="+12125551234", + ) + assert_matches_type(PhoneNumberLookupResult, lookup, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncBlooio) -> None: + response = await async_client.phone_numbers.lookup.with_raw_response.retrieve( + number="+12125551234", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + lookup = await response.parse() + assert_matches_type(PhoneNumberLookupResult, lookup, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncBlooio) -> None: + async with async_client.phone_numbers.lookup.with_streaming_response.retrieve( + number="+12125551234", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + lookup = await response.parse() + assert_matches_type(PhoneNumberLookupResult, lookup, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_batches.py b/tests/api_resources/test_batches.py deleted file mode 100644 index 78c8c35..0000000 --- a/tests/api_resources/test_batches.py +++ /dev/null @@ -1,330 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from blooio import Blooio, AsyncBlooio - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestBatches: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_create(self, client: Blooio) -> None: - batch = client.batches.create() - assert batch is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_create(self, client: Blooio) -> None: - response = client.batches.with_raw_response.create() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - batch = response.parse() - assert batch is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_create(self, client: Blooio) -> None: - with client.batches.with_streaming_response.create() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - batch = response.parse() - assert batch is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_retrieve(self, client: Blooio) -> None: - batch = client.batches.retrieve( - "batchId", - ) - assert batch is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_retrieve(self, client: Blooio) -> None: - response = client.batches.with_raw_response.retrieve( - "batchId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - batch = response.parse() - assert batch is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_retrieve(self, client: Blooio) -> None: - with client.batches.with_streaming_response.retrieve( - "batchId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - batch = response.parse() - assert batch is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_retrieve(self, client: Blooio) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `batch_id` but received ''"): - client.batches.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_list_messages(self, client: Blooio) -> None: - batch = client.batches.list_messages( - "batchId", - ) - assert batch is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_list_messages(self, client: Blooio) -> None: - response = client.batches.with_raw_response.list_messages( - "batchId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - batch = response.parse() - assert batch is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_list_messages(self, client: Blooio) -> None: - with client.batches.with_streaming_response.list_messages( - "batchId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - batch = response.parse() - assert batch is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_list_messages(self, client: Blooio) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `batch_id` but received ''"): - client.batches.with_raw_response.list_messages( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_retrieve_status(self, client: Blooio) -> None: - batch = client.batches.retrieve_status( - "batchId", - ) - assert batch is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_retrieve_status(self, client: Blooio) -> None: - response = client.batches.with_raw_response.retrieve_status( - "batchId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - batch = response.parse() - assert batch is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_retrieve_status(self, client: Blooio) -> None: - with client.batches.with_streaming_response.retrieve_status( - "batchId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - batch = response.parse() - assert batch is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_retrieve_status(self, client: Blooio) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `batch_id` but received ''"): - client.batches.with_raw_response.retrieve_status( - "", - ) - - -class TestAsyncBatches: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_create(self, async_client: AsyncBlooio) -> None: - batch = await async_client.batches.create() - assert batch is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_create(self, async_client: AsyncBlooio) -> None: - response = await async_client.batches.with_raw_response.create() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - batch = await response.parse() - assert batch is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_create(self, async_client: AsyncBlooio) -> None: - async with async_client.batches.with_streaming_response.create() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - batch = await response.parse() - assert batch is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_retrieve(self, async_client: AsyncBlooio) -> None: - batch = await async_client.batches.retrieve( - "batchId", - ) - assert batch is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncBlooio) -> None: - response = await async_client.batches.with_raw_response.retrieve( - "batchId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - batch = await response.parse() - assert batch is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncBlooio) -> None: - async with async_client.batches.with_streaming_response.retrieve( - "batchId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - batch = await response.parse() - assert batch is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_retrieve(self, async_client: AsyncBlooio) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `batch_id` but received ''"): - await async_client.batches.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_list_messages(self, async_client: AsyncBlooio) -> None: - batch = await async_client.batches.list_messages( - "batchId", - ) - assert batch is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_list_messages(self, async_client: AsyncBlooio) -> None: - response = await async_client.batches.with_raw_response.list_messages( - "batchId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - batch = await response.parse() - assert batch is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_list_messages(self, async_client: AsyncBlooio) -> None: - async with async_client.batches.with_streaming_response.list_messages( - "batchId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - batch = await response.parse() - assert batch is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_list_messages(self, async_client: AsyncBlooio) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `batch_id` but received ''"): - await async_client.batches.with_raw_response.list_messages( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_retrieve_status(self, async_client: AsyncBlooio) -> None: - batch = await async_client.batches.retrieve_status( - "batchId", - ) - assert batch is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_retrieve_status(self, async_client: AsyncBlooio) -> None: - response = await async_client.batches.with_raw_response.retrieve_status( - "batchId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - batch = await response.parse() - assert batch is None - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_retrieve_status(self, async_client: AsyncBlooio) -> None: - async with async_client.batches.with_streaming_response.retrieve_status( - "batchId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - batch = await response.parse() - assert batch is None - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_retrieve_status(self, async_client: AsyncBlooio) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `batch_id` but received ''"): - await async_client.batches.with_raw_response.retrieve_status( - "", - ) diff --git a/tests/api_resources/test_chats.py b/tests/api_resources/test_chats.py new file mode 100644 index 0000000..55da4a4 --- /dev/null +++ b/tests/api_resources/test_chats.py @@ -0,0 +1,359 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from blooio import Blooio, AsyncBlooio +from tests.utils import assert_matches_type +from blooio.types import ( + ChatListResponse, + ChatRetrieveResponse, + ChatMarkAsReadResponse, + ChatShareContactCardResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestChats: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: Blooio) -> None: + chat = client.chats.retrieve( + "chatId", + ) + assert_matches_type(ChatRetrieveResponse, chat, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Blooio) -> None: + response = client.chats.with_raw_response.retrieve( + "chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + chat = response.parse() + assert_matches_type(ChatRetrieveResponse, chat, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Blooio) -> None: + with client.chats.with_streaming_response.retrieve( + "chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + chat = response.parse() + assert_matches_type(ChatRetrieveResponse, chat, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.chats.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: Blooio) -> None: + chat = client.chats.list() + assert_matches_type(ChatListResponse, chat, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Blooio) -> None: + chat = client.chats.list( + limit=1, + offset=0, + q="q", + sort="recent", + ) + assert_matches_type(ChatListResponse, chat, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: Blooio) -> None: + response = client.chats.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + chat = response.parse() + assert_matches_type(ChatListResponse, chat, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Blooio) -> None: + with client.chats.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + chat = response.parse() + assert_matches_type(ChatListResponse, chat, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_mark_as_read(self, client: Blooio) -> None: + chat = client.chats.mark_as_read( + "chatId", + ) + assert_matches_type(ChatMarkAsReadResponse, chat, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_mark_as_read(self, client: Blooio) -> None: + response = client.chats.with_raw_response.mark_as_read( + "chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + chat = response.parse() + assert_matches_type(ChatMarkAsReadResponse, chat, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_mark_as_read(self, client: Blooio) -> None: + with client.chats.with_streaming_response.mark_as_read( + "chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + chat = response.parse() + assert_matches_type(ChatMarkAsReadResponse, chat, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_mark_as_read(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.chats.with_raw_response.mark_as_read( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_share_contact_card(self, client: Blooio) -> None: + chat = client.chats.share_contact_card( + "chatId", + ) + assert_matches_type(ChatShareContactCardResponse, chat, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_share_contact_card(self, client: Blooio) -> None: + response = client.chats.with_raw_response.share_contact_card( + "chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + chat = response.parse() + assert_matches_type(ChatShareContactCardResponse, chat, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_share_contact_card(self, client: Blooio) -> None: + with client.chats.with_streaming_response.share_contact_card( + "chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + chat = response.parse() + assert_matches_type(ChatShareContactCardResponse, chat, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_share_contact_card(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + client.chats.with_raw_response.share_contact_card( + "", + ) + + +class TestAsyncChats: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncBlooio) -> None: + chat = await async_client.chats.retrieve( + "chatId", + ) + assert_matches_type(ChatRetrieveResponse, chat, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncBlooio) -> None: + response = await async_client.chats.with_raw_response.retrieve( + "chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + chat = await response.parse() + assert_matches_type(ChatRetrieveResponse, chat, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncBlooio) -> None: + async with async_client.chats.with_streaming_response.retrieve( + "chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + chat = await response.parse() + assert_matches_type(ChatRetrieveResponse, chat, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.chats.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncBlooio) -> None: + chat = await async_client.chats.list() + assert_matches_type(ChatListResponse, chat, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncBlooio) -> None: + chat = await async_client.chats.list( + limit=1, + offset=0, + q="q", + sort="recent", + ) + assert_matches_type(ChatListResponse, chat, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncBlooio) -> None: + response = await async_client.chats.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + chat = await response.parse() + assert_matches_type(ChatListResponse, chat, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncBlooio) -> None: + async with async_client.chats.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + chat = await response.parse() + assert_matches_type(ChatListResponse, chat, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_mark_as_read(self, async_client: AsyncBlooio) -> None: + chat = await async_client.chats.mark_as_read( + "chatId", + ) + assert_matches_type(ChatMarkAsReadResponse, chat, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_mark_as_read(self, async_client: AsyncBlooio) -> None: + response = await async_client.chats.with_raw_response.mark_as_read( + "chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + chat = await response.parse() + assert_matches_type(ChatMarkAsReadResponse, chat, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_mark_as_read(self, async_client: AsyncBlooio) -> None: + async with async_client.chats.with_streaming_response.mark_as_read( + "chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + chat = await response.parse() + assert_matches_type(ChatMarkAsReadResponse, chat, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_mark_as_read(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.chats.with_raw_response.mark_as_read( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_share_contact_card(self, async_client: AsyncBlooio) -> None: + chat = await async_client.chats.share_contact_card( + "chatId", + ) + assert_matches_type(ChatShareContactCardResponse, chat, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_share_contact_card(self, async_client: AsyncBlooio) -> None: + response = await async_client.chats.with_raw_response.share_contact_card( + "chatId", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + chat = await response.parse() + assert_matches_type(ChatShareContactCardResponse, chat, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_share_contact_card(self, async_client: AsyncBlooio) -> None: + async with async_client.chats.with_streaming_response.share_contact_card( + "chatId", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + chat = await response.parse() + assert_matches_type(ChatShareContactCardResponse, chat, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_share_contact_card(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `chat_id` but received ''"): + await async_client.chats.with_raw_response.share_contact_card( + "", + ) diff --git a/tests/api_resources/test_contacts.py b/tests/api_resources/test_contacts.py index 1a70b94..6157271 100644 --- a/tests/api_resources/test_contacts.py +++ b/tests/api_resources/test_contacts.py @@ -9,7 +9,12 @@ from blooio import Blooio, AsyncBlooio from tests.utils import assert_matches_type -from blooio.types import ContactCheckCapabilitiesResponse +from blooio.types import ( + Contact, + DeleteResponse, + ContactListResponse, + ContactCheckCapabilitiesResponse, +) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -17,19 +22,236 @@ class TestContacts: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create(self, client: Blooio) -> None: + contact = client.contacts.create( + identifier="+15551234567", + ) + assert_matches_type(Contact, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: Blooio) -> None: + contact = client.contacts.create( + identifier="+15551234567", + name="John Doe", + ) + assert_matches_type(Contact, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create(self, client: Blooio) -> None: + response = client.contacts.with_raw_response.create( + identifier="+15551234567", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact = response.parse() + assert_matches_type(Contact, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create(self, client: Blooio) -> None: + with client.contacts.with_streaming_response.create( + identifier="+15551234567", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact = response.parse() + assert_matches_type(Contact, contact, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: Blooio) -> None: + contact = client.contacts.retrieve( + "%2B15551234567", + ) + assert_matches_type(Contact, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Blooio) -> None: + response = client.contacts.with_raw_response.retrieve( + "%2B15551234567", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact = response.parse() + assert_matches_type(Contact, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Blooio) -> None: + with client.contacts.with_streaming_response.retrieve( + "%2B15551234567", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact = response.parse() + assert_matches_type(Contact, contact, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `contact_id` but received ''"): + client.contacts.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update(self, client: Blooio) -> None: + contact = client.contacts.update( + contact_id="%2B15551234567", + ) + assert_matches_type(Contact, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: Blooio) -> None: + contact = client.contacts.update( + contact_id="%2B15551234567", + name="Jane Doe", + ) + assert_matches_type(Contact, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update(self, client: Blooio) -> None: + response = client.contacts.with_raw_response.update( + contact_id="%2B15551234567", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact = response.parse() + assert_matches_type(Contact, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update(self, client: Blooio) -> None: + with client.contacts.with_streaming_response.update( + contact_id="%2B15551234567", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact = response.parse() + assert_matches_type(Contact, contact, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_update(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `contact_id` but received ''"): + client.contacts.with_raw_response.update( + contact_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: Blooio) -> None: + contact = client.contacts.list() + assert_matches_type(ContactListResponse, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Blooio) -> None: + contact = client.contacts.list( + limit=1, + offset=0, + q="q", + sort="recent", + ) + assert_matches_type(ContactListResponse, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: Blooio) -> None: + response = client.contacts.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact = response.parse() + assert_matches_type(ContactListResponse, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Blooio) -> None: + with client.contacts.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact = response.parse() + assert_matches_type(ContactListResponse, contact, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_delete(self, client: Blooio) -> None: + contact = client.contacts.delete( + "%2B15551234567", + ) + assert_matches_type(DeleteResponse, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_delete(self, client: Blooio) -> None: + response = client.contacts.with_raw_response.delete( + "%2B15551234567", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact = response.parse() + assert_matches_type(DeleteResponse, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: Blooio) -> None: + with client.contacts.with_streaming_response.delete( + "%2B15551234567", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact = response.parse() + assert_matches_type(DeleteResponse, contact, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_delete(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `contact_id` but received ''"): + client.contacts.with_raw_response.delete( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_check_capabilities(self, client: Blooio) -> None: contact = client.contacts.check_capabilities( - "contact", + "%2B15551234567", ) assert_matches_type(ContactCheckCapabilitiesResponse, contact, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_check_capabilities(self, client: Blooio) -> None: response = client.contacts.with_raw_response.check_capabilities( - "contact", + "%2B15551234567", ) assert response.is_closed is True @@ -37,11 +259,11 @@ def test_raw_response_check_capabilities(self, client: Blooio) -> None: contact = response.parse() assert_matches_type(ContactCheckCapabilitiesResponse, contact, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_check_capabilities(self, client: Blooio) -> None: with client.contacts.with_streaming_response.check_capabilities( - "contact", + "%2B15551234567", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -51,10 +273,10 @@ def test_streaming_response_check_capabilities(self, client: Blooio) -> None: assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_path_params_check_capabilities(self, client: Blooio) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `contact` but received ''"): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `contact_id` but received ''"): client.contacts.with_raw_response.check_capabilities( "", ) @@ -65,19 +287,236 @@ class TestAsyncContacts: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncBlooio) -> None: + contact = await async_client.contacts.create( + identifier="+15551234567", + ) + assert_matches_type(Contact, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncBlooio) -> None: + contact = await async_client.contacts.create( + identifier="+15551234567", + name="John Doe", + ) + assert_matches_type(Contact, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncBlooio) -> None: + response = await async_client.contacts.with_raw_response.create( + identifier="+15551234567", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact = await response.parse() + assert_matches_type(Contact, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncBlooio) -> None: + async with async_client.contacts.with_streaming_response.create( + identifier="+15551234567", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact = await response.parse() + assert_matches_type(Contact, contact, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncBlooio) -> None: + contact = await async_client.contacts.retrieve( + "%2B15551234567", + ) + assert_matches_type(Contact, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncBlooio) -> None: + response = await async_client.contacts.with_raw_response.retrieve( + "%2B15551234567", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact = await response.parse() + assert_matches_type(Contact, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncBlooio) -> None: + async with async_client.contacts.with_streaming_response.retrieve( + "%2B15551234567", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact = await response.parse() + assert_matches_type(Contact, contact, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `contact_id` but received ''"): + await async_client.contacts.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncBlooio) -> None: + contact = await async_client.contacts.update( + contact_id="%2B15551234567", + ) + assert_matches_type(Contact, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncBlooio) -> None: + contact = await async_client.contacts.update( + contact_id="%2B15551234567", + name="Jane Doe", + ) + assert_matches_type(Contact, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncBlooio) -> None: + response = await async_client.contacts.with_raw_response.update( + contact_id="%2B15551234567", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact = await response.parse() + assert_matches_type(Contact, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncBlooio) -> None: + async with async_client.contacts.with_streaming_response.update( + contact_id="%2B15551234567", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact = await response.parse() + assert_matches_type(Contact, contact, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_update(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `contact_id` but received ''"): + await async_client.contacts.with_raw_response.update( + contact_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncBlooio) -> None: + contact = await async_client.contacts.list() + assert_matches_type(ContactListResponse, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncBlooio) -> None: + contact = await async_client.contacts.list( + limit=1, + offset=0, + q="q", + sort="recent", + ) + assert_matches_type(ContactListResponse, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncBlooio) -> None: + response = await async_client.contacts.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact = await response.parse() + assert_matches_type(ContactListResponse, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncBlooio) -> None: + async with async_client.contacts.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact = await response.parse() + assert_matches_type(ContactListResponse, contact, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_delete(self, async_client: AsyncBlooio) -> None: + contact = await async_client.contacts.delete( + "%2B15551234567", + ) + assert_matches_type(DeleteResponse, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_delete(self, async_client: AsyncBlooio) -> None: + response = await async_client.contacts.with_raw_response.delete( + "%2B15551234567", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + contact = await response.parse() + assert_matches_type(DeleteResponse, contact, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncBlooio) -> None: + async with async_client.contacts.with_streaming_response.delete( + "%2B15551234567", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + contact = await response.parse() + assert_matches_type(DeleteResponse, contact, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_delete(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `contact_id` but received ''"): + await async_client.contacts.with_raw_response.delete( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_check_capabilities(self, async_client: AsyncBlooio) -> None: contact = await async_client.contacts.check_capabilities( - "contact", + "%2B15551234567", ) assert_matches_type(ContactCheckCapabilitiesResponse, contact, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_check_capabilities(self, async_client: AsyncBlooio) -> None: response = await async_client.contacts.with_raw_response.check_capabilities( - "contact", + "%2B15551234567", ) assert response.is_closed is True @@ -85,11 +524,11 @@ async def test_raw_response_check_capabilities(self, async_client: AsyncBlooio) contact = await response.parse() assert_matches_type(ContactCheckCapabilitiesResponse, contact, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_check_capabilities(self, async_client: AsyncBlooio) -> None: async with async_client.contacts.with_streaming_response.check_capabilities( - "contact", + "%2B15551234567", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -99,10 +538,10 @@ async def test_streaming_response_check_capabilities(self, async_client: AsyncBl assert cast(Any, response.is_closed) is True - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_path_params_check_capabilities(self, async_client: AsyncBlooio) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `contact` but received ''"): + with pytest.raises(ValueError, match=r"Expected a non-empty value for `contact_id` but received ''"): await async_client.contacts.with_raw_response.check_capabilities( "", ) diff --git a/tests/api_resources/test_facetime.py b/tests/api_resources/test_facetime.py new file mode 100644 index 0000000..3611d91 --- /dev/null +++ b/tests/api_resources/test_facetime.py @@ -0,0 +1,92 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from blooio import Blooio, AsyncBlooio +from tests.utils import assert_matches_type +from blooio.types import FacetimeInitiateCallResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestFacetime: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_initiate_call(self, client: Blooio) -> None: + facetime = client.facetime.initiate_call( + handle="+15551234567", + ) + assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_initiate_call(self, client: Blooio) -> None: + response = client.facetime.with_raw_response.initiate_call( + handle="+15551234567", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + facetime = response.parse() + assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_initiate_call(self, client: Blooio) -> None: + with client.facetime.with_streaming_response.initiate_call( + handle="+15551234567", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + facetime = response.parse() + assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncFacetime: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_initiate_call(self, async_client: AsyncBlooio) -> None: + facetime = await async_client.facetime.initiate_call( + handle="+15551234567", + ) + assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_initiate_call(self, async_client: AsyncBlooio) -> None: + response = await async_client.facetime.with_raw_response.initiate_call( + handle="+15551234567", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + facetime = await response.parse() + assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_initiate_call(self, async_client: AsyncBlooio) -> None: + async with async_client.facetime.with_streaming_response.initiate_call( + handle="+15551234567", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + facetime = await response.parse() + assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_groups.py b/tests/api_resources/test_groups.py new file mode 100644 index 0000000..83f3a9a --- /dev/null +++ b/tests/api_resources/test_groups.py @@ -0,0 +1,466 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from blooio import Blooio, AsyncBlooio +from tests.utils import assert_matches_type +from blooio.types import ( + Group, + GroupListResponse, + GroupCreateResponse, + GroupDeleteResponse, + GroupUpdateResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestGroups: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create(self, client: Blooio) -> None: + group = client.groups.create( + name="Sales Team", + ) + assert_matches_type(GroupCreateResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: Blooio) -> None: + group = client.groups.create( + name="Sales Team", + chat_guid="iMessage;+;chat123456789", + members=["+15551234567", "+15559876543"], + ) + assert_matches_type(GroupCreateResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create(self, client: Blooio) -> None: + response = client.groups.with_raw_response.create( + name="Sales Team", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + group = response.parse() + assert_matches_type(GroupCreateResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create(self, client: Blooio) -> None: + with client.groups.with_streaming_response.create( + name="Sales Team", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + group = response.parse() + assert_matches_type(GroupCreateResponse, group, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: Blooio) -> None: + group = client.groups.retrieve( + "grp_abc123def456", + ) + assert_matches_type(Group, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Blooio) -> None: + response = client.groups.with_raw_response.retrieve( + "grp_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + group = response.parse() + assert_matches_type(Group, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Blooio) -> None: + with client.groups.with_streaming_response.retrieve( + "grp_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + group = response.parse() + assert_matches_type(Group, group, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + client.groups.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update(self, client: Blooio) -> None: + group = client.groups.update( + group_id="grp_abc123def456", + ) + assert_matches_type(GroupUpdateResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: Blooio) -> None: + group = client.groups.update( + group_id="grp_abc123def456", + name="Marketing Team", + ) + assert_matches_type(GroupUpdateResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update(self, client: Blooio) -> None: + response = client.groups.with_raw_response.update( + group_id="grp_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + group = response.parse() + assert_matches_type(GroupUpdateResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update(self, client: Blooio) -> None: + with client.groups.with_streaming_response.update( + group_id="grp_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + group = response.parse() + assert_matches_type(GroupUpdateResponse, group, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_update(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + client.groups.with_raw_response.update( + group_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: Blooio) -> None: + group = client.groups.list() + assert_matches_type(GroupListResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Blooio) -> None: + group = client.groups.list( + limit=1, + offset=0, + q="q", + sort="recent", + ) + assert_matches_type(GroupListResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: Blooio) -> None: + response = client.groups.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + group = response.parse() + assert_matches_type(GroupListResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Blooio) -> None: + with client.groups.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + group = response.parse() + assert_matches_type(GroupListResponse, group, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_delete(self, client: Blooio) -> None: + group = client.groups.delete( + "grp_abc123def456", + ) + assert_matches_type(GroupDeleteResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_delete(self, client: Blooio) -> None: + response = client.groups.with_raw_response.delete( + "grp_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + group = response.parse() + assert_matches_type(GroupDeleteResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: Blooio) -> None: + with client.groups.with_streaming_response.delete( + "grp_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + group = response.parse() + assert_matches_type(GroupDeleteResponse, group, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_delete(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + client.groups.with_raw_response.delete( + "", + ) + + +class TestAsyncGroups: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncBlooio) -> None: + group = await async_client.groups.create( + name="Sales Team", + ) + assert_matches_type(GroupCreateResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncBlooio) -> None: + group = await async_client.groups.create( + name="Sales Team", + chat_guid="iMessage;+;chat123456789", + members=["+15551234567", "+15559876543"], + ) + assert_matches_type(GroupCreateResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncBlooio) -> None: + response = await async_client.groups.with_raw_response.create( + name="Sales Team", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + group = await response.parse() + assert_matches_type(GroupCreateResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncBlooio) -> None: + async with async_client.groups.with_streaming_response.create( + name="Sales Team", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + group = await response.parse() + assert_matches_type(GroupCreateResponse, group, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncBlooio) -> None: + group = await async_client.groups.retrieve( + "grp_abc123def456", + ) + assert_matches_type(Group, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncBlooio) -> None: + response = await async_client.groups.with_raw_response.retrieve( + "grp_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + group = await response.parse() + assert_matches_type(Group, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncBlooio) -> None: + async with async_client.groups.with_streaming_response.retrieve( + "grp_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + group = await response.parse() + assert_matches_type(Group, group, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + await async_client.groups.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncBlooio) -> None: + group = await async_client.groups.update( + group_id="grp_abc123def456", + ) + assert_matches_type(GroupUpdateResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncBlooio) -> None: + group = await async_client.groups.update( + group_id="grp_abc123def456", + name="Marketing Team", + ) + assert_matches_type(GroupUpdateResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncBlooio) -> None: + response = await async_client.groups.with_raw_response.update( + group_id="grp_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + group = await response.parse() + assert_matches_type(GroupUpdateResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncBlooio) -> None: + async with async_client.groups.with_streaming_response.update( + group_id="grp_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + group = await response.parse() + assert_matches_type(GroupUpdateResponse, group, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_update(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + await async_client.groups.with_raw_response.update( + group_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncBlooio) -> None: + group = await async_client.groups.list() + assert_matches_type(GroupListResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncBlooio) -> None: + group = await async_client.groups.list( + limit=1, + offset=0, + q="q", + sort="recent", + ) + assert_matches_type(GroupListResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncBlooio) -> None: + response = await async_client.groups.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + group = await response.parse() + assert_matches_type(GroupListResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncBlooio) -> None: + async with async_client.groups.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + group = await response.parse() + assert_matches_type(GroupListResponse, group, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_delete(self, async_client: AsyncBlooio) -> None: + group = await async_client.groups.delete( + "grp_abc123def456", + ) + assert_matches_type(GroupDeleteResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_delete(self, async_client: AsyncBlooio) -> None: + response = await async_client.groups.with_raw_response.delete( + "grp_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + group = await response.parse() + assert_matches_type(GroupDeleteResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncBlooio) -> None: + async with async_client.groups.with_streaming_response.delete( + "grp_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + group = await response.parse() + assert_matches_type(GroupDeleteResponse, group, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_delete(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + await async_client.groups.with_raw_response.delete( + "", + ) diff --git a/tests/api_resources/test_me.py b/tests/api_resources/test_me.py index 046d4d3..54edc2d 100644 --- a/tests/api_resources/test_me.py +++ b/tests/api_resources/test_me.py @@ -17,13 +17,13 @@ class TestMe: parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_retrieve(self, client: Blooio) -> None: me = client.me.retrieve() assert_matches_type(MeRetrieveResponse, me, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_retrieve(self, client: Blooio) -> None: response = client.me.with_raw_response.retrieve() @@ -33,7 +33,7 @@ def test_raw_response_retrieve(self, client: Blooio) -> None: me = response.parse() assert_matches_type(MeRetrieveResponse, me, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_retrieve(self, client: Blooio) -> None: with client.me.with_streaming_response.retrieve() as response: @@ -51,13 +51,13 @@ class TestAsyncMe: "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] ) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_retrieve(self, async_client: AsyncBlooio) -> None: me = await async_client.me.retrieve() assert_matches_type(MeRetrieveResponse, me, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_retrieve(self, async_client: AsyncBlooio) -> None: response = await async_client.me.with_raw_response.retrieve() @@ -67,7 +67,7 @@ async def test_raw_response_retrieve(self, async_client: AsyncBlooio) -> None: me = await response.parse() assert_matches_type(MeRetrieveResponse, me, path=["response"]) - @pytest.mark.skip(reason="Prism tests are disabled") + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_retrieve(self, async_client: AsyncBlooio) -> None: async with async_client.me.with_streaming_response.retrieve() as response: diff --git a/tests/api_resources/test_messages.py b/tests/api_resources/test_messages.py deleted file mode 100644 index bea0b8b..0000000 --- a/tests/api_resources/test_messages.py +++ /dev/null @@ -1,373 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -import os -from typing import Any, cast - -import pytest - -from blooio import Blooio, AsyncBlooio -from tests.utils import assert_matches_type -from blooio.types import ( - MessageSendResponse, - MessageCancelResponse, - MessageRetrieveResponse, - MessageGetStatusResponse, -) - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestMessages: - parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_retrieve(self, client: Blooio) -> None: - message = client.messages.retrieve( - "messageId", - ) - assert_matches_type(MessageRetrieveResponse, message, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_retrieve(self, client: Blooio) -> None: - response = client.messages.with_raw_response.retrieve( - "messageId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - message = response.parse() - assert_matches_type(MessageRetrieveResponse, message, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_retrieve(self, client: Blooio) -> None: - with client.messages.with_streaming_response.retrieve( - "messageId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - message = response.parse() - assert_matches_type(MessageRetrieveResponse, message, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_retrieve(self, client: Blooio) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `message_id` but received ''"): - client.messages.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_cancel(self, client: Blooio) -> None: - message = client.messages.cancel( - "messageId", - ) - assert_matches_type(MessageCancelResponse, message, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_cancel(self, client: Blooio) -> None: - response = client.messages.with_raw_response.cancel( - "messageId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - message = response.parse() - assert_matches_type(MessageCancelResponse, message, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_cancel(self, client: Blooio) -> None: - with client.messages.with_streaming_response.cancel( - "messageId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - message = response.parse() - assert_matches_type(MessageCancelResponse, message, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_cancel(self, client: Blooio) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `message_id` but received ''"): - client.messages.with_raw_response.cancel( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_get_status(self, client: Blooio) -> None: - message = client.messages.get_status( - "messageId", - ) - assert_matches_type(MessageGetStatusResponse, message, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_get_status(self, client: Blooio) -> None: - response = client.messages.with_raw_response.get_status( - "messageId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - message = response.parse() - assert_matches_type(MessageGetStatusResponse, message, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_get_status(self, client: Blooio) -> None: - with client.messages.with_streaming_response.get_status( - "messageId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - message = response.parse() - assert_matches_type(MessageGetStatusResponse, message, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_path_params_get_status(self, client: Blooio) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `message_id` but received ''"): - client.messages.with_raw_response.get_status( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_send(self, client: Blooio) -> None: - message = client.messages.send( - to="+15551234567", - ) - assert_matches_type(MessageSendResponse, message, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_method_send_with_all_params(self, client: Blooio) -> None: - message = client.messages.send( - to="+15551234567", - attachments=["https://example.com/image.png"], - metadata={"ticket_id": "TCK-123"}, - text="Hello world!", - idempotency_key="Idempotency-Key", - ) - assert_matches_type(MessageSendResponse, message, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_raw_response_send(self, client: Blooio) -> None: - response = client.messages.with_raw_response.send( - to="+15551234567", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - message = response.parse() - assert_matches_type(MessageSendResponse, message, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - def test_streaming_response_send(self, client: Blooio) -> None: - with client.messages.with_streaming_response.send( - to="+15551234567", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - message = response.parse() - assert_matches_type(MessageSendResponse, message, path=["response"]) - - assert cast(Any, response.is_closed) is True - - -class TestAsyncMessages: - parametrize = pytest.mark.parametrize( - "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_retrieve(self, async_client: AsyncBlooio) -> None: - message = await async_client.messages.retrieve( - "messageId", - ) - assert_matches_type(MessageRetrieveResponse, message, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_retrieve(self, async_client: AsyncBlooio) -> None: - response = await async_client.messages.with_raw_response.retrieve( - "messageId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - message = await response.parse() - assert_matches_type(MessageRetrieveResponse, message, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_retrieve(self, async_client: AsyncBlooio) -> None: - async with async_client.messages.with_streaming_response.retrieve( - "messageId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - message = await response.parse() - assert_matches_type(MessageRetrieveResponse, message, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_retrieve(self, async_client: AsyncBlooio) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `message_id` but received ''"): - await async_client.messages.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_cancel(self, async_client: AsyncBlooio) -> None: - message = await async_client.messages.cancel( - "messageId", - ) - assert_matches_type(MessageCancelResponse, message, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_cancel(self, async_client: AsyncBlooio) -> None: - response = await async_client.messages.with_raw_response.cancel( - "messageId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - message = await response.parse() - assert_matches_type(MessageCancelResponse, message, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_cancel(self, async_client: AsyncBlooio) -> None: - async with async_client.messages.with_streaming_response.cancel( - "messageId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - message = await response.parse() - assert_matches_type(MessageCancelResponse, message, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_cancel(self, async_client: AsyncBlooio) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `message_id` but received ''"): - await async_client.messages.with_raw_response.cancel( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_get_status(self, async_client: AsyncBlooio) -> None: - message = await async_client.messages.get_status( - "messageId", - ) - assert_matches_type(MessageGetStatusResponse, message, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_get_status(self, async_client: AsyncBlooio) -> None: - response = await async_client.messages.with_raw_response.get_status( - "messageId", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - message = await response.parse() - assert_matches_type(MessageGetStatusResponse, message, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_get_status(self, async_client: AsyncBlooio) -> None: - async with async_client.messages.with_streaming_response.get_status( - "messageId", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - message = await response.parse() - assert_matches_type(MessageGetStatusResponse, message, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_path_params_get_status(self, async_client: AsyncBlooio) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `message_id` but received ''"): - await async_client.messages.with_raw_response.get_status( - "", - ) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_send(self, async_client: AsyncBlooio) -> None: - message = await async_client.messages.send( - to="+15551234567", - ) - assert_matches_type(MessageSendResponse, message, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_method_send_with_all_params(self, async_client: AsyncBlooio) -> None: - message = await async_client.messages.send( - to="+15551234567", - attachments=["https://example.com/image.png"], - metadata={"ticket_id": "TCK-123"}, - text="Hello world!", - idempotency_key="Idempotency-Key", - ) - assert_matches_type(MessageSendResponse, message, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_raw_response_send(self, async_client: AsyncBlooio) -> None: - response = await async_client.messages.with_raw_response.send( - to="+15551234567", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - message = await response.parse() - assert_matches_type(MessageSendResponse, message, path=["response"]) - - @pytest.mark.skip(reason="Prism tests are disabled") - @parametrize - async def test_streaming_response_send(self, async_client: AsyncBlooio) -> None: - async with async_client.messages.with_streaming_response.send( - to="+15551234567", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - message = await response.parse() - assert_matches_type(MessageSendResponse, message, path=["response"]) - - assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_phone_numbers.py b/tests/api_resources/test_phone_numbers.py new file mode 100644 index 0000000..37a2874 --- /dev/null +++ b/tests/api_resources/test_phone_numbers.py @@ -0,0 +1,92 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from blooio import Blooio, AsyncBlooio +from tests.utils import assert_matches_type +from blooio.types import PhoneNumberBatchCreateResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestPhoneNumbers: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_batch_create(self, client: Blooio) -> None: + phone_number = client.phone_numbers.batch_create( + numbers=["+12125551234", "+14155551234", "+18582849901"], + ) + assert_matches_type(PhoneNumberBatchCreateResponse, phone_number, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_batch_create(self, client: Blooio) -> None: + response = client.phone_numbers.with_raw_response.batch_create( + numbers=["+12125551234", "+14155551234", "+18582849901"], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + phone_number = response.parse() + assert_matches_type(PhoneNumberBatchCreateResponse, phone_number, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_batch_create(self, client: Blooio) -> None: + with client.phone_numbers.with_streaming_response.batch_create( + numbers=["+12125551234", "+14155551234", "+18582849901"], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + phone_number = response.parse() + assert_matches_type(PhoneNumberBatchCreateResponse, phone_number, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncPhoneNumbers: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_batch_create(self, async_client: AsyncBlooio) -> None: + phone_number = await async_client.phone_numbers.batch_create( + numbers=["+12125551234", "+14155551234", "+18582849901"], + ) + assert_matches_type(PhoneNumberBatchCreateResponse, phone_number, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_batch_create(self, async_client: AsyncBlooio) -> None: + response = await async_client.phone_numbers.with_raw_response.batch_create( + numbers=["+12125551234", "+14155551234", "+18582849901"], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + phone_number = await response.parse() + assert_matches_type(PhoneNumberBatchCreateResponse, phone_number, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_batch_create(self, async_client: AsyncBlooio) -> None: + async with async_client.phone_numbers.with_streaming_response.batch_create( + numbers=["+12125551234", "+14155551234", "+18582849901"], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + phone_number = await response.parse() + assert_matches_type(PhoneNumberBatchCreateResponse, phone_number, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_webhooks.py b/tests/api_resources/test_webhooks.py new file mode 100644 index 0000000..a6595cc --- /dev/null +++ b/tests/api_resources/test_webhooks.py @@ -0,0 +1,447 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from blooio import Blooio, AsyncBlooio +from tests.utils import assert_matches_type +from blooio.types import ( + Webhook, + WebhookListResponse, + WebhookCreateResponse, + WebhookDeleteResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestWebhooks: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create(self, client: Blooio) -> None: + webhook = client.webhooks.create( + webhook_url="https://example.com/webhook", + ) + assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: Blooio) -> None: + webhook = client.webhooks.create( + webhook_url="https://example.com/webhook", + valid_until=0, + webhook_type="message", + ) + assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create(self, client: Blooio) -> None: + response = client.webhooks.with_raw_response.create( + webhook_url="https://example.com/webhook", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook = response.parse() + assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create(self, client: Blooio) -> None: + with client.webhooks.with_streaming_response.create( + webhook_url="https://example.com/webhook", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = response.parse() + assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: Blooio) -> None: + webhook = client.webhooks.retrieve( + "wh_abc123def456", + ) + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: Blooio) -> None: + response = client.webhooks.with_raw_response.retrieve( + "wh_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook = response.parse() + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: Blooio) -> None: + with client.webhooks.with_streaming_response.retrieve( + "wh_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = response.parse() + assert_matches_type(Webhook, webhook, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"): + client.webhooks.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update(self, client: Blooio) -> None: + webhook = client.webhooks.update( + webhook_id="wh_abc123def456", + ) + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: Blooio) -> None: + webhook = client.webhooks.update( + webhook_id="wh_abc123def456", + deprecate=True, + valid_until=0, + webhook_type="message", + ) + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update(self, client: Blooio) -> None: + response = client.webhooks.with_raw_response.update( + webhook_id="wh_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook = response.parse() + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update(self, client: Blooio) -> None: + with client.webhooks.with_streaming_response.update( + webhook_id="wh_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = response.parse() + assert_matches_type(Webhook, webhook, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_update(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"): + client.webhooks.with_raw_response.update( + webhook_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: Blooio) -> None: + webhook = client.webhooks.list() + assert_matches_type(WebhookListResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: Blooio) -> None: + response = client.webhooks.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook = response.parse() + assert_matches_type(WebhookListResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Blooio) -> None: + with client.webhooks.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = response.parse() + assert_matches_type(WebhookListResponse, webhook, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_delete(self, client: Blooio) -> None: + webhook = client.webhooks.delete( + "wh_abc123def456", + ) + assert_matches_type(WebhookDeleteResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_delete(self, client: Blooio) -> None: + response = client.webhooks.with_raw_response.delete( + "wh_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook = response.parse() + assert_matches_type(WebhookDeleteResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_delete(self, client: Blooio) -> None: + with client.webhooks.with_streaming_response.delete( + "wh_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = response.parse() + assert_matches_type(WebhookDeleteResponse, webhook, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_delete(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"): + client.webhooks.with_raw_response.delete( + "", + ) + + +class TestAsyncWebhooks: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncBlooio) -> None: + webhook = await async_client.webhooks.create( + webhook_url="https://example.com/webhook", + ) + assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncBlooio) -> None: + webhook = await async_client.webhooks.create( + webhook_url="https://example.com/webhook", + valid_until=0, + webhook_type="message", + ) + assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncBlooio) -> None: + response = await async_client.webhooks.with_raw_response.create( + webhook_url="https://example.com/webhook", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook = await response.parse() + assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncBlooio) -> None: + async with async_client.webhooks.with_streaming_response.create( + webhook_url="https://example.com/webhook", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = await response.parse() + assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncBlooio) -> None: + webhook = await async_client.webhooks.retrieve( + "wh_abc123def456", + ) + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncBlooio) -> None: + response = await async_client.webhooks.with_raw_response.retrieve( + "wh_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook = await response.parse() + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncBlooio) -> None: + async with async_client.webhooks.with_streaming_response.retrieve( + "wh_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = await response.parse() + assert_matches_type(Webhook, webhook, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"): + await async_client.webhooks.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncBlooio) -> None: + webhook = await async_client.webhooks.update( + webhook_id="wh_abc123def456", + ) + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncBlooio) -> None: + webhook = await async_client.webhooks.update( + webhook_id="wh_abc123def456", + deprecate=True, + valid_until=0, + webhook_type="message", + ) + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncBlooio) -> None: + response = await async_client.webhooks.with_raw_response.update( + webhook_id="wh_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook = await response.parse() + assert_matches_type(Webhook, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncBlooio) -> None: + async with async_client.webhooks.with_streaming_response.update( + webhook_id="wh_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = await response.parse() + assert_matches_type(Webhook, webhook, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_update(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"): + await async_client.webhooks.with_raw_response.update( + webhook_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncBlooio) -> None: + webhook = await async_client.webhooks.list() + assert_matches_type(WebhookListResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncBlooio) -> None: + response = await async_client.webhooks.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook = await response.parse() + assert_matches_type(WebhookListResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncBlooio) -> None: + async with async_client.webhooks.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = await response.parse() + assert_matches_type(WebhookListResponse, webhook, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_delete(self, async_client: AsyncBlooio) -> None: + webhook = await async_client.webhooks.delete( + "wh_abc123def456", + ) + assert_matches_type(WebhookDeleteResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_delete(self, async_client: AsyncBlooio) -> None: + response = await async_client.webhooks.with_raw_response.delete( + "wh_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook = await response.parse() + assert_matches_type(WebhookDeleteResponse, webhook, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncBlooio) -> None: + async with async_client.webhooks.with_streaming_response.delete( + "wh_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = await response.parse() + assert_matches_type(WebhookDeleteResponse, webhook, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_delete(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"): + await async_client.webhooks.with_raw_response.delete( + "", + ) diff --git a/tests/api_resources/webhooks/__init__.py b/tests/api_resources/webhooks/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/webhooks/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/webhooks/test_logs.py b/tests/api_resources/webhooks/test_logs.py new file mode 100644 index 0000000..1882071 --- /dev/null +++ b/tests/api_resources/webhooks/test_logs.py @@ -0,0 +1,240 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from blooio import Blooio, AsyncBlooio +from tests.utils import assert_matches_type +from blooio.types.webhooks import LogListResponse, LogReplayResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestLogs: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: Blooio) -> None: + log = client.webhooks.logs.list( + webhook_id="wh_abc123def456", + ) + assert_matches_type(LogListResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: Blooio) -> None: + log = client.webhooks.logs.list( + webhook_id="wh_abc123def456", + limit=1, + max_status=0, + min_status=0, + offset=0, + sort="asc", + status=0, + ) + assert_matches_type(LogListResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: Blooio) -> None: + response = client.webhooks.logs.with_raw_response.list( + webhook_id="wh_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + log = response.parse() + assert_matches_type(LogListResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: Blooio) -> None: + with client.webhooks.logs.with_streaming_response.list( + webhook_id="wh_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + log = response.parse() + assert_matches_type(LogListResponse, log, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_list(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"): + client.webhooks.logs.with_raw_response.list( + webhook_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_replay(self, client: Blooio) -> None: + log = client.webhooks.logs.replay( + event_id="eventId", + webhook_id="wh_abc123def456", + ) + assert_matches_type(LogReplayResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_replay(self, client: Blooio) -> None: + response = client.webhooks.logs.with_raw_response.replay( + event_id="eventId", + webhook_id="wh_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + log = response.parse() + assert_matches_type(LogReplayResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_replay(self, client: Blooio) -> None: + with client.webhooks.logs.with_streaming_response.replay( + event_id="eventId", + webhook_id="wh_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + log = response.parse() + assert_matches_type(LogReplayResponse, log, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_replay(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"): + client.webhooks.logs.with_raw_response.replay( + event_id="eventId", + webhook_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `event_id` but received ''"): + client.webhooks.logs.with_raw_response.replay( + event_id="", + webhook_id="wh_abc123def456", + ) + + +class TestAsyncLogs: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncBlooio) -> None: + log = await async_client.webhooks.logs.list( + webhook_id="wh_abc123def456", + ) + assert_matches_type(LogListResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncBlooio) -> None: + log = await async_client.webhooks.logs.list( + webhook_id="wh_abc123def456", + limit=1, + max_status=0, + min_status=0, + offset=0, + sort="asc", + status=0, + ) + assert_matches_type(LogListResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncBlooio) -> None: + response = await async_client.webhooks.logs.with_raw_response.list( + webhook_id="wh_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + log = await response.parse() + assert_matches_type(LogListResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncBlooio) -> None: + async with async_client.webhooks.logs.with_streaming_response.list( + webhook_id="wh_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + log = await response.parse() + assert_matches_type(LogListResponse, log, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_list(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"): + await async_client.webhooks.logs.with_raw_response.list( + webhook_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_replay(self, async_client: AsyncBlooio) -> None: + log = await async_client.webhooks.logs.replay( + event_id="eventId", + webhook_id="wh_abc123def456", + ) + assert_matches_type(LogReplayResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_replay(self, async_client: AsyncBlooio) -> None: + response = await async_client.webhooks.logs.with_raw_response.replay( + event_id="eventId", + webhook_id="wh_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + log = await response.parse() + assert_matches_type(LogReplayResponse, log, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_replay(self, async_client: AsyncBlooio) -> None: + async with async_client.webhooks.logs.with_streaming_response.replay( + event_id="eventId", + webhook_id="wh_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + log = await response.parse() + assert_matches_type(LogReplayResponse, log, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_replay(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"): + await async_client.webhooks.logs.with_raw_response.replay( + event_id="eventId", + webhook_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `event_id` but received ''"): + await async_client.webhooks.logs.with_raw_response.replay( + event_id="", + webhook_id="wh_abc123def456", + ) diff --git a/tests/api_resources/webhooks/test_secret.py b/tests/api_resources/webhooks/test_secret.py new file mode 100644 index 0000000..e0d5af7 --- /dev/null +++ b/tests/api_resources/webhooks/test_secret.py @@ -0,0 +1,108 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from blooio import Blooio, AsyncBlooio +from tests.utils import assert_matches_type +from blooio.types.webhooks import SecretRotateResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestSecret: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_rotate(self, client: Blooio) -> None: + secret = client.webhooks.secret.rotate( + "wh_abc123def456", + ) + assert_matches_type(SecretRotateResponse, secret, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_rotate(self, client: Blooio) -> None: + response = client.webhooks.secret.with_raw_response.rotate( + "wh_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + secret = response.parse() + assert_matches_type(SecretRotateResponse, secret, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_rotate(self, client: Blooio) -> None: + with client.webhooks.secret.with_streaming_response.rotate( + "wh_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + secret = response.parse() + assert_matches_type(SecretRotateResponse, secret, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_rotate(self, client: Blooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"): + client.webhooks.secret.with_raw_response.rotate( + "", + ) + + +class TestAsyncSecret: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_rotate(self, async_client: AsyncBlooio) -> None: + secret = await async_client.webhooks.secret.rotate( + "wh_abc123def456", + ) + assert_matches_type(SecretRotateResponse, secret, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_rotate(self, async_client: AsyncBlooio) -> None: + response = await async_client.webhooks.secret.with_raw_response.rotate( + "wh_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + secret = await response.parse() + assert_matches_type(SecretRotateResponse, secret, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_rotate(self, async_client: AsyncBlooio) -> None: + async with async_client.webhooks.secret.with_streaming_response.rotate( + "wh_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + secret = await response.parse() + assert_matches_type(SecretRotateResponse, secret, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_rotate(self, async_client: AsyncBlooio) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `webhook_id` but received ''"): + await async_client.webhooks.secret.with_raw_response.rotate( + "", + ) diff --git a/tests/test_client.py b/tests/test_client.py index f9ee51f..878c240 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,10 +8,11 @@ import json import asyncio import inspect +import dataclasses import tracemalloc -from typing import Any, Union, cast +from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast from unittest import mock -from typing_extensions import Literal +from typing_extensions import Literal, AsyncIterator, override import httpx import pytest @@ -36,6 +37,7 @@ from .utils import update_env +T = TypeVar("T") base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") api_key = "My API Key" @@ -50,6 +52,57 @@ def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: return 0.1 +def mirror_request_content(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, content=request.content) + + +# note: we can't use the httpx.MockTransport class as it consumes the request +# body itself, which means we can't test that the body is read lazily +class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport): + def __init__( + self, + handler: Callable[[httpx.Request], httpx.Response] + | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]], + ) -> None: + self.handler = handler + + @override + def handle_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function" + assert inspect.isfunction(self.handler), "handler must be a function" + return self.handler(request) + + @override + async def handle_async_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function" + return await self.handler(request) + + +@dataclasses.dataclass +class Counter: + value: int = 0 + + +def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + +async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + def _get_open_connections(client: Blooio | AsyncBlooio) -> int: transport = client._client._transport assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) @@ -374,6 +427,30 @@ def test_default_query_option(self) -> None: client.close() + def test_hardcoded_query_params_in_url(self, client: Blooio) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: Blooio) -> None: request = client._build_request( FinalRequestOptions( @@ -500,6 +577,70 @@ def test_multipart_repeating_array(self, client: Blooio) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload(self, respx_mock: MockRouter, client: Blooio) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + def test_binary_content_upload_with_iterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_sync_iterator([file_content], counter=counter) + + def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=request.read()) + + with Blooio( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(transport=MockTransport(handler=mock_handler)), + ) as client: + response = client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload_with_body_is_deprecated(self, respx_mock: MockRouter, client: Blooio) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) def test_basic_union_response(self, respx_mock: MockRouter, client: Blooio) -> None: class Model1(BaseModel): @@ -732,7 +873,7 @@ def test_parse_retry_after_header( @mock.patch("blooio._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: Blooio) -> None: - respx_mock.get("/v1/api/me").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.get("/me").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): client.me.with_streaming_response.retrieve().__enter__() @@ -742,7 +883,7 @@ def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, clien @mock.patch("blooio._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: Blooio) -> None: - respx_mock.get("/v1/api/me").mock(return_value=httpx.Response(500)) + respx_mock.get("/me").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): client.me.with_streaming_response.retrieve().__enter__() @@ -772,7 +913,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.get("/v1/api/me").mock(side_effect=retry_handler) + respx_mock.get("/me").mock(side_effect=retry_handler) response = client.me.with_raw_response.retrieve() @@ -796,7 +937,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.get("/v1/api/me").mock(side_effect=retry_handler) + respx_mock.get("/me").mock(side_effect=retry_handler) response = client.me.with_raw_response.retrieve(extra_headers={"x-stainless-retry-count": Omit()}) @@ -819,7 +960,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.get("/v1/api/me").mock(side_effect=retry_handler) + respx_mock.get("/me").mock(side_effect=retry_handler) response = client.me.with_raw_response.retrieve(extra_headers={"x-stainless-retry-count": "42"}) @@ -828,6 +969,14 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has any proxy env vars set + monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) client = DefaultHttpxClient() @@ -1193,6 +1342,30 @@ async def test_default_query_option(self) -> None: await client.close() + async def test_hardcoded_query_params_in_url(self, async_client: AsyncBlooio) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + def test_request_extra_json(self, client: Blooio) -> None: request = client._build_request( FinalRequestOptions( @@ -1319,6 +1492,72 @@ def test_multipart_repeating_array(self, async_client: AsyncBlooio) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncBlooio) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = await async_client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + async def test_binary_content_upload_with_asynciterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_async_iterator([file_content], counter=counter) + + async def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=await request.aread()) + + async with AsyncBlooio( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)), + ) as client: + response = await client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload_with_body_is_deprecated( + self, respx_mock: MockRouter, async_client: AsyncBlooio + ) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = await async_client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + @pytest.mark.respx(base_url=base_url) async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncBlooio) -> None: class Model1(BaseModel): @@ -1564,7 +1803,7 @@ async def test_parse_retry_after_header( @mock.patch("blooio._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncBlooio) -> None: - respx_mock.get("/v1/api/me").mock(side_effect=httpx.TimeoutException("Test timeout error")) + respx_mock.get("/me").mock(side_effect=httpx.TimeoutException("Test timeout error")) with pytest.raises(APITimeoutError): await async_client.me.with_streaming_response.retrieve().__aenter__() @@ -1574,7 +1813,7 @@ async def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, @mock.patch("blooio._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) @pytest.mark.respx(base_url=base_url) async def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, async_client: AsyncBlooio) -> None: - respx_mock.get("/v1/api/me").mock(return_value=httpx.Response(500)) + respx_mock.get("/me").mock(return_value=httpx.Response(500)) with pytest.raises(APIStatusError): await async_client.me.with_streaming_response.retrieve().__aenter__() @@ -1604,7 +1843,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.get("/v1/api/me").mock(side_effect=retry_handler) + respx_mock.get("/me").mock(side_effect=retry_handler) response = await client.me.with_raw_response.retrieve() @@ -1628,7 +1867,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.get("/v1/api/me").mock(side_effect=retry_handler) + respx_mock.get("/me").mock(side_effect=retry_handler) response = await client.me.with_raw_response.retrieve(extra_headers={"x-stainless-retry-count": Omit()}) @@ -1651,7 +1890,7 @@ def retry_handler(_request: httpx.Request) -> httpx.Response: return httpx.Response(500) return httpx.Response(200) - respx_mock.get("/v1/api/me").mock(side_effect=retry_handler) + respx_mock.get("/me").mock(side_effect=retry_handler) response = await client.me.with_raw_response.retrieve(extra_headers={"x-stainless-retry-count": "42"}) @@ -1664,6 +1903,14 @@ async def test_get_platform(self) -> None: async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: # Test that the proxy environment variables are set correctly monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has any proxy env vars set + monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) client = DefaultAsyncHttpxClient() diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py deleted file mode 100644 index 92ce2e9..0000000 --- a/tests/test_deepcopy.py +++ /dev/null @@ -1,58 +0,0 @@ -from blooio._utils import deepcopy_minimal - - -def assert_different_identities(obj1: object, obj2: object) -> None: - assert obj1 == obj2 - assert id(obj1) != id(obj2) - - -def test_simple_dict() -> None: - obj1 = {"foo": "bar"} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - - -def test_nested_dict() -> None: - obj1 = {"foo": {"bar": True}} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1["foo"], obj2["foo"]) - - -def test_complex_nested_dict() -> None: - obj1 = {"foo": {"bar": [{"hello": "world"}]}} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1["foo"], obj2["foo"]) - assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"]) - assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0]) - - -def test_simple_list() -> None: - obj1 = ["a", "b", "c"] - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - - -def test_nested_list() -> None: - obj1 = ["a", [1, 2, 3]] - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1[1], obj2[1]) - - -class MyObject: ... - - -def test_ignores_other_types() -> None: - # custom classes - my_obj = MyObject() - obj1 = {"foo": my_obj} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert obj1["foo"] is my_obj - - # tuples - obj3 = ("a", "b") - obj4 = deepcopy_minimal(obj3) - assert obj3 is obj4 diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index a9d8f5e..1195df8 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -4,7 +4,7 @@ import pytest -from blooio._types import FileTypes +from blooio._types import FileTypes, ArrayFormat from blooio._utils import extract_files @@ -35,6 +35,12 @@ def test_multiple_files() -> None: assert query == {"documents": [{}, {}]} +def test_top_level_file_array() -> None: + query = {"files": [b"file one", b"file two"], "title": "hello"} + assert extract_files(query, paths=[["files", ""]]) == [("files[]", b"file one"), ("files[]", b"file two")] + assert query == {"title": "hello"} + + @pytest.mark.parametrize( "query,paths,expected", [ @@ -62,3 +68,24 @@ def test_ignores_incorrect_paths( expected: list[tuple[str, FileTypes]], ) -> None: assert extract_files(query, paths=paths) == expected + + +@pytest.mark.parametrize( + "array_format,expected_top_level,expected_nested", + [ + ("brackets", [("files[]", b"a"), ("files[]", b"b")], [("items[][file]", b"a"), ("items[][file]", b"b")]), + ("repeat", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("comma", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("indices", [("files[0]", b"a"), ("files[1]", b"b")], [("items[0][file]", b"a"), ("items[1][file]", b"b")]), + ], +) +def test_array_format_controls_file_field_names( + array_format: ArrayFormat, + expected_top_level: list[tuple[str, FileTypes]], + expected_nested: list[tuple[str, FileTypes]], +) -> None: + top_level = {"files": [b"a", b"b"]} + assert extract_files(top_level, paths=[["files", ""]], array_format=array_format) == expected_top_level + + nested = {"items": [{"file": b"a"}, {"file": b"b"}]} + assert extract_files(nested, paths=[["items", "", "file"]], array_format=array_format) == expected_nested diff --git a/tests/test_files.py b/tests/test_files.py index fb77ae0..a12ea55 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -4,7 +4,8 @@ import pytest from dirty_equals import IsDict, IsList, IsBytes, IsTuple -from blooio._files import to_httpx_files, async_to_httpx_files +from blooio._files import to_httpx_files, deepcopy_with_paths, async_to_httpx_files +from blooio._utils import extract_files readme_path = Path(__file__).parent.parent.joinpath("README.md") @@ -49,3 +50,99 @@ def test_string_not_allowed() -> None: "file": "foo", # type: ignore } ) + + +def assert_different_identities(obj1: object, obj2: object) -> None: + assert obj1 == obj2 + assert obj1 is not obj2 + + +class TestDeepcopyWithPaths: + def test_copies_top_level_dict(self) -> None: + original = {"file": b"data", "other": "value"} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + + def test_file_value_is_same_reference(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + assert result["file"] is file_bytes + + def test_list_popped_wholesale(self) -> None: + files = [b"f1", b"f2"] + original = {"files": files, "title": "t"} + result = deepcopy_with_paths(original, [["files", ""]]) + assert_different_identities(result, original) + result_files = result["files"] + assert isinstance(result_files, list) + assert_different_identities(result_files, files) + + def test_nested_array_path_copies_list_and_elements(self) -> None: + elem1 = {"file": b"f1", "extra": 1} + elem2 = {"file": b"f2", "extra": 2} + original = {"items": [elem1, elem2]} + result = deepcopy_with_paths(original, [["items", "", "file"]]) + assert_different_identities(result, original) + result_items = result["items"] + assert isinstance(result_items, list) + assert_different_identities(result_items, original["items"]) + assert_different_identities(result_items[0], elem1) + assert_different_identities(result_items[1], elem2) + + def test_empty_paths_returns_same_object(self) -> None: + original = {"foo": "bar"} + result = deepcopy_with_paths(original, []) + assert result is original + + def test_multiple_paths(self) -> None: + f1 = b"file1" + f2 = b"file2" + original = {"a": f1, "b": f2, "c": "unchanged"} + result = deepcopy_with_paths(original, [["a"], ["b"]]) + assert_different_identities(result, original) + assert result["a"] is f1 + assert result["b"] is f2 + assert result["c"] is original["c"] + + def test_extract_files_does_not_mutate_original_top_level(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes, "other": "value"} + + copied = deepcopy_with_paths(original, [["file"]]) + extracted = extract_files(copied, paths=[["file"]]) + + assert extracted == [("file", file_bytes)] + assert original == {"file": file_bytes, "other": "value"} + assert copied == {"other": "value"} + + def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: + file1 = b"f1" + file2 = b"f2" + original = { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + + copied = deepcopy_with_paths(original, [["items", "", "file"]]) + extracted = extract_files(copied, paths=[["items", "", "file"]]) + + assert [entry for _, entry in extracted] == [file1, file2] + assert original == { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + assert copied == { + "items": [ + {"extra": 1}, + {"extra": 2}, + ], + "title": "example", + } diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py new file mode 100644 index 0000000..9f63846 --- /dev/null +++ b/tests/test_utils/test_json.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import datetime +from typing import Union + +import pydantic + +from blooio import _compat +from blooio._utils._json import openapi_dumps + + +class TestOpenapiDumps: + def test_basic(self) -> None: + data = {"key": "value", "number": 42} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"key":"value","number":42}' + + def test_datetime_serialization(self) -> None: + dt = datetime.datetime(2023, 1, 1, 12, 0, 0) + data = {"datetime": dt} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}' + + def test_pydantic_model_serialization(self) -> None: + class User(pydantic.BaseModel): + first_name: str + last_name: str + age: int + + model_instance = User(first_name="John", last_name="Kramer", age=83) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}' + + def test_pydantic_model_with_default_values(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + score: int = 0 + + model_instance = User(name="Alice") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Alice"}}' + + def test_pydantic_model_with_default_values_overridden(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + + model_instance = User(name="Bob", role="admin", active=False) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}' + + def test_pydantic_model_with_alias(self) -> None: + class User(pydantic.BaseModel): + first_name: str = pydantic.Field(alias="firstName") + last_name: str = pydantic.Field(alias="lastName") + + model_instance = User(firstName="John", lastName="Doe") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}' + + def test_pydantic_model_with_alias_and_default(self) -> None: + class User(pydantic.BaseModel): + user_name: str = pydantic.Field(alias="userName") + user_role: str = pydantic.Field(default="member", alias="userRole") + is_active: bool = pydantic.Field(default=True, alias="isActive") + + model_instance = User(userName="charlie") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"charlie"}}' + + model_with_overrides = User(userName="diana", userRole="admin", isActive=False) + data = {"model": model_with_overrides} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}' + + def test_pydantic_model_with_nested_models_and_defaults(self) -> None: + class Address(pydantic.BaseModel): + street: str + city: str = "Unknown" + + class User(pydantic.BaseModel): + name: str + address: Address + verified: bool = False + + if _compat.PYDANTIC_V1: + # to handle forward references in Pydantic v1 + User.update_forward_refs(**locals()) # type: ignore[reportDeprecated] + + address = Address(street="123 Main St") + user = User(name="Diana", address=address) + data = {"user": user} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}' + + address_with_city = Address(street="456 Oak Ave", city="Boston") + user_verified = User(name="Eve", address=address_with_city, verified=True) + data = {"user": user_verified} + json_bytes = openapi_dumps(data) + assert ( + json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}' + ) + + def test_pydantic_model_with_optional_fields(self) -> None: + class User(pydantic.BaseModel): + name: str + email: Union[str, None] + phone: Union[str, None] + + model_with_none = User(name="Eve", email=None, phone=None) + data = {"model": model_with_none} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}' + + model_with_values = User(name="Frank", email="frank@example.com", phone=None) + data = {"model": model_with_values} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}' diff --git a/tests/test_utils/test_path.py b/tests/test_utils/test_path.py new file mode 100644 index 0000000..e0015a7 --- /dev/null +++ b/tests/test_utils/test_path.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from typing import Any + +import pytest + +from blooio._utils._path import path_template + + +@pytest.mark.parametrize( + "template, kwargs, expected", + [ + ("/v1/{id}", dict(id="abc"), "/v1/abc"), + ("/v1/{a}/{b}", dict(a="x", b="y"), "/v1/x/y"), + ("/v1/{a}{b}/path/{c}?val={d}#{e}", dict(a="x", b="y", c="z", d="u", e="v"), "/v1/xy/path/z?val=u#v"), + ("/{w}/{w}", dict(w="echo"), "/echo/echo"), + ("/v1/static", {}, "/v1/static"), + ("", {}, ""), + ("/v1/?q={n}&count=10", dict(n=42), "/v1/?q=42&count=10"), + ("/v1/{v}", dict(v=None), "/v1/null"), + ("/v1/{v}", dict(v=True), "/v1/true"), + ("/v1/{v}", dict(v=False), "/v1/false"), + ("/v1/{v}", dict(v=".hidden"), "/v1/.hidden"), # dot prefix ok + ("/v1/{v}", dict(v="file.txt"), "/v1/file.txt"), # dot in middle ok + ("/v1/{v}", dict(v="..."), "/v1/..."), # triple dot ok + ("/v1/{a}{b}", dict(a=".", b="txt"), "/v1/.txt"), # dot var combining with adjacent to be ok + ("/items?q={v}#{f}", dict(v=".", f=".."), "/items?q=.#.."), # dots in query/fragment are fine + ( + "/v1/{a}?query={b}", + dict(a="../../other/endpoint", b="a&bad=true"), + "/v1/..%2F..%2Fother%2Fendpoint?query=a%26bad%3Dtrue", + ), + ("/v1/{val}", dict(val="a/b/c"), "/v1/a%2Fb%2Fc"), + ("/v1/{val}", dict(val="a/b/c?query=value"), "/v1/a%2Fb%2Fc%3Fquery=value"), + ("/v1/{val}", dict(val="a/b/c?query=value&bad=true"), "/v1/a%2Fb%2Fc%3Fquery=value&bad=true"), + ("/v1/{val}", dict(val="%20"), "/v1/%2520"), # escapes escape sequences in input + # Query: slash and ? are safe, # is not + ("/items?q={v}", dict(v="a/b"), "/items?q=a/b"), + ("/items?q={v}", dict(v="a?b"), "/items?q=a?b"), + ("/items?q={v}", dict(v="a#b"), "/items?q=a%23b"), + ("/items?q={v}", dict(v="a b"), "/items?q=a%20b"), + # Fragment: slash and ? are safe + ("/docs#{v}", dict(v="a/b"), "/docs#a/b"), + ("/docs#{v}", dict(v="a?b"), "/docs#a?b"), + # Path: slash, ? and # are all encoded + ("/v1/{v}", dict(v="a/b"), "/v1/a%2Fb"), + ("/v1/{v}", dict(v="a?b"), "/v1/a%3Fb"), + ("/v1/{v}", dict(v="a#b"), "/v1/a%23b"), + # same var encoded differently by component + ( + "/v1/{v}?q={v}#{v}", + dict(v="a/b?c#d"), + "/v1/a%2Fb%3Fc%23d?q=a/b?c%23d#a/b?c%23d", + ), + ("/v1/{val}", dict(val="x?admin=true"), "/v1/x%3Fadmin=true"), # query injection + ("/v1/{val}", dict(val="x#admin"), "/v1/x%23admin"), # fragment injection + ], +) +def test_interpolation(template: str, kwargs: dict[str, Any], expected: str) -> None: + assert path_template(template, **kwargs) == expected + + +def test_missing_kwarg_raises_key_error() -> None: + with pytest.raises(KeyError, match="org_id"): + path_template("/v1/{org_id}") + + +@pytest.mark.parametrize( + "template, kwargs", + [ + ("{a}/path", dict(a=".")), + ("{a}/path", dict(a="..")), + ("/v1/{a}", dict(a=".")), + ("/v1/{a}", dict(a="..")), + ("/v1/{a}/path", dict(a=".")), + ("/v1/{a}/path", dict(a="..")), + ("/v1/{a}{b}", dict(a=".", b=".")), # adjacent vars → ".." + ("/v1/{a}.", dict(a=".")), # var + static → ".." + ("/v1/{a}{b}", dict(a="", b=".")), # empty + dot → "." + ("/v1/%2e/{x}", dict(x="ok")), # encoded dot in static text + ("/v1/%2e./{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/.%2E/{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/{v}?q=1", dict(v="..")), + ("/v1/{v}#frag", dict(v="..")), + ], +) +def test_dot_segment_rejected(template: str, kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError, match="dot-segment"): + path_template(template, **kwargs) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..fd58302 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1829 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-6-blooio-pydantic-v1' and extra != 'group-6-blooio-pydantic-v2'", + "python_full_version < '3.10' and extra == 'group-6-blooio-pydantic-v1' and extra != 'group-6-blooio-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-6-blooio-pydantic-v1' and extra != 'group-6-blooio-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-6-blooio-pydantic-v1' and extra != 'group-6-blooio-pydantic-v2'", +] +conflicts = [[ + { package = "blooio", group = "pydantic-v1" }, + { package = "blooio", group = "pydantic-v2" }, +]] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950, upload-time = "2026-01-03T17:29:13.002Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099, upload-time = "2026-01-03T17:29:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072, upload-time = "2026-01-03T17:29:16.922Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588, upload-time = "2026-01-03T17:29:18.539Z" }, + { url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334, upload-time = "2026-01-03T17:29:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656, upload-time = "2026-01-03T17:29:22.531Z" }, + { url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625, upload-time = "2026-01-03T17:29:24.276Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604, upload-time = "2026-01-03T17:29:26.099Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370, upload-time = "2026-01-03T17:29:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023, upload-time = "2026-01-03T17:29:30.002Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680, upload-time = "2026-01-03T17:29:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407, upload-time = "2026-01-03T17:29:33.392Z" }, + { url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047, upload-time = "2026-01-03T17:29:34.855Z" }, + { url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264, upload-time = "2026-01-03T17:29:36.389Z" }, + { url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275, upload-time = "2026-01-03T17:29:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/3f/4f/2c41f800a0b560785c10fb316216ac058c105f9be50bdc6a285de88db625/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", size = 434053, upload-time = "2026-01-03T17:29:40.074Z" }, + { url = "https://files.pythonhosted.org/packages/80/df/29cd63c7ecfdb65ccc12f7d808cac4fa2a19544660c06c61a4a48462de0c/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", size = 456687, upload-time = "2026-01-03T17:29:41.819Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, + { url = "https://files.pythonhosted.org/packages/bf/79/446655656861d3e7e2c32bfcf160c7aa9e9dc63776a691b124dba65cdd77/aiohttp-3.13.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e", size = 741433, upload-time = "2026-01-03T17:32:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/cb/49/773c4b310b5140d2fb5e79bb0bf40b7b41dad80a288ca1a8759f5f72bda9/aiohttp-3.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7", size = 497332, upload-time = "2026-01-03T17:32:28.37Z" }, + { url = "https://files.pythonhosted.org/packages/bc/31/1dcbc4b83a4e6f76a0ad883f07f21ffbfe29750c89db97381701508c9f45/aiohttp-3.13.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02", size = 492365, upload-time = "2026-01-03T17:32:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/5a/b5/b50657496c8754482cd7964e50aaf3aa84b3db61ed45daec4c1aec5b94b4/aiohttp-3.13.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43", size = 1660440, upload-time = "2026-01-03T17:32:32.586Z" }, + { url = "https://files.pythonhosted.org/packages/2a/73/9b69e5139d89d75127569298931444ad78ea86a5befd5599780b1e9a6880/aiohttp-3.13.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6", size = 1632740, upload-time = "2026-01-03T17:32:34.793Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fe/3ea9b5af694b4e3aec0d0613a806132ca744747146fca68e96bf056f61a7/aiohttp-3.13.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce", size = 1719782, upload-time = "2026-01-03T17:32:37.737Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c2/46b3b06e60851cbb71efb0f79a3267279cbef7b12c58e68a1e897f269cca/aiohttp-3.13.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80", size = 1813527, upload-time = "2026-01-03T17:32:39.973Z" }, + { url = "https://files.pythonhosted.org/packages/36/23/71ceb78c769ed65fe4c697692de232b63dab399210678d2b00961ccb0619/aiohttp-3.13.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a", size = 1661268, upload-time = "2026-01-03T17:32:42.082Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8d/86e929523d955e85ebab7c0e2b9e0cb63604cfc27dc3280e10d0063cf682/aiohttp-3.13.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6", size = 1552742, upload-time = "2026-01-03T17:32:44.622Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/3f5987cba1bab6bd151f0d97aa60f0ce04d3c83316692a6bb6ba2fb69f92/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558", size = 1632918, upload-time = "2026-01-03T17:32:46.749Z" }, + { url = "https://files.pythonhosted.org/packages/be/2c/7e1e85121f2e31ee938cb83a8f32dfafd4908530c10fabd6d46761c12ac7/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7", size = 1644446, upload-time = "2026-01-03T17:32:49.063Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/ce6133d423ad0e8ca976a7c848f7146bca3520eea4ccf6b95e2d077c9d20/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877", size = 1689487, upload-time = "2026-01-03T17:32:51.113Z" }, + { url = "https://files.pythonhosted.org/packages/50/f7/ff7a27c15603d460fd1366b3c22054f7ae4fa9310aca40b43bde35867fcd/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3", size = 1540715, upload-time = "2026-01-03T17:32:53.38Z" }, + { url = "https://files.pythonhosted.org/packages/17/02/053f11346e5b962e6d8a1c4f8c70c29d5970a1b4b8e7894c68e12c27a57f/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704", size = 1711835, upload-time = "2026-01-03T17:32:56.088Z" }, + { url = "https://files.pythonhosted.org/packages/fb/71/9b9761ddf276fd6708d13720197cbac19b8d67ecfa9116777924056cfcaa/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f", size = 1649593, upload-time = "2026-01-03T17:32:58.181Z" }, + { url = "https://files.pythonhosted.org/packages/ae/72/5d817e9ea218acae12a5e3b9ad1178cf0c12fc3570c0b47eea2daf95f9ea/aiohttp-3.13.3-cp39-cp39-win32.whl", hash = "sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1", size = 434831, upload-time = "2026-01-03T17:33:00.577Z" }, + { url = "https://files.pythonhosted.org/packages/39/cb/22659d9bf3149b7a2927bc2769cc9c8f8f5a80eba098398e03c199a43a85/aiohttp-3.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538", size = 457697, upload-time = "2026-01-03T17:33:03.167Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "blooio" +version = "1.0.5" +source = { editable = "." } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic", version = "1.10.26", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-6-blooio-pydantic-v1'" }, + { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-6-blooio-pydantic-v2' or extra != 'group-6-blooio-pydantic-v1'" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] + +[package.optional-dependencies] +aiohttp = [ + { name = "aiohttp" }, + { name = "httpx-aiohttp" }, +] + +[package.dev-dependencies] +dev = [ + { name = "dirty-equals" }, + { name = "importlib-metadata" }, + { name = "mypy" }, + { name = "pyright" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "pytest-asyncio", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "pytest-xdist" }, + { name = "respx" }, + { name = "rich" }, + { name = "ruff" }, + { name = "time-machine", version = "2.19.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "time-machine", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, +] +pydantic-v1 = [ + { name = "pydantic", version = "1.10.26", source = { registry = "https://pypi.org/simple" } }, +] +pydantic-v2 = [ + { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" } }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", marker = "extra == 'aiohttp'" }, + { name = "anyio", specifier = ">=3.5.0,<5" }, + { name = "distro", specifier = ">=1.7.0,<2" }, + { name = "httpx", specifier = ">=0.23.0,<1" }, + { name = "httpx-aiohttp", marker = "extra == 'aiohttp'", specifier = ">=0.1.9" }, + { name = "pydantic", specifier = ">=1.9.0,<3" }, + { name = "sniffio" }, + { name = "typing-extensions", specifier = ">=4.14,<5" }, +] +provides-extras = ["aiohttp"] + +[package.metadata.requires-dev] +dev = [ + { name = "dirty-equals", specifier = ">=0.6.0" }, + { name = "importlib-metadata", specifier = ">=6.7.0" }, + { name = "mypy", specifier = "==1.17" }, + { name = "pyright", specifier = "==1.1.399" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-xdist", specifier = ">=3.6.1" }, + { name = "respx" }, + { name = "rich", specifier = ">=13.7.1" }, + { name = "ruff" }, + { name = "time-machine" }, +] +pydantic-v1 = [{ name = "pydantic", specifier = ">=1.9.0,<2" }] +pydantic-v2 = [ + { name = "pydantic", marker = "python_full_version < '3.14'", specifier = "~=2.0" }, + { name = "pydantic", marker = "python_full_version >= '3.14'", specifier = "~=2.12" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "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 = "dirty-equals" +version = "0.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/1d/c5913ac9d6615515a00f4bdc71356d302437cb74ff2e9aaccd3c14493b78/dirty_equals-0.11.tar.gz", hash = "sha256:f4ac74ee88f2d11e2fa0f65eb30ee4f07105c5f86f4dc92b09eb1138775027c3", size = 128067, upload-time = "2025-11-17T01:51:24.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/8d/dbff05239043271dbeace563a7686212a3dd517864a35623fe4d4a64ca19/dirty_equals-0.11-py3-none-any.whl", hash = "sha256:b1d7093273fc2f9be12f443a8ead954ef6daaf6746fd42ef3a5616433ee85286", size = 28051, upload-time = "2025-11-17T01:51:22.849Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/c2/59/ae5cdac87a00962122ea37bb346d41b66aec05f9ce328fa2b9e216f8967b/frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47", size = 86967, upload-time = "2025-10-06T05:37:55.607Z" }, + { url = "https://files.pythonhosted.org/packages/8a/10/17059b2db5a032fd9323c41c39e9d1f5f9d0c8f04d1e4e3e788573086e61/frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca", size = 49984, upload-time = "2025-10-06T05:37:57.049Z" }, + { url = "https://files.pythonhosted.org/packages/4b/de/ad9d82ca8e5fa8f0c636e64606553c79e2b859ad253030b62a21fe9986f5/frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068", size = 50240, upload-time = "2025-10-06T05:37:58.145Z" }, + { url = "https://files.pythonhosted.org/packages/4e/45/3dfb7767c2a67d123650122b62ce13c731b6c745bc14424eea67678b508c/frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95", size = 219472, upload-time = "2025-10-06T05:37:59.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/bf/5bf23d913a741b960d5c1dac7c1985d8a2a1d015772b2d18ea168b08e7ff/frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459", size = 221531, upload-time = "2025-10-06T05:38:00.521Z" }, + { url = "https://files.pythonhosted.org/packages/d0/03/27ec393f3b55860859f4b74cdc8c2a4af3dbf3533305e8eacf48a4fd9a54/frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675", size = 219211, upload-time = "2025-10-06T05:38:01.842Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ad/0fd00c404fa73fe9b169429e9a972d5ed807973c40ab6b3cf9365a33d360/frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61", size = 231775, upload-time = "2025-10-06T05:38:03.384Z" }, + { url = "https://files.pythonhosted.org/packages/8a/c3/86962566154cb4d2995358bc8331bfc4ea19d07db1a96f64935a1607f2b6/frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6", size = 236631, upload-time = "2025-10-06T05:38:04.609Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/6ffad161dbd83782d2c66dc4d378a9103b31770cb1e67febf43aea42d202/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5", size = 218632, upload-time = "2025-10-06T05:38:05.917Z" }, + { url = "https://files.pythonhosted.org/packages/58/b2/4677eee46e0a97f9b30735e6ad0bf6aba3e497986066eb68807ac85cf60f/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3", size = 235967, upload-time = "2025-10-06T05:38:07.614Z" }, + { url = "https://files.pythonhosted.org/packages/05/f3/86e75f8639c5a93745ca7addbbc9de6af56aebb930d233512b17e46f6493/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1", size = 228799, upload-time = "2025-10-06T05:38:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/30/00/39aad3a7f0d98f5eb1d99a3c311215674ed87061aecee7851974b335c050/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178", size = 230566, upload-time = "2025-10-06T05:38:10.52Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4d/aa144cac44568d137846ddc4d5210fb5d9719eb1d7ec6fa2728a54b5b94a/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda", size = 217715, upload-time = "2025-10-06T05:38:11.832Z" }, + { url = "https://files.pythonhosted.org/packages/64/4c/8f665921667509d25a0dd72540513bc86b356c95541686f6442a3283019f/frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087", size = 39933, upload-time = "2025-10-06T05:38:13.061Z" }, + { url = "https://files.pythonhosted.org/packages/79/bd/bcc926f87027fad5e59926ff12d136e1082a115025d33c032d1cd69ab377/frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a", size = 44121, upload-time = "2025-10-06T05:38:14.572Z" }, + { url = "https://files.pythonhosted.org/packages/4c/07/9c2e4eb7584af4b705237b971b89a4155a8e57599c4483a131a39256a9a0/frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103", size = 40312, upload-time = "2025-10-06T05:38:15.699Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[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-aiohttp" +version = "0.1.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/2c/b894861cecf030fb45675ea24aa55b5722e97c602a163d872fca66c5a6d8/httpx_aiohttp-0.1.12.tar.gz", hash = "sha256:81feec51fd82c0ecfa0e9aaf1b1a6c2591260d5e2bcbeb7eb0277a78e610df2c", size = 275945, upload-time = "2025-12-12T10:12:15.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8d/85c9701e9af72ca132a1783e2a54364a90c6da832304416a30fc11196ab2/httpx_aiohttp-0.1.12-py3-none-any.whl", hash = "sha256:5b0eac39a7f360fa7867a60bcb46bb1024eada9c01cbfecdb54dc1edb3fb7141", size = 6367, upload-time = "2025-12-12T10:12:14.018Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-6-blooio-pydantic-v1' and extra != 'group-6-blooio-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-6-blooio-pydantic-v1' and extra != 'group-6-blooio-pydantic-v2'", +] +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 = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-6-blooio-pydantic-v1' and extra != 'group-6-blooio-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-6-blooio-pydantic-v1' and extra != 'group-6-blooio-pydantic-v2'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[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 = "multidict" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/63/7bdd4adc330abcca54c85728db2327130e49e52e8c3ce685cec44e0f2e9f/multidict-6.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349", size = 77153, upload-time = "2025-10-06T14:48:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/b6c35ff175ed1a3142222b78455ee31be71a8396ed3ab5280fbe3ebe4e85/multidict-6.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e", size = 44993, upload-time = "2025-10-06T14:48:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/e0/1f/064c77877c5fa6df6d346e68075c0f6998547afe952d6471b4c5f6a7345d/multidict-6.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3", size = 44607, upload-time = "2025-10-06T14:48:29.581Z" }, + { url = "https://files.pythonhosted.org/packages/04/7a/bf6aa92065dd47f287690000b3d7d332edfccb2277634cadf6a810463c6a/multidict-6.7.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046", size = 241847, upload-time = "2025-10-06T14:48:32.107Z" }, + { url = "https://files.pythonhosted.org/packages/94/39/297a8de920f76eda343e4ce05f3b489f0ab3f9504f2576dfb37b7c08ca08/multidict-6.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32", size = 242616, upload-time = "2025-10-06T14:48:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/39/3a/d0eee2898cfd9d654aea6cb8c4addc2f9756e9a7e09391cfe55541f917f7/multidict-6.7.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73", size = 222333, upload-time = "2025-10-06T14:48:35.9Z" }, + { url = "https://files.pythonhosted.org/packages/05/48/3b328851193c7a4240815b71eea165b49248867bbb6153a0aee227a0bb47/multidict-6.7.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc", size = 253239, upload-time = "2025-10-06T14:48:37.302Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ca/0706a98c8d126a89245413225ca4a3fefc8435014de309cf8b30acb68841/multidict-6.7.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62", size = 251618, upload-time = "2025-10-06T14:48:38.963Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/9c7992f245554d8b173f6f0a048ad24b3e645d883f096857ec2c0822b8bd/multidict-6.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84", size = 241655, upload-time = "2025-10-06T14:48:40.312Z" }, + { url = "https://files.pythonhosted.org/packages/31/79/26a85991ae67efd1c0b1fc2e0c275b8a6aceeb155a68861f63f87a798f16/multidict-6.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0", size = 239245, upload-time = "2025-10-06T14:48:41.848Z" }, + { url = "https://files.pythonhosted.org/packages/14/1e/75fa96394478930b79d0302eaf9a6c69f34005a1a5251ac8b9c336486ec9/multidict-6.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e", size = 233523, upload-time = "2025-10-06T14:48:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5e/085544cb9f9c4ad2b5d97467c15f856df8d9bac410cffd5c43991a5d878b/multidict-6.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4", size = 243129, upload-time = "2025-10-06T14:48:45.225Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c3/e9d9e2f20c9474e7a8fcef28f863c5cbd29bb5adce6b70cebe8bdad0039d/multidict-6.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648", size = 248999, upload-time = "2025-10-06T14:48:46.703Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3f/df171b6efa3239ae33b97b887e42671cd1d94d460614bfb2c30ffdab3b95/multidict-6.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111", size = 243711, upload-time = "2025-10-06T14:48:48.146Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2f/9b5564888c4e14b9af64c54acf149263721a283aaf4aa0ae89b091d5d8c1/multidict-6.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36", size = 237504, upload-time = "2025-10-06T14:48:49.447Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3a/0bd6ca0f7d96d790542d591c8c3354c1e1b6bfd2024d4d92dc3d87485ec7/multidict-6.7.0-cp310-cp310-win32.whl", hash = "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85", size = 41422, upload-time = "2025-10-06T14:48:50.789Z" }, + { url = "https://files.pythonhosted.org/packages/00/35/f6a637ea2c75f0d3b7c7d41b1189189acff0d9deeb8b8f35536bb30f5e33/multidict-6.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7", size = 46050, upload-time = "2025-10-06T14:48:51.938Z" }, + { url = "https://files.pythonhosted.org/packages/e7/b8/f7bf8329b39893d02d9d95cf610c75885d12fc0f402b1c894e1c8e01c916/multidict-6.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0", size = 43153, upload-time = "2025-10-06T14:48:53.146Z" }, + { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, + { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, + { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, + { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, + { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, + { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, + { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, + { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, + { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, + { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, + { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, + { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, + { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, + { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, + { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, + { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, + { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, + { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, + { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, + { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, + { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, + { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, + { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, + { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, + { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, + { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, + { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, + { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, + { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, + { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, + { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, + { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, + { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, + { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, + { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, + { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, + { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, + { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, + { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, + { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, + { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, + { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, + { url = "https://files.pythonhosted.org/packages/90/d7/4cf84257902265c4250769ac49f4eaab81c182ee9aff8bf59d2714dbb174/multidict-6.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c", size = 77073, upload-time = "2025-10-06T14:51:57.386Z" }, + { url = "https://files.pythonhosted.org/packages/6d/51/194e999630a656e76c2965a1590d12faa5cd528170f2abaa04423e09fe8d/multidict-6.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40", size = 44928, upload-time = "2025-10-06T14:51:58.791Z" }, + { url = "https://files.pythonhosted.org/packages/e5/6b/2a195373c33068c9158e0941d0b46cfcc9c1d894ca2eb137d1128081dff0/multidict-6.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851", size = 44581, upload-time = "2025-10-06T14:52:00.174Z" }, + { url = "https://files.pythonhosted.org/packages/69/7b/7f4f2e644b6978bf011a5fd9a5ebb7c21de3f38523b1f7897d36a1ac1311/multidict-6.7.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687", size = 239901, upload-time = "2025-10-06T14:52:02.416Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b5/952c72786710a031aa204a9adf7db66d7f97a2c6573889d58b9e60fe6702/multidict-6.7.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5", size = 240534, upload-time = "2025-10-06T14:52:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ef/109fe1f2471e4c458c74242c7e4a833f2d9fc8a6813cd7ee345b0bad18f9/multidict-6.7.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb", size = 219545, upload-time = "2025-10-06T14:52:06.208Z" }, + { url = "https://files.pythonhosted.org/packages/42/bd/327d91288114967f9fe90dc53de70aa3fec1b9073e46aa32c4828f771a87/multidict-6.7.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6", size = 251187, upload-time = "2025-10-06T14:52:08.049Z" }, + { url = "https://files.pythonhosted.org/packages/f4/13/a8b078ebbaceb7819fd28cd004413c33b98f1b70d542a62e6a00b74fb09f/multidict-6.7.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e", size = 249379, upload-time = "2025-10-06T14:52:09.831Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6d/ab12e1246be4d65d1f55de1e6f6aaa9b8120eddcfdd1d290439c7833d5ce/multidict-6.7.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e", size = 239241, upload-time = "2025-10-06T14:52:11.561Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/079a93625208c173b8fa756396814397c0fd9fee61ef87b75a748820b86e/multidict-6.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32", size = 237418, upload-time = "2025-10-06T14:52:13.671Z" }, + { url = "https://files.pythonhosted.org/packages/c9/29/03777c2212274aa9440918d604dc9d6af0e6b4558c611c32c3dcf1a13870/multidict-6.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c", size = 232987, upload-time = "2025-10-06T14:52:15.708Z" }, + { url = "https://files.pythonhosted.org/packages/d9/00/11188b68d85a84e8050ee34724d6ded19ad03975caebe0c8dcb2829b37bf/multidict-6.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84", size = 240985, upload-time = "2025-10-06T14:52:17.317Z" }, + { url = "https://files.pythonhosted.org/packages/df/0c/12eef6aeda21859c6cdf7d75bd5516d83be3efe3d8cc45fd1a3037f5b9dc/multidict-6.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329", size = 246855, upload-time = "2025-10-06T14:52:19.096Z" }, + { url = "https://files.pythonhosted.org/packages/69/f6/076120fd8bb3975f09228e288e08bff6b9f1bfd5166397c7ba284f622ab2/multidict-6.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e", size = 241804, upload-time = "2025-10-06T14:52:21.166Z" }, + { url = "https://files.pythonhosted.org/packages/5f/51/41bb950c81437b88a93e6ddfca1d8763569ae861e638442838c4375f7497/multidict-6.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4", size = 235321, upload-time = "2025-10-06T14:52:23.208Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cf/5bbd31f055199d56c1f6b04bbadad3ccb24e6d5d4db75db774fc6d6674b8/multidict-6.7.0-cp39-cp39-win32.whl", hash = "sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91", size = 41435, upload-time = "2025-10-06T14:52:24.735Z" }, + { url = "https://files.pythonhosted.org/packages/af/01/547ffe9c2faec91c26965c152f3fea6cff068b6037401f61d310cc861ff4/multidict-6.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f", size = 46193, upload-time = "2025-10-06T14:52:26.101Z" }, + { url = "https://files.pythonhosted.org/packages/27/77/cfa5461d1d2651d6fc24216c92b4a21d4e385a41c46e0d9f3b070675167b/multidict-6.7.0-cp39-cp39-win_arm64.whl", hash = "sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546", size = 43118, upload-time = "2025-10-06T14:52:27.876Z" }, + { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, +] + +[[package]] +name = "mypy" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/e3/034322d5a779685218ed69286c32faa505247f1f096251ef66c8fd203b08/mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03", size = 3352114, upload-time = "2025-07-14T20:34:30.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/31/e762baa3b73905c856d45ab77b4af850e8159dffffd86a52879539a08c6b/mypy-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8e08de6138043108b3b18f09d3f817a4783912e48828ab397ecf183135d84d6", size = 10998313, upload-time = "2025-07-14T20:33:24.519Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c1/25b2f0d46fb7e0b5e2bee61ec3a47fe13eff9e3c2f2234f144858bbe6485/mypy-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce4a17920ec144647d448fc43725b5873548b1aae6c603225626747ededf582d", size = 10128922, upload-time = "2025-07-14T20:34:06.414Z" }, + { url = "https://files.pythonhosted.org/packages/02/78/6d646603a57aa8a2886df1b8881fe777ea60f28098790c1089230cd9c61d/mypy-1.17.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ff25d151cc057fdddb1cb1881ef36e9c41fa2a5e78d8dd71bee6e4dcd2bc05b", size = 11913524, upload-time = "2025-07-14T20:33:19.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/19/dae6c55e87ee426fb76980f7e78484450cad1c01c55a1dc4e91c930bea01/mypy-1.17.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93468cf29aa9a132bceb103bd8475f78cacde2b1b9a94fd978d50d4bdf616c9a", size = 12650527, upload-time = "2025-07-14T20:32:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/86/e1/f916845a235235a6c1e4d4d065a3930113767001d491b8b2e1b61ca56647/mypy-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:98189382b310f16343151f65dd7e6867386d3e35f7878c45cfa11383d175d91f", size = 12897284, upload-time = "2025-07-14T20:33:38.168Z" }, + { url = "https://files.pythonhosted.org/packages/ae/dc/414760708a4ea1b096bd214d26a24e30ac5e917ef293bc33cdb6fe22d2da/mypy-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:c004135a300ab06a045c1c0d8e3f10215e71d7b4f5bb9a42ab80236364429937", size = 9506493, upload-time = "2025-07-14T20:34:01.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/82efb502b0b0f661c49aa21cfe3e1999ddf64bf5500fc03b5a1536a39d39/mypy-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d4fe5c72fd262d9c2c91c1117d16aac555e05f5beb2bae6a755274c6eec42be", size = 10914150, upload-time = "2025-07-14T20:31:51.985Z" }, + { url = "https://files.pythonhosted.org/packages/03/96/8ef9a6ff8cedadff4400e2254689ca1dc4b420b92c55255b44573de10c54/mypy-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96b196e5c16f41b4f7736840e8455958e832871990c7ba26bf58175e357ed61", size = 10039845, upload-time = "2025-07-14T20:32:30.527Z" }, + { url = "https://files.pythonhosted.org/packages/df/32/7ce359a56be779d38021d07941cfbb099b41411d72d827230a36203dbb81/mypy-1.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73a0ff2dd10337ceb521c080d4147755ee302dcde6e1a913babd59473904615f", size = 11837246, upload-time = "2025-07-14T20:32:01.28Z" }, + { url = "https://files.pythonhosted.org/packages/82/16/b775047054de4d8dbd668df9137707e54b07fe18c7923839cd1e524bf756/mypy-1.17.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cfcc1179c4447854e9e406d3af0f77736d631ec87d31c6281ecd5025df625d", size = 12571106, upload-time = "2025-07-14T20:34:26.942Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/fa33eaf29a606102c8d9ffa45a386a04c2203d9ad18bf4eef3e20c43ebc8/mypy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56f180ff6430e6373db7a1d569317675b0a451caf5fef6ce4ab365f5f2f6c3", size = 12759960, upload-time = "2025-07-14T20:33:42.882Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/3f5a29209f27e739ca57e6350bc6b783a38c7621bdf9cac3ab8a08665801/mypy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:eafaf8b9252734400f9b77df98b4eee3d2eecab16104680d51341c75702cad70", size = 9503888, upload-time = "2025-07-14T20:32:34.392Z" }, + { url = "https://files.pythonhosted.org/packages/12/e9/e6824ed620bbf51d3bf4d6cbbe4953e83eaf31a448d1b3cfb3620ccb641c/mypy-1.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f986f1cab8dbec39ba6e0eaa42d4d3ac6686516a5d3dccd64be095db05ebc6bb", size = 11086395, upload-time = "2025-07-14T20:34:11.452Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/a4afd1ae279707953be175d303f04a5a7bd7e28dc62463ad29c1c857927e/mypy-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:51e455a54d199dd6e931cd7ea987d061c2afbaf0960f7f66deef47c90d1b304d", size = 10120052, upload-time = "2025-07-14T20:33:09.897Z" }, + { url = "https://files.pythonhosted.org/packages/8a/71/19adfeac926ba8205f1d1466d0d360d07b46486bf64360c54cb5a2bd86a8/mypy-1.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3204d773bab5ff4ebbd1f8efa11b498027cd57017c003ae970f310e5b96be8d8", size = 11861806, upload-time = "2025-07-14T20:32:16.028Z" }, + { url = "https://files.pythonhosted.org/packages/0b/64/d6120eca3835baf7179e6797a0b61d6c47e0bc2324b1f6819d8428d5b9ba/mypy-1.17.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1051df7ec0886fa246a530ae917c473491e9a0ba6938cfd0ec2abc1076495c3e", size = 12744371, upload-time = "2025-07-14T20:33:33.503Z" }, + { url = "https://files.pythonhosted.org/packages/1f/dc/56f53b5255a166f5bd0f137eed960e5065f2744509dfe69474ff0ba772a5/mypy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f773c6d14dcc108a5b141b4456b0871df638eb411a89cd1c0c001fc4a9d08fc8", size = 12914558, upload-time = "2025-07-14T20:33:56.961Z" }, + { url = "https://files.pythonhosted.org/packages/69/ac/070bad311171badc9add2910e7f89271695a25c136de24bbafc7eded56d5/mypy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:1619a485fd0e9c959b943c7b519ed26b712de3002d7de43154a489a2d0fd817d", size = 9585447, upload-time = "2025-07-14T20:32:20.594Z" }, + { url = "https://files.pythonhosted.org/packages/be/7b/5f8ab461369b9e62157072156935cec9d272196556bdc7c2ff5f4c7c0f9b/mypy-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c41aa59211e49d717d92b3bb1238c06d387c9325d3122085113c79118bebb06", size = 11070019, upload-time = "2025-07-14T20:32:07.99Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/c49c9e5a2ac0badcc54beb24e774d2499748302c9568f7f09e8730e953fa/mypy-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e69db1fb65b3114f98c753e3930a00514f5b68794ba80590eb02090d54a5d4a", size = 10114457, upload-time = "2025-07-14T20:33:47.285Z" }, + { url = "https://files.pythonhosted.org/packages/89/0c/fb3f9c939ad9beed3e328008b3fb90b20fda2cddc0f7e4c20dbefefc3b33/mypy-1.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03ba330b76710f83d6ac500053f7727270b6b8553b0423348ffb3af6f2f7b889", size = 11857838, upload-time = "2025-07-14T20:33:14.462Z" }, + { url = "https://files.pythonhosted.org/packages/4c/66/85607ab5137d65e4f54d9797b77d5a038ef34f714929cf8ad30b03f628df/mypy-1.17.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:037bc0f0b124ce46bfde955c647f3e395c6174476a968c0f22c95a8d2f589bba", size = 12731358, upload-time = "2025-07-14T20:32:25.579Z" }, + { url = "https://files.pythonhosted.org/packages/73/d0/341dbbfb35ce53d01f8f2969facbb66486cee9804048bf6c01b048127501/mypy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c38876106cb6132259683632b287238858bd58de267d80defb6f418e9ee50658", size = 12917480, upload-time = "2025-07-14T20:34:21.868Z" }, + { url = "https://files.pythonhosted.org/packages/64/63/70c8b7dbfc520089ac48d01367a97e8acd734f65bd07813081f508a8c94c/mypy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:d30ba01c0f151998f367506fab31c2ac4527e6a7b2690107c7a7f9e3cb419a9c", size = 9589666, upload-time = "2025-07-14T20:34:16.841Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a0/6263dd11941231f688f0a8f2faf90ceac1dc243d148d314a089d2fe25108/mypy-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:63e751f1b5ab51d6f3d219fe3a2fe4523eaa387d854ad06906c63883fde5b1ab", size = 10988185, upload-time = "2025-07-14T20:33:04.797Z" }, + { url = "https://files.pythonhosted.org/packages/02/13/b8f16d6b0dc80277129559c8e7dbc9011241a0da8f60d031edb0e6e9ac8f/mypy-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f7fb09d05e0f1c329a36dcd30e27564a3555717cde87301fae4fb542402ddfad", size = 10120169, upload-time = "2025-07-14T20:32:38.84Z" }, + { url = "https://files.pythonhosted.org/packages/14/ef/978ba79df0d65af680e20d43121363cf643eb79b04bf3880d01fc8afeb6f/mypy-1.17.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b72c34ce05ac3a1361ae2ebb50757fb6e3624032d91488d93544e9f82db0ed6c", size = 11918121, upload-time = "2025-07-14T20:33:52.328Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/55ef70b104151a0d8280474f05268ff0a2a79be8d788d5e647257d121309/mypy-1.17.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434ad499ad8dde8b2f6391ddfa982f41cb07ccda8e3c67781b1bfd4e5f9450a8", size = 12648821, upload-time = "2025-07-14T20:32:59.631Z" }, + { url = "https://files.pythonhosted.org/packages/26/8c/7781fcd2e1eef48fbedd3a422c21fe300a8e03ed5be2eb4bd10246a77f4e/mypy-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f105f61a5eff52e137fd73bee32958b2add9d9f0a856f17314018646af838e97", size = 12896955, upload-time = "2025-07-14T20:32:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/03ac759dabe86e98ca7b6681f114f90ee03f3ff8365a57049d311bd4a4e3/mypy-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:ba06254a5a22729853209550d80f94e28690d5530c661f9416a68ac097b13fc4", size = 9512957, upload-time = "2025-07-14T20:33:28.619Z" }, + { url = "https://files.pythonhosted.org/packages/e3/fc/ee058cc4316f219078464555873e99d170bde1d9569abd833300dbeb484a/mypy-1.17.0-py3-none-any.whl", hash = "sha256:15d9d0018237ab058e5de3d8fce61b6fa72cc59cc78fd91f1b474bce12abf496", size = 2283195, upload-time = "2025-07-14T20:31:54.753Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, +] + +[[package]] +name = "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 = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/9b/01/0ebaec9003f5d619a7475165961f8e3083cf8644d704b60395df3601632d/propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff", size = 80277, upload-time = "2025-10-08T19:48:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/34/58/04af97ac586b4ef6b9026c3fd36ee7798b737a832f5d3440a4280dcebd3a/propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb", size = 45865, upload-time = "2025-10-08T19:48:37.859Z" }, + { url = "https://files.pythonhosted.org/packages/7c/19/b65d98ae21384518b291d9939e24a8aeac4fdb5101b732576f8f7540e834/propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac", size = 47636, upload-time = "2025-10-08T19:48:39.038Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/317048c6d91c356c7154dca5af019e6effeb7ee15fa6a6db327cc19e12b4/propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888", size = 201126, upload-time = "2025-10-08T19:48:40.774Z" }, + { url = "https://files.pythonhosted.org/packages/71/69/0b2a7a5a6ee83292b4b997dbd80549d8ce7d40b6397c1646c0d9495f5a85/propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc", size = 209837, upload-time = "2025-10-08T19:48:42.167Z" }, + { url = "https://files.pythonhosted.org/packages/a5/92/c699ac495a6698df6e497fc2de27af4b6ace10d8e76528357ce153722e45/propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a", size = 215578, upload-time = "2025-10-08T19:48:43.56Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ee/14de81c5eb02c0ee4f500b4e39c4e1bd0677c06e72379e6ab18923c773fc/propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88", size = 197187, upload-time = "2025-10-08T19:48:45.309Z" }, + { url = "https://files.pythonhosted.org/packages/1d/94/48dce9aaa6d8dd5a0859bad75158ec522546d4ac23f8e2f05fac469477dd/propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00", size = 193478, upload-time = "2025-10-08T19:48:47.743Z" }, + { url = "https://files.pythonhosted.org/packages/60/b5/0516b563e801e1ace212afde869a0596a0d7115eec0b12d296d75633fb29/propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0", size = 190650, upload-time = "2025-10-08T19:48:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/24/89/e0f7d4a5978cd56f8cd67735f74052f257dc471ec901694e430f0d1572fe/propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e", size = 200251, upload-time = "2025-10-08T19:48:51.4Z" }, + { url = "https://files.pythonhosted.org/packages/06/7d/a1fac863d473876ed4406c914f2e14aa82d2f10dd207c9e16fc383cc5a24/propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781", size = 200919, upload-time = "2025-10-08T19:48:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4e/f86a256ff24944cf5743e4e6c6994e3526f6acfcfb55e21694c2424f758c/propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183", size = 193211, upload-time = "2025-10-08T19:48:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/6e/3f/3fbad5f4356b068f1b047d300a6ff2c66614d7030f078cd50be3fec04228/propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19", size = 38314, upload-time = "2025-10-08T19:48:56.792Z" }, + { url = "https://files.pythonhosted.org/packages/a4/45/d78d136c3a3d215677abb886785aae744da2c3005bcb99e58640c56529b1/propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f", size = 41912, upload-time = "2025-10-08T19:48:57.995Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2a/b0632941f25139f4e58450b307242951f7c2717a5704977c6d5323a800af/propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938", size = 38450, upload-time = "2025-10-08T19:48:59.349Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "pydantic" +version = "1.10.26" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] +dependencies = [ + { name = "typing-extensions", marker = "extra == 'group-6-blooio-pydantic-v1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/da/fd89f987a376c807cd81ea0eff4589aade783bbb702637b4734ef2c743a2/pydantic-1.10.26.tar.gz", hash = "sha256:8c6aa39b494c5af092e690127c283d84f363ac36017106a9e66cb33a22ac412e", size = 357906, upload-time = "2025-12-18T15:47:46.557Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/08/2587a6d4314e7539eec84acd062cb7b037638edb57a0335d20e4c5b8878c/pydantic-1.10.26-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f7ae36fa0ecef8d39884120f212e16c06bb096a38f523421278e2f39c1784546", size = 2444588, upload-time = "2025-12-18T15:46:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/47/e6/10df5f08c105bcbb4adbee7d1108ff4b347702b110fed058f6a03f1c6b73/pydantic-1.10.26-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d95a76cf503f0f72ed7812a91de948440b2bf564269975738a4751e4fadeb572", size = 2255972, upload-time = "2025-12-18T15:46:31.72Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/fdb961e7adc2c31f394feba6f560ef2c74c446f0285e2c2eb87d2b7206c7/pydantic-1.10.26-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a943ce8e00ad708ed06a1d9df5b4fd28f5635a003b82a4908ece6f24c0b18464", size = 2857175, upload-time = "2025-12-18T15:46:34Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6c/f21e27dda475d4c562bd01b5874284dd3180f336c1e669413b743ca8b278/pydantic-1.10.26-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:465ad8edb29b15c10b779b16431fe8e77c380098badf6db367b7a1d3e572cf53", size = 2947001, upload-time = "2025-12-18T15:46:35.922Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f6/27ea206232cbb6ec24dc4e4e8888a9a734f96a1eaf13504be4b30ef26aa7/pydantic-1.10.26-cp310-cp310-win_amd64.whl", hash = "sha256:80e6be6272839c8a7641d26ad569ab77772809dd78f91d0068dc0fc97f071945", size = 2066217, upload-time = "2025-12-18T15:46:37.614Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c1/d521e64c8130e1ad9d22c270bed3fabcc0940c9539b076b639c88fd32a8d/pydantic-1.10.26-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:116233e53889bcc536f617e38c1b8337d7fa9c280f0fd7a4045947515a785637", size = 2428347, upload-time = "2025-12-18T15:46:39.41Z" }, + { url = "https://files.pythonhosted.org/packages/2c/08/f4b804a00c16e3ea994cb640a7c25c579b4f1fa674cde6a19fa0dfb0ae4f/pydantic-1.10.26-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3cfdd361addb6eb64ccd26ac356ad6514cee06a61ab26b27e16b5ed53108f77", size = 2212605, upload-time = "2025-12-18T15:46:41.006Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/0df4b9efef29bbc5e39f247fcba99060d15946b4463d82a5589cf7923d71/pydantic-1.10.26-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e4451951a9a93bf9a90576f3e25240b47ee49ab5236adccb8eff6ac943adf0f", size = 2753560, upload-time = "2025-12-18T15:46:43.215Z" }, + { url = "https://files.pythonhosted.org/packages/68/66/6ab6c1d3a116d05d2508fce64f96e35242938fac07544d611e11d0d363a0/pydantic-1.10.26-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9858ed44c6bea5f29ffe95308db9e62060791c877766c67dd5f55d072c8612b5", size = 2859235, upload-time = "2025-12-18T15:46:45.112Z" }, + { url = "https://files.pythonhosted.org/packages/61/4e/f1676bb0fcdf6ed2ce4670d7d1fc1d6c3a06d84497644acfbe02649503f1/pydantic-1.10.26-cp311-cp311-win_amd64.whl", hash = "sha256:ac1089f723e2106ebde434377d31239e00870a7563245072968e5af5cc4d33df", size = 2066646, upload-time = "2025-12-18T15:46:46.816Z" }, + { url = "https://files.pythonhosted.org/packages/02/6c/cd97a5a776c4515e6ee2ae81c2f2c5be51376dda6c31f965d7746ce0019f/pydantic-1.10.26-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:468d5b9cacfcaadc76ed0a4645354ab6f263ec01a63fb6d05630ea1df6ae453f", size = 2433795, upload-time = "2025-12-18T15:46:49.321Z" }, + { url = "https://files.pythonhosted.org/packages/47/12/de20affa30dcef728fcf9cc98e13ff4438c7a630de8d2f90eb38eba0891c/pydantic-1.10.26-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2c1b0b914be31671000ca25cf7ea17fcaaa68cfeadf6924529c5c5aa24b7ab1f", size = 2227387, upload-time = "2025-12-18T15:46:50.877Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/9d65dcc5b8c17ba590f1f9f486e9306346831902318b7ee93f63516f4003/pydantic-1.10.26-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15b13b9f8ba8867095769e1156e0d7fbafa1f65b898dd40fd1c02e34430973cb", size = 2629594, upload-time = "2025-12-18T15:46:53.42Z" }, + { url = "https://files.pythonhosted.org/packages/3f/76/acb41409356789e23e1a7ef58f93821410c96409183ce314ddb58d97f23e/pydantic-1.10.26-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad7025ca324ae263d4313998e25078dcaec5f9ed0392c06dedb57e053cc8086b", size = 2745305, upload-time = "2025-12-18T15:46:55.987Z" }, + { url = "https://files.pythonhosted.org/packages/22/72/a98c0c5e527a66057d969fedd61675223c7975ade61acebbca9f1abd6dc0/pydantic-1.10.26-cp312-cp312-win_amd64.whl", hash = "sha256:4482b299874dabb88a6c3759e3d85c6557c407c3b586891f7d808d8a38b66b9c", size = 1937647, upload-time = "2025-12-18T15:46:57.905Z" }, + { url = "https://files.pythonhosted.org/packages/28/b9/17a5a5a421c23ac27486b977724a42c9d5f8b7f0f4aab054251066223900/pydantic-1.10.26-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ae7913bb40a96c87e3d3f6fe4e918ef53bf181583de4e71824360a9b11aef1c", size = 2494599, upload-time = "2025-12-18T15:47:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8e/6e3bd4241076cf227b443d7577245dd5d181ecf40b3182fcb908bc8c197d/pydantic-1.10.26-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8154c13f58d4de5d3a856bb6c909c7370f41fb876a5952a503af6b975265f4ba", size = 2254391, upload-time = "2025-12-18T15:47:02.268Z" }, + { url = "https://files.pythonhosted.org/packages/a8/30/a1c4092eda2145ecbead6c92db489b223e101e1ba0da82576d0cf73dd422/pydantic-1.10.26-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8af0507bf6118b054a9765fb2e402f18a8b70c964f420d95b525eb711122d62", size = 2609445, upload-time = "2025-12-18T15:47:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/3a/2a/0491f1729ee4b7b6bc859ec22f69752f0c09bee1b66ac6f5f701136f34c3/pydantic-1.10.26-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dcb5a7318fb43189fde6af6f21ac7149c4bcbcfffc54bc87b5becddc46084847", size = 2732124, upload-time = "2025-12-18T15:47:07.464Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/b59f3b2f84e1df2b04ae768a1bb04d9f0288ff71b67cdcbb07683757b2c0/pydantic-1.10.26-cp313-cp313-win_amd64.whl", hash = "sha256:71cde228bc0600cf8619f0ee62db050d1880dcc477eba0e90b23011b4ee0f314", size = 1939888, upload-time = "2025-12-18T15:47:09.618Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8b/0c3dc02d4b97790b0f199bf933f677c14e7be4a8d21307c5f2daae06aa41/pydantic-1.10.26-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6b40730cc81d53d515dc0b8bb5c9b43fadb9bed46de4a3c03bd95e8571616dba", size = 2502689, upload-time = "2025-12-18T15:47:12.308Z" }, + { url = "https://files.pythonhosted.org/packages/d4/9d/d31aeea45542b2ae4b09ecba92b88aaba696b801c31919811aa979a1242d/pydantic-1.10.26-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c3bbb9c0eecdf599e4db9b372fa9cc55be12e80a0d9c6d307950a39050cb0e37", size = 2269494, upload-time = "2025-12-18T15:47:14.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/c1/3a4d069593283ca4dd0006039ba33644e21e432cddc09da706ac50441610/pydantic-1.10.26-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc2e3fe7bc4993626ef6b6fa855defafa1d6f8996aa1caef2deb83c5ac4d043a", size = 2620047, upload-time = "2025-12-18T15:47:17.089Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0e/340c3d29197d99c15ab04093d43bb9c9d0fd17c2a34b80cb9d36ed732b09/pydantic-1.10.26-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:36d9e46b588aaeb1dcd2409fa4c467fe0b331f3cc9f227b03a7a00643704e962", size = 2747625, upload-time = "2025-12-18T15:47:19.21Z" }, + { url = "https://files.pythonhosted.org/packages/1e/58/f12ab3727339b172c830b32151919456b67787cdfe8808b2568b322fb15c/pydantic-1.10.26-cp314-cp314-win_amd64.whl", hash = "sha256:81ce3c8616d12a7be31b4aadfd3434f78f6b44b75adbfaec2fe1ad4f7f999b8c", size = 1976436, upload-time = "2025-12-18T15:47:21.384Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8a/3a5a6267d5f03617b5c0f1985aa9fdfbafd33a50ef6dadd866a15ed4d123/pydantic-1.10.26-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:502b9d30d18a2dfaf81b7302f6ba0e5853474b1c96212449eb4db912cb604b7d", size = 2457039, upload-time = "2025-12-18T15:47:34.584Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/343ac0db26918a033ac6256c036d72c3b6eb1196b7de622e2e8a94b19079/pydantic-1.10.26-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0d8f6087bf697dec3bf7ffcd7fe8362674f16519f3151789f33cbe8f1d19fc15", size = 2266441, upload-time = "2025-12-18T15:47:36.807Z" }, + { url = "https://files.pythonhosted.org/packages/fc/36/1ab48136578608dba2f2a62e452f3db2083b474d4e49be5749c6ae0c123c/pydantic-1.10.26-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dd40a99c358419910c85e6f5d22f9c56684c25b5e7abc40879b3b4a52f34ae90", size = 2869383, upload-time = "2025-12-18T15:47:38.883Z" }, + { url = "https://files.pythonhosted.org/packages/a2/25/41dbf1bffc31eb242cece8080561a4133eaeb513372dec36a84477a3fb71/pydantic-1.10.26-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ce3293b86ca9f4125df02ff0a70be91bc7946522467cbd98e7f1493f340616ba", size = 2963582, upload-time = "2025-12-18T15:47:40.854Z" }, + { url = "https://files.pythonhosted.org/packages/61/2f/f072ae160a300c85eb9f059915101fd33dacf12d8df08c2b804acb3b95d1/pydantic-1.10.26-cp39-cp39-win_amd64.whl", hash = "sha256:1a4e3062b71ab1d5df339ba12c48f9ed5817c5de6cb92a961dd5c64bb32e7b96", size = 2075530, upload-time = "2025-12-18T15:47:43.181Z" }, + { url = "https://files.pythonhosted.org/packages/1f/98/556e82f00b98486def0b8af85da95e69d2be7e367cf2431408e108bc3095/pydantic-1.10.26-py3-none-any.whl", hash = "sha256:c43ad70dc3ce7787543d563792426a16fd7895e14be4b194b5665e36459dd917", size = 166975, upload-time = "2025-12-18T15:47:44.927Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-6-blooio-pydantic-v1' and extra != 'group-6-blooio-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-6-blooio-pydantic-v1' and extra != 'group-6-blooio-pydantic-v2'", +] +dependencies = [ + { name = "annotated-types", marker = "extra == 'group-6-blooio-pydantic-v2' or extra != 'group-6-blooio-pydantic-v1'" }, + { name = "pydantic-core", marker = "extra == 'group-6-blooio-pydantic-v2' or extra != 'group-6-blooio-pydantic-v1'" }, + { name = "typing-extensions", marker = "extra == 'group-6-blooio-pydantic-v2' or extra != 'group-6-blooio-pydantic-v1'" }, + { name = "typing-inspection", marker = "extra == 'group-6-blooio-pydantic-v2' or extra != 'group-6-blooio-pydantic-v1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "extra == 'group-6-blooio-pydantic-v2' or extra != 'group-6-blooio-pydantic-v1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/54/db/160dffb57ed9a3705c4cbcbff0ac03bdae45f1ca7d58ab74645550df3fbd/pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", size = 2107999, upload-time = "2025-11-04T13:42:03.885Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7d/88e7de946f60d9263cc84819f32513520b85c0f8322f9b8f6e4afc938383/pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", size = 1929745, upload-time = "2025-11-04T13:42:06.075Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c2/aef51e5b283780e85e99ff19db0f05842d2d4a8a8cd15e63b0280029b08f/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", size = 1920220, upload-time = "2025-11-04T13:42:08.457Z" }, + { url = "https://files.pythonhosted.org/packages/c7/97/492ab10f9ac8695cd76b2fdb24e9e61f394051df71594e9bcc891c9f586e/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", size = 2067296, upload-time = "2025-11-04T13:42:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/984149650e5269c59a2a4c41d234a9570adc68ab29981825cfaf4cfad8f4/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", size = 2231548, upload-time = "2025-11-04T13:42:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/71/0c/85bcbb885b9732c28bec67a222dbed5ed2d77baee1f8bba2002e8cd00c5c/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", size = 2362571, upload-time = "2025-11-04T13:42:16.208Z" }, + { url = "https://files.pythonhosted.org/packages/c0/4a/412d2048be12c334003e9b823a3fa3d038e46cc2d64dd8aab50b31b65499/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", size = 2068175, upload-time = "2025-11-04T13:42:18.911Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/c58b6a776b502d0a5540ad02e232514285513572060f0d78f7832ca3c98b/pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", size = 2177203, upload-time = "2025-11-04T13:42:22.578Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ae/f06ea4c7e7a9eead3d165e7623cd2ea0cb788e277e4f935af63fc98fa4e6/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", size = 2148191, upload-time = "2025-11-04T13:42:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/c1/57/25a11dcdc656bf5f8b05902c3c2934ac3ea296257cc4a3f79a6319e61856/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", size = 2343907, upload-time = "2025-11-04T13:42:27.683Z" }, + { url = "https://files.pythonhosted.org/packages/96/82/e33d5f4933d7a03327c0c43c65d575e5919d4974ffc026bc917a5f7b9f61/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", size = 2322174, upload-time = "2025-11-04T13:42:30.776Z" }, + { url = "https://files.pythonhosted.org/packages/81/45/4091be67ce9f469e81656f880f3506f6a5624121ec5eb3eab37d7581897d/pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", size = 1990353, upload-time = "2025-11-04T13:42:33.111Z" }, + { url = "https://files.pythonhosted.org/packages/44/8a/a98aede18db6e9cd5d66bcacd8a409fcf8134204cdede2e7de35c5a2c5ef/pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", size = 2015698, upload-time = "2025-11-04T13:42:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.399" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/9d/d91d5f6d26b2db95476fefc772e2b9a16d54c6bd0ea6bb5c1b6d635ab8b4/pyright-1.1.399.tar.gz", hash = "sha256:439035d707a36c3d1b443aec980bc37053fbda88158eded24b8eedcf1c7b7a1b", size = 3856954, upload-time = "2025-04-10T04:40:25.703Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/b5/380380c9e7a534cb1783c70c3e8ac6d1193c599650a55838d0557586796e/pyright-1.1.399-py3-none-any.whl", hash = "sha256:55f9a875ddf23c9698f24208c764465ffdfd38be6265f7faf9a176e1dc549f3b", size = 5592584, upload-time = "2025-04-10T04:40:23.502Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "(python_full_version < '3.10' and sys_platform == 'win32') or (python_full_version >= '3.10' and extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "packaging", marker = "python_full_version < '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "pluggy", marker = "python_full_version < '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "pygments", marker = "python_full_version < '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "tomli", marker = "python_full_version < '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-6-blooio-pydantic-v1' and extra != 'group-6-blooio-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-6-blooio-pydantic-v1' and extra != 'group-6-blooio-pydantic-v2'", +] +dependencies = [ + { name = "colorama", marker = "(python_full_version >= '3.10' and sys_platform == 'win32') or (python_full_version < '3.10' and extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "packaging", marker = "python_full_version >= '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "pluggy", marker = "python_full_version >= '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "pygments", marker = "python_full_version >= '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "tomli", marker = "python_full_version == '3.10.*' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-6-blooio-pydantic-v1' and extra != 'group-6-blooio-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-6-blooio-pydantic-v1' and extra != 'group-6-blooio-pydantic-v2'", +] +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version == '3.10.*' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.10' and python_full_version < '3.13') or (python_full_version < '3.10' and extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2') or (python_full_version >= '3.13' and extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "python_full_version < '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, +] +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 = "respx" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" }, + { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" }, + { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" }, + { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" }, + { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" }, + { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" }, + { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" }, + { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "time-machine" +version = "2.19.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "python-dateutil", marker = "python_full_version < '3.10' or (extra == 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/a4/1b5fdd165f61b67f445fac2a7feb0c655118edef429cd09ff5a8067f7f1d/time_machine-2.19.0.tar.gz", hash = "sha256:7c5065a8b3f2bbb449422c66ef71d114d3f909c276a6469642ecfffb6a0fcd29", size = 14576, upload-time = "2025-08-19T17:22:08.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/8f/19125611ebbcb3a14da14cd982b9eb4573e2733db60c9f1fbf6a39534f40/time_machine-2.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b5169018ef47206997b46086ce01881cd3a4666fd2998c9d76a87858ca3e49e9", size = 19659, upload-time = "2025-08-19T17:20:30.062Z" }, + { url = "https://files.pythonhosted.org/packages/74/da/9b0a928321e7822a3ff96dbd1eae089883848e30e9e1b149b85fb96ba56b/time_machine-2.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85bb7ed440fccf6f6d0c8f7d68d849e7c3d1f771d5e0b2cdf871fa6561da569f", size = 15157, upload-time = "2025-08-19T17:20:31.931Z" }, + { url = "https://files.pythonhosted.org/packages/36/ff/d7e943422038f5f2161fe2c2d791e64a45be691ef946020b20f3a6efc4d4/time_machine-2.19.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a3b12028af1cdc09ccd595be2168b7b26f206c1e190090b048598fbe278beb8e", size = 32860, upload-time = "2025-08-19T17:20:33.241Z" }, + { url = "https://files.pythonhosted.org/packages/fc/80/2b0f1070ed9808ee7da7a6da62a4a0b776957cb4d861578348f86446e778/time_machine-2.19.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c261f073086cf081d1443cbf7684148c662659d3d139d06b772bfe3fe7cc71a6", size = 34510, upload-time = "2025-08-19T17:20:34.221Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b4/48038691c8d89924b36c83335a73adeeb68c884f5a1da08a5b17b8a956f3/time_machine-2.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:011954d951230a9f1079f22b39ed1a3a9abb50ee297dfb8c557c46351659d94d", size = 36204, upload-time = "2025-08-19T17:20:35.163Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/60e8adb541df195e83cb74b720b2cfb1f22ed99c5a7f8abf2a9ab3442cb5/time_machine-2.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b0f83308b29c7872006803f2e77318874eb84d0654f2afe0e48e3822e7a2e39b", size = 34936, upload-time = "2025-08-19T17:20:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/72/e8cee59c6cd99dd3b25b8001a0253e779a286aa8f44d5b40777cbd66210b/time_machine-2.19.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:39733ef844e2984620ec9382a42d00cccc4757d75a5dd572be8c2572e86e50b9", size = 32932, upload-time = "2025-08-19T17:20:37.901Z" }, + { url = "https://files.pythonhosted.org/packages/2c/eb/83f300d93c1504965d944e03679f1c943a923bce2d0fdfadef0e2e22cc13/time_machine-2.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f8db99f6334432e9ffbf00c215caf2ae9773f17cec08304d77e9e90febc3507b", size = 34010, upload-time = "2025-08-19T17:20:39.202Z" }, + { url = "https://files.pythonhosted.org/packages/e1/77/f35f2500e04daac5033a22fbfd17e68467822b8406ee77966bf222ccaa26/time_machine-2.19.0-cp310-cp310-win32.whl", hash = "sha256:72bf66cd19e27ffd26516b9cbe676d50c2e0b026153289765dfe0cf406708128", size = 17121, upload-time = "2025-08-19T17:20:40.108Z" }, + { url = "https://files.pythonhosted.org/packages/db/df/32d3e0404be1760a64a44caab2af34b07e952bfe00a23134fea9ddba3e8a/time_machine-2.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:46f1c945934ce3d6b4f388b8e581fce7f87ec891ea90d7128e19520e434f96f0", size = 17957, upload-time = "2025-08-19T17:20:41.079Z" }, + { url = "https://files.pythonhosted.org/packages/66/df/598a71a1afb4b509a4587273b76590b16d9110a3e9106f01eedc68d02bb2/time_machine-2.19.0-cp310-cp310-win_arm64.whl", hash = "sha256:fb4897c7a5120a4fd03f0670f332d83b7e55645886cd8864a71944c4c2e5b35b", size = 16821, upload-time = "2025-08-19T17:20:41.967Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ed/4815ebcc9b6c14273f692b9be38a9b09eae52a7e532407cc61a51912b121/time_machine-2.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5ee91664880434d98e41585c3446dac7180ec408c786347451ddfca110d19296", size = 19342, upload-time = "2025-08-19T17:20:43.207Z" }, + { url = "https://files.pythonhosted.org/packages/ee/08/154cce8b11b60d8238b0b751b8901d369999f4e8f7c3a5f917caa5d95b0b/time_machine-2.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed3732b83a893d1c7b8cabde762968b4dc5680ee0d305b3ecca9bb516f4e3862", size = 14978, upload-time = "2025-08-19T17:20:44.134Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b7/b689d8c8eeca7af375cfcd64973e49e83aa817cc00f80f98548d42c0eb50/time_machine-2.19.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6ba0303e9cc9f7f947e344f501e26bedfb68fab521e3c2729d370f4f332d2d55", size = 30964, upload-time = "2025-08-19T17:20:45.366Z" }, + { url = "https://files.pythonhosted.org/packages/80/91/38bf9c79674e95ce32e23c267055f281dff651eec77ed32a677db3dc011a/time_machine-2.19.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2851825b524a988ee459c37c1c26bdfaa7eff78194efb2b562ea497a6f375b0a", size = 32606, upload-time = "2025-08-19T17:20:46.693Z" }, + { url = "https://files.pythonhosted.org/packages/19/4a/e9222d85d4de68975a5e799f539a9d32f3a134a9101fca0a61fa6aa33d68/time_machine-2.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68d32b09ecfd7fef59255c091e8e7c24dd117f882c4880b5c7ab8c5c32a98f89", size = 34405, upload-time = "2025-08-19T17:20:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/09480d608d42d6876f9ff74593cfc9197a7eb2c31381a74fb2b145575b65/time_machine-2.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60c46ab527bf2fa144b530f639cc9e12803524c9e1f111dc8c8f493bb6586eeb", size = 33181, upload-time = "2025-08-19T17:20:48.937Z" }, + { url = "https://files.pythonhosted.org/packages/84/64/f9359e000fad32d9066305c48abc527241d608bcdf77c19d67d66e268455/time_machine-2.19.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:56f26ab9f0201c453d18fe76bb7d1cf05fe58c1b9d9cb0c7d243d05132e01292", size = 31036, upload-time = "2025-08-19T17:20:50.276Z" }, + { url = "https://files.pythonhosted.org/packages/71/0d/fab2aacec71e3e482bd7fce0589381f9414a4a97f8766bddad04ad047b7b/time_machine-2.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6c806cf3c1185baa1d807b7f51bed0db7a6506832c961d5d1b4c94c775749bc0", size = 32145, upload-time = "2025-08-19T17:20:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/44/fb/faeba2405fb27553f7b28db441a500e2064ffdb2dcba001ee315fdd2c121/time_machine-2.19.0-cp311-cp311-win32.whl", hash = "sha256:b30039dfd89855c12138095bee39c540b4633cbc3684580d684ef67a99a91587", size = 17004, upload-time = "2025-08-19T17:20:52.38Z" }, + { url = "https://files.pythonhosted.org/packages/2f/84/87e483d660ca669426192969280366635c845c3154a9fe750be546ed3afc/time_machine-2.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:13ed8b34430f1de79905877f5600adffa626793ab4546a70a99fb72c6a3350d8", size = 17822, upload-time = "2025-08-19T17:20:53.348Z" }, + { url = "https://files.pythonhosted.org/packages/41/f4/ebf7bbf5047854a528adaf54a5e8780bc5f7f0104c298ab44566a3053bf8/time_machine-2.19.0-cp311-cp311-win_arm64.whl", hash = "sha256:cc29a50a0257d8750b08056b66d7225daab47606832dea1a69e8b017323bf511", size = 16680, upload-time = "2025-08-19T17:20:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/9b/aa/7e00614d339e4d687f6e96e312a1566022528427d237ec639df66c4547bc/time_machine-2.19.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c85cf437dc3c07429456d8d6670ac90ecbd8241dcd0fbf03e8db2800576f91ff", size = 19308, upload-time = "2025-08-19T17:20:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/ab/3c/bde3c757394f5bca2fbc1528d4117960a26c38f9b160bf471b38d2378d8f/time_machine-2.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d9238897e8ef54acdf59f5dff16f59ca0720e7c02d820c56b4397c11db5d3eb9", size = 15019, upload-time = "2025-08-19T17:20:56.204Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e0/8ca916dd918018352d377f1f5226ee071cfbeb7dbbde2b03d14a411ac2b1/time_machine-2.19.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e312c7d5d6bfffb96c6a7b39ff29e3046de100d7efaa3c01552654cfbd08f14c", size = 33079, upload-time = "2025-08-19T17:20:57.166Z" }, + { url = "https://files.pythonhosted.org/packages/48/69/184a0209f02dd0cb5e01e8d13cd4c97a5f389c4e3d09b95160dd676ad1e7/time_machine-2.19.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:714c40b2c90d1c57cc403382d5a9cf16e504cb525bfe9650095317da3c3d62b5", size = 34925, upload-time = "2025-08-19T17:20:58.117Z" }, + { url = "https://files.pythonhosted.org/packages/43/42/4bbf4309e8e57cea1086eb99052d97ff6ddecc1ab6a3b07aa4512f8bf963/time_machine-2.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2eaa1c675d500dc3ccae19e9fb1feff84458a68c132bbea47a80cc3dd2df7072", size = 36384, upload-time = "2025-08-19T17:20:59.108Z" }, + { url = "https://files.pythonhosted.org/packages/b1/af/9f510dc1719157348c1a2e87423aed406589070b54b503cb237d9bf3a4fe/time_machine-2.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e77a414e9597988af53b2b2e67242c9d2f409769df0d264b6d06fda8ca3360d4", size = 34881, upload-time = "2025-08-19T17:21:00.116Z" }, + { url = "https://files.pythonhosted.org/packages/ca/28/61764a635c70cc76c76ba582dfdc1a84834cddaeb96789023af5214426b2/time_machine-2.19.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cd93996970e11c382b04d4937c3cd0b0167adeef14725ece35aae88d8a01733c", size = 32931, upload-time = "2025-08-19T17:21:01.095Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e0/f028d93b266e6ade8aca5851f76ebbc605b2905cdc29981a2943b43e1a6c/time_machine-2.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8e20a6d8d6e23174bd7e931e134d9610b136db460b249d07e84ecdad029ec352", size = 34241, upload-time = "2025-08-19T17:21:02.052Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a6/36d1950ed1d3f613158024cf1dcc73db1d9ef0b9117cf51ef2e37dc06499/time_machine-2.19.0-cp312-cp312-win32.whl", hash = "sha256:95afc9bc65228b27be80c2756799c20b8eb97c4ef382a9b762b6d7888bc84099", size = 17021, upload-time = "2025-08-19T17:21:03.374Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0d/e2dce93355abda3cac69e77fe96566757e98b8fe7fdcbddce89c9ced3f5f/time_machine-2.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:e84909af950e2448f4e2562ea5759c946248c99ab380d2b47d79b62bd76fa236", size = 17857, upload-time = "2025-08-19T17:21:04.331Z" }, + { url = "https://files.pythonhosted.org/packages/eb/28/50ae6fb83b7feeeca7a461c0dc156cf7ef5e6ef594a600d06634fde6a2cb/time_machine-2.19.0-cp312-cp312-win_arm64.whl", hash = "sha256:0390a1ea9fa7e9d772a39b7c61b34fdcca80eb9ffac339cc0441c6c714c81470", size = 16677, upload-time = "2025-08-19T17:21:05.39Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b8/24ebce67aa531bae2cbe164bb3f4abc6467dc31f3aead35e77f5a075ea3e/time_machine-2.19.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5e172866753e6041d3b29f3037dc47c20525176a494a71bbd0998dfdc4f11f2f", size = 19373, upload-time = "2025-08-19T17:21:06.701Z" }, + { url = "https://files.pythonhosted.org/packages/53/a5/c9a5240fd2f845d3ff9fa26f8c8eaa29f7239af9d65007e61d212250f15b/time_machine-2.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f70f68379bd6f542ae6775cce9a4fa3dcc20bf7959c42eaef871c14469e18097", size = 15056, upload-time = "2025-08-19T17:21:07.667Z" }, + { url = "https://files.pythonhosted.org/packages/b9/92/66cce5d2fb2a5e68459aca85fd18a7e2d216f725988940cd83f96630f2f1/time_machine-2.19.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e69e0b0f694728a00e72891ef8dd00c7542952cb1c87237db594b6b27d504a96", size = 33172, upload-time = "2025-08-19T17:21:08.619Z" }, + { url = "https://files.pythonhosted.org/packages/ae/20/b499e9ab4364cd466016c33dcdf4f56629ca4c20b865bd4196d229f31d92/time_machine-2.19.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3ae0a8b869574301ec5637e32c270c7384cca5cd6e230f07af9d29271a7fa293", size = 35042, upload-time = "2025-08-19T17:21:09.622Z" }, + { url = "https://files.pythonhosted.org/packages/41/32/b252d3d32791eb16c07d553c820dbc33d9c7fa771de3d1c602190bded2b7/time_machine-2.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:554e4317de90e2f7605ff80d153c8bb56b38c0d0c0279feb17e799521e987b8c", size = 36535, upload-time = "2025-08-19T17:21:10.571Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/4d0470062b9742e1b040ab81bad04d1a5d1de09806507bb6188989cfa1a7/time_machine-2.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6567a5ec5538ed550539ac29be11b3cb36af1f9894e2a72940cba0292cc7c3c9", size = 34945, upload-time = "2025-08-19T17:21:11.538Z" }, + { url = "https://files.pythonhosted.org/packages/24/71/2f741b29d98b1c18f6777a32236497c3d3264b6077e431cea4695684c8a1/time_machine-2.19.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82e9ffe8dfff07b0d810a2ad015a82cd78c6a237f6c7cf185fa7f747a3256f8a", size = 33014, upload-time = "2025-08-19T17:21:12.858Z" }, + { url = "https://files.pythonhosted.org/packages/e8/83/ca8dba6106562843fd99f672e5aaf95badbc10f4f13f7cfe8d8640a7019d/time_machine-2.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e1c4e578cdd69b3531d8dd3fbcb92a0cd879dadb912ee37af99c3a9e3c0d285", size = 34350, upload-time = "2025-08-19T17:21:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/21/7f/34fe540450e18d0a993240100e4b86e8d03d831b92af8bb6ddb2662dc6fc/time_machine-2.19.0-cp313-cp313-win32.whl", hash = "sha256:72dbd4cbc3d96dec9dd281ddfbb513982102776b63e4e039f83afb244802a9e5", size = 17047, upload-time = "2025-08-19T17:21:14.874Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5d/c8be73df82c7ebe7cd133279670e89b8b110af3ce1412c551caa9d08e625/time_machine-2.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:e17e3e089ac95f9a145ce07ff615e3c85674f7de36f2d92aaf588493a23ffb4b", size = 17868, upload-time = "2025-08-19T17:21:15.819Z" }, + { url = "https://files.pythonhosted.org/packages/92/13/2dfd3b8fb285308f61cd7aa9bfa96f46ddf916e3549a0f0afd094c556599/time_machine-2.19.0-cp313-cp313-win_arm64.whl", hash = "sha256:149072aff8e3690e14f4916103d898ea0d5d9c95531b6aa0995251c299533f7b", size = 16710, upload-time = "2025-08-19T17:21:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/05/c1/deebb361727d2c5790f9d4d874be1b19afd41f4375581df465e6718b46a2/time_machine-2.19.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f3589fee1ed0ab6ee424a55b0ea1ec694c4ba64cc26895bcd7d99f3d1bc6a28a", size = 20053, upload-time = "2025-08-19T17:21:17.704Z" }, + { url = "https://files.pythonhosted.org/packages/45/e8/fe3376951e6118d8ec1d1f94066a169b791424fe4a26c7dfc069b153ee08/time_machine-2.19.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7887e85275c4975fe54df03dcdd5f38bd36be973adc68a8c77e17441c3b443d6", size = 15423, upload-time = "2025-08-19T17:21:18.668Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c7/f88d95cd1a87c650cf3749b4d64afdaf580297aa18ad7f4b44ec9d252dfc/time_machine-2.19.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ce0be294c209928563fcce1c587963e60ec803436cf1e181acd5bc1e425d554b", size = 39630, upload-time = "2025-08-19T17:21:19.645Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5d/65a5c48a65357e56ec6f032972e4abd1c02d4fca4b0717a3aaefd19014d4/time_machine-2.19.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a62fd1ab380012c86f4c042010418ed45eb31604f4bf4453e17c9fa60bc56a29", size = 41242, upload-time = "2025-08-19T17:21:20.979Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/fe5209e1615fde0a8cad6c4e857157b150333ed1fe31a7632b08cfe0ebdd/time_machine-2.19.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b25ec853a4530a5800731257f93206b12cbdee85ede964ebf8011b66086a7914", size = 44278, upload-time = "2025-08-19T17:21:21.984Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3a/a5e5fe9c5d614cde0a9387ff35e8dfd12c5ef6384e4c1a21b04e6e0b905d/time_machine-2.19.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a430e4d0e0556f021a9c78e9b9f68e5e8910bdace4aa34ed4d1a73e239ed9384", size = 42321, upload-time = "2025-08-19T17:21:23.755Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c5/56eca774e9162bc1ce59111d2bd69140dc8908c9478c92ec7bd15d547600/time_machine-2.19.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2415b7495ec4364c8067071e964fbadfe746dd4cdb43983f2f0bd6ebed13315c", size = 39270, upload-time = "2025-08-19T17:21:26.009Z" }, + { url = "https://files.pythonhosted.org/packages/9b/69/5dd0c420667578169a12acc8c8fd7452e8cfb181e41c9b4ac7e88fa36686/time_machine-2.19.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbfc6b90c10f288594e1bf89a728a98cc0030791fd73541bbdc6b090aff83143", size = 40193, upload-time = "2025-08-19T17:21:27.054Z" }, + { url = "https://files.pythonhosted.org/packages/75/a7/de974d421bd55c9355583427c2a38fb0237bb5fd6614af492ba89dacb2f9/time_machine-2.19.0-cp313-cp313t-win32.whl", hash = "sha256:16f5d81f650c0a4d117ab08036dc30b5f8b262e11a4a0becc458e7f1c011b228", size = 17542, upload-time = "2025-08-19T17:21:28.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/aa0d05becd5d06ae8d3f16d657dc8cc9400c8d79aef80299de196467ff12/time_machine-2.19.0-cp313-cp313t-win_amd64.whl", hash = "sha256:645699616ec14e147094f601e6ab9553ff6cea37fad9c42720a6d7ed04bcd5dc", size = 18703, upload-time = "2025-08-19T17:21:29.663Z" }, + { url = "https://files.pythonhosted.org/packages/1f/c0/f785a4c7c73aa176510f7c48b84b49c26be84af0d534deb222e0327f750e/time_machine-2.19.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b32daa965d13237536ea3afaa5ad61ade2b2d9314bc3a20196a0d2e1d7b57c6a", size = 17020, upload-time = "2025-08-19T17:21:30.653Z" }, + { url = "https://files.pythonhosted.org/packages/ed/97/c5fb51def06c0b2b6735332ad118ab35b4d9b85368792e5b638e99b1b686/time_machine-2.19.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:31cb43c8fd2d961f31bed0ff4e0026964d2b35e5de9e0fabbfecf756906d3612", size = 19360, upload-time = "2025-08-19T17:21:31.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4e/2d795f7d6b7f5205ffe737a05bb1cf19d8038233b797062b2ef412b8512b/time_machine-2.19.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bdf481a75afc6bff3e520db594501975b652f7def21cd1de6aa971d35ba644e6", size = 15033, upload-time = "2025-08-19T17:21:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/dd/32/9bad501e360b4e758c58fae616ca5f8c7ad974b343f2463a15b2bf77a366/time_machine-2.19.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:00bee4bb950ac6a08d62af78e4da0cf2b4fc2abf0de2320d0431bf610db06e7c", size = 33379, upload-time = "2025-08-19T17:21:33.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/45/eda0ca4d793dfd162478d6163759b1c6ce7f6e61daa7fd7d62b31f21f87f/time_machine-2.19.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f02199490906582302ce09edd32394fb393271674c75d7aa76c7a3245f16003", size = 35123, upload-time = "2025-08-19T17:21:34.945Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5a/97e16325442ae5731fcaac794f0a1ef9980eff8a5491e58201d7eb814a34/time_machine-2.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e35726c7ba625f844c13b1fc0d4f81f394eefaee1d3a094a9093251521f2ef15", size = 36588, upload-time = "2025-08-19T17:21:35.975Z" }, + { url = "https://files.pythonhosted.org/packages/e8/9d/bf0b2ccc930cc4a316f26f1c78d3f313cd0fa13bb7480369b730a8f129db/time_machine-2.19.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:304315023999cd401ff02698870932b893369e1cfeb2248d09f6490507a92e97", size = 35013, upload-time = "2025-08-19T17:21:37.017Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5a/39ac6a3078174f9715d88364871348b249631f12e76de1b862433b3f8862/time_machine-2.19.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9765d4f003f263ea8bfd90d2d15447ca4b3dfa181922cf6cf808923b02ac180a", size = 33303, upload-time = "2025-08-19T17:21:38.352Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ac/d8646baf9f95f2e792a6d7a7b35e92fca253c4a992afff801beafae0e5c2/time_machine-2.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7837ef3fd5911eb9b480909bb93d922737b6bdecea99dfcedb0a03807de9b2d3", size = 34440, upload-time = "2025-08-19T17:21:39.382Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8b/8b6568c5ae966d80ead03ab537be3c6acf2af06fb501c2d466a3162c6295/time_machine-2.19.0-cp314-cp314-win32.whl", hash = "sha256:4bb5bd43b1bdfac3007b920b51d8e761f024ed465cfeec63ac4296922a4ec428", size = 17162, upload-time = "2025-08-19T17:21:40.381Z" }, + { url = "https://files.pythonhosted.org/packages/46/a5/211c1ab4566eba5308b2dc001b6349e3a032e3f6afa67ca2f27ea6b27af5/time_machine-2.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:f583bbd0aa8ab4a7c45a684bf636d9e042d466e30bcbae1d13e7541e2cbe7207", size = 18040, upload-time = "2025-08-19T17:21:41.363Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fc/4c2fb705f6371cb83824da45a8b967514a922fc092a0ef53979334d97a70/time_machine-2.19.0-cp314-cp314-win_arm64.whl", hash = "sha256:f379c6f8a6575a8284592179cf528ce89373f060301323edcc44f1fa1d37be12", size = 16752, upload-time = "2025-08-19T17:21:42.336Z" }, + { url = "https://files.pythonhosted.org/packages/79/ab/6437d18f31c666b5116c97572a282ac2590a82a0a9867746a6647eaf4613/time_machine-2.19.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a3b8981f9c663b0906b05ab4d0ca211fae4b63b47c6ec26de5374fe56c836162", size = 20057, upload-time = "2025-08-19T17:21:43.35Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a2/e03639ec2ba7200328bbcad8a2b2b1d5fccca9cceb9481b164a1cabdcb33/time_machine-2.19.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8e9c6363893e7f52c226afbebb23e825259222d100e67dfd24c8a6d35f1a1907", size = 15430, upload-time = "2025-08-19T17:21:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ff/39e63a48e840f3e36ce24846ee51dd99c6dba635659b1750a2993771e88e/time_machine-2.19.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:206fcd6c9a6f00cac83db446ad1effc530a8cec244d2780af62db3a2d0a9871b", size = 39622, upload-time = "2025-08-19T17:21:45.821Z" }, + { url = "https://files.pythonhosted.org/packages/9a/2e/ee5ac79c4954768705801e54817c7d58e07e25a0bb227e775f501f3e2122/time_machine-2.19.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf33016a1403c123373ffaeff25e26e69d63bf2c63b6163932efed94160db7ef", size = 41235, upload-time = "2025-08-19T17:21:46.783Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3e/9af5f39525e779185c77285b8bbae15340eeeaa0afb33d458bc8b47d459b/time_machine-2.19.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9247c4bb9bbd3ff584ef4efbdec8efd9f37aa08bcfc4728bde1e489c2cb445bd", size = 44276, upload-time = "2025-08-19T17:21:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/572c7443cc27140bbeae3947279bbd4a120f9e8622253a20637f260b7813/time_machine-2.19.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:77f9bb0b86758d1f2d9352642c874946ad5815df53ef4ca22eb9d532179fe50d", size = 42330, upload-time = "2025-08-19T17:21:48.881Z" }, + { url = "https://files.pythonhosted.org/packages/cf/24/1a81c2e08ee7dae13ec8ceed27a29afa980c3d63852e42f1e023bf0faa03/time_machine-2.19.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0b529e262df3b9c449f427385f4d98250828c879168c2e00eec844439f40b370", size = 39281, upload-time = "2025-08-19T17:21:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/d2/60/6f0d6e5108978ca1a2a4ffb4d1c7e176d9199bb109fd44efe2680c60b52a/time_machine-2.19.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9199246e31cdc810e5d89cb71d09144c4d745960fdb0824da4994d152aca3303", size = 40201, upload-time = "2025-08-19T17:21:50.953Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/3ea4951e8293b0643feb98c0b9a176fa822154f1810835db3f282968ab10/time_machine-2.19.0-cp314-cp314t-win32.whl", hash = "sha256:0fe81bae55b7aefc2c2a34eb552aa82e6c61a86b3353a3c70df79b9698cb02ca", size = 17743, upload-time = "2025-08-19T17:21:51.948Z" }, + { url = "https://files.pythonhosted.org/packages/e4/8b/cd802884ca8a98e2b6cdc2397d57dd12ff8a7d1481e06fc3fad3d4e7e5ff/time_machine-2.19.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7253791b8d7e7399fbeed7a8193cb01bc004242864306288797056badbdaf80b", size = 18956, upload-time = "2025-08-19T17:21:52.997Z" }, + { url = "https://files.pythonhosted.org/packages/c6/49/cabb1593896082fd55e34768029b8b0ca23c9be8b2dc127e0fc14796d33e/time_machine-2.19.0-cp314-cp314t-win_arm64.whl", hash = "sha256:536bd1ac31ab06a1522e7bf287602188f502dc19d122b1502c4f60b1e8efac79", size = 17068, upload-time = "2025-08-19T17:21:54.064Z" }, + { url = "https://files.pythonhosted.org/packages/d6/05/0608376c3167afe6cf7cdfd2b05c142ea4c42616eee9ba06d1799965806a/time_machine-2.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8bb00b30ec9fe56d01e9812df1ffe39f331437cef9bfaebcc81c83f7f8f8ee2", size = 19659, upload-time = "2025-08-19T17:21:55.426Z" }, + { url = "https://files.pythonhosted.org/packages/11/c4/72eb8c7b36830cf36c51d7bc2f1ac313d68881c3a58040fb6b42c4523d20/time_machine-2.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d821c60efc08a97cc11e5482798e6fd5eba5c0f22a02db246b50895dbdc0de41", size = 15153, upload-time = "2025-08-19T17:21:56.505Z" }, + { url = "https://files.pythonhosted.org/packages/89/1a/0782e1f5c8ab8809ebd992709e1bb69d67600191baa023af7a5d32023a3c/time_machine-2.19.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb051aec7b3b6e96a200d911c225901e6133ff3da11e470e24111a53bbc13637", size = 32555, upload-time = "2025-08-19T17:21:57.74Z" }, + { url = "https://files.pythonhosted.org/packages/94/b0/8ef58e2f6321851d5900ca3d18044938832c2ed42a2ac7570ca6aa29768a/time_machine-2.19.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe59909d95a2ef5e01ce3354fdea3908404c2932c2069f00f66dff6f27e9363e", size = 34185, upload-time = "2025-08-19T17:21:59.361Z" }, + { url = "https://files.pythonhosted.org/packages/82/74/ce0c9867f788c1fb22c417ec1aae47a24117e53d51f6ff97d7c6ca5392f6/time_machine-2.19.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29e84b8682645b16eb6f9e8ec11c35324ad091841a11cf4fc3fc7f6119094c89", size = 35917, upload-time = "2025-08-19T17:22:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/d2/70/6f97a8f552dbaa66feb10170b5726dab74bc531673d1ed9d6f271547e54c/time_machine-2.19.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a11f1c0e0d06023dc01614c964e256138913551d3ae6dca5148f79081156336", size = 34584, upload-time = "2025-08-19T17:22:01.447Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/cf139088ce537c15d7f03cf56ec317d3a5cfb520e30aa711ea0248d0ae8a/time_machine-2.19.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:57a235a6307c54df50e69f1906e2f199e47da91bde4b886ee05aff57fe4b6bf6", size = 32608, upload-time = "2025-08-19T17:22:02.548Z" }, + { url = "https://files.pythonhosted.org/packages/b1/17/0ec41ef7a30c6753fb226a28b74162b264b35724905ced4098f2f5076ded/time_machine-2.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:426aba552f7af9604adad9ef570c859af7c1081d878db78089fac159cd911b0a", size = 33686, upload-time = "2025-08-19T17:22:03.606Z" }, + { url = "https://files.pythonhosted.org/packages/b0/19/586f15159083ec84f178d494c60758c46603b00c9641b04deb63f1950128/time_machine-2.19.0-cp39-cp39-win32.whl", hash = "sha256:67772c7197a3a712d1b970ed545c6e98db73524bd90e245fd3c8fa7ad7630768", size = 17133, upload-time = "2025-08-19T17:22:04.989Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c2/bfe4b906a9fe0bf2d011534314212ed752d6b8f392c9c82f6ac63dccc5ab/time_machine-2.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:011d7859089263204dc5fdf83dce7388f986fe833c9381d6106b4edfda2ebd3e", size = 17972, upload-time = "2025-08-19T17:22:06.026Z" }, + { url = "https://files.pythonhosted.org/packages/5d/73/182343eba05aa5787732aaa68f3b3feb5e40ddf86b928ae941be45646393/time_machine-2.19.0-cp39-cp39-win_arm64.whl", hash = "sha256:e1af66550fa4685434f00002808a525f176f1f92746646c0019bb86fbff48b27", size = 16820, upload-time = "2025-08-19T17:22:07.227Z" }, +] + +[[package]] +name = "time-machine" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-6-blooio-pydantic-v1' and extra == 'group-6-blooio-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-6-blooio-pydantic-v1' and extra != 'group-6-blooio-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-6-blooio-pydantic-v1' and extra != 'group-6-blooio-pydantic-v2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/02/fc/37b02f6094dbb1f851145330460532176ed2f1dc70511a35828166c41e52/time_machine-3.2.0.tar.gz", hash = "sha256:a4ddd1cea17b8950e462d1805a42b20c81eb9aafc8f66b392dd5ce997e037d79", size = 14804, upload-time = "2025-12-17T23:33:02.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/31/6bf41cb4a326230518d9b76c910dfc11d4fc23444d1cbfdf2d7652bd99f4/time_machine-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:68142c070e78b62215d8029ec7394905083a4f9aacb0a2a11514ce70b5951b13", size = 19447, upload-time = "2025-12-17T23:31:30.181Z" }, + { url = "https://files.pythonhosted.org/packages/fa/14/d71ce771712e1cbfa15d8c24452225109262b16cb6caaf967e9f60662b67/time_machine-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:161bbd0648802ffdfcb4bb297ecb26b3009684a47d3a4dedb90bc549df4fa2ad", size = 15432, upload-time = "2025-12-17T23:31:31.381Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/dcb43a11f8029561996fad58ff9d3dc5e6d7f32b74f0745a2965d7e4b4f3/time_machine-3.2.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1359ba8c258be695ba69253bc84db882fd616fe69b426cc6056536da2c7bf68e", size = 32956, upload-time = "2025-12-17T23:31:32.469Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/d802cd3c335c414f9b11b479f7459aa72df5de6485c799966cfdf8856d53/time_machine-3.2.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c85b169998ca2c24a78fb214586ec11c4cad56d9c38f55ad8326235cb481c884", size = 34556, upload-time = "2025-12-17T23:31:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/85/ee/51ad553514ab0b940c7c82c6e1519dd10fd06ac07b32039a1d153ef09c88/time_machine-3.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65b9367cb8a10505bc8f67da0da514ba20fa816fc47e11f434f7c60350322b4c", size = 36101, upload-time = "2025-12-17T23:31:35.462Z" }, + { url = "https://files.pythonhosted.org/packages/11/39/938b111b5bb85a2b07502d0f9d8a704fc75bd760d62e76bce23c89ed16c9/time_machine-3.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9faca6a0f1973d7df3233c951fc2a11ff0c54df74087d8aaf41ae3deb19d0893", size = 34905, upload-time = "2025-12-17T23:31:36.543Z" }, + { url = "https://files.pythonhosted.org/packages/dd/50/0951f73b23e76455de0b4a3a58ac5a24bd8d10489624b1c5e03f10c6fc0b/time_machine-3.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:213b1ada7f385d467e598999b642eda4a8e89ae10ad5dc4f5d8f672cbf604261", size = 33012, upload-time = "2025-12-17T23:31:37.967Z" }, + { url = "https://files.pythonhosted.org/packages/4f/95/5304912d3dcecc4e14ed222dbe0396352efdf8497534abc3c9edd67a7528/time_machine-3.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:160b6afd94c39855af04d39c58e4cf602406abd6d79427ab80e830ea71789cfb", size = 34104, upload-time = "2025-12-17T23:31:39.449Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/af56518652ec7adac4ced193b7a42c4ff354fef28a412b3b5ffa5763aead/time_machine-3.2.0-cp310-cp310-win32.whl", hash = "sha256:c15d9ac257c78c124d112e4fc91fa9f3dcb004bdda913c19f0e7368d713cf080", size = 17468, upload-time = "2025-12-17T23:31:40.432Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/0213f00ca3cf6fe1c9fdbd7fd467e801052fc85534f30c0e4684bd474190/time_machine-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:3bf0f428487f93b8fe9d27aa01eccc817885da3290b467341b4a4a795e1d1891", size = 18313, upload-time = "2025-12-17T23:31:41.617Z" }, + { url = "https://files.pythonhosted.org/packages/77/e4/811f96aa7a634b2b264d9a476f3400e710744dda503b4ad87a5c76db32c9/time_machine-3.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:347f6be2129fcd35b1c94b9387fcb2cbe7949b1e649228c5f22949a811b78976", size = 17037, upload-time = "2025-12-17T23:31:42.924Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e1/03aae5fbaa53859f665094af696338fc7cae733d926a024af69982712350/time_machine-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c188a9dda9fcf975022f1b325b466651b96a4dfc223c523ed7ed8d979f9bf3e8", size = 19143, upload-time = "2025-12-17T23:31:44.258Z" }, + { url = "https://files.pythonhosted.org/packages/75/8f/98cb17bebb52b22ff4ec26984dd44280f9c71353c3bae0640a470e6683e5/time_machine-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17245f1cc2dd13f9d63a174be59bb2684a9e5e0a112ab707e37be92068cd655f", size = 15273, upload-time = "2025-12-17T23:31:45.246Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2f/ca11e4a7897234bb9331fcc5f4ed4714481ba4012370cc79a0ae8c42ea0a/time_machine-3.2.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9bd1de1996e76efd36ae15970206c5089fb3728356794455bd5cd8d392b5537", size = 31049, upload-time = "2025-12-17T23:31:46.613Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ad/d17d83a59943094e6b6c6a3743caaf6811b12203c3e07a30cc7bcc2ab7ee/time_machine-3.2.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98493cd50e8b7f941eab69b9e18e697ad69db1a0ec1959f78f3d7b0387107e5c", size = 32632, upload-time = "2025-12-17T23:31:47.72Z" }, + { url = "https://files.pythonhosted.org/packages/71/50/d60576d047a0dfb5638cdfb335e9c3deb6e8528544fa0b3966a8480f72b7/time_machine-3.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31f2a33d595d9f91eb9bc7f157f0dc5721f5789f4c4a9e8b852cdedb2a7d9b16", size = 34289, upload-time = "2025-12-17T23:31:48.913Z" }, + { url = "https://files.pythonhosted.org/packages/fa/fe/4afa602dbdebddde6d0ea4a7fe849e49b9bb85dc3fb415725a87ccb4b471/time_machine-3.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9f78ac4213c10fbc44283edd1a29cfb7d3382484f4361783ddc057292aaa1889", size = 33175, upload-time = "2025-12-17T23:31:50.611Z" }, + { url = "https://files.pythonhosted.org/packages/0d/87/c152e23977c1d7d7c94eb3ed3ea45cc55971796205125c6fdff40db2c60f/time_machine-3.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c1326b09e947b360926d529a96d1d9e126ce120359b63b506ecdc6ee20755c23", size = 31170, upload-time = "2025-12-17T23:31:51.645Z" }, + { url = "https://files.pythonhosted.org/packages/80/af/54acf51d0f3ade3b51eab73df6192937c9a938753ef5456dff65eb8630be/time_machine-3.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9f2949f03d15264cc15c38918a2cda8966001f0f4ebe190cbfd9c56d91aed8ac", size = 32292, upload-time = "2025-12-17T23:31:52.803Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bc/3745963f36e75661a807196428639327a366f4332f35f1f775c074d4062f/time_machine-3.2.0-cp311-cp311-win32.whl", hash = "sha256:6dfe48e0499e6e16751476b9799e67be7514e6ef04cdf39571ef95a279645831", size = 17349, upload-time = "2025-12-17T23:31:54.19Z" }, + { url = "https://files.pythonhosted.org/packages/82/a2/057469232a99d1f5a0160ae7c5bae7b095c9168b333dd598fcbcfbc1c87b/time_machine-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:809bdf267a29189c304154873620fe0bcc0c9513295fa46b19e21658231c4915", size = 18191, upload-time = "2025-12-17T23:31:55.472Z" }, + { url = "https://files.pythonhosted.org/packages/79/d8/bf9c8de57262ee7130d92a6ed49ed6a6e40a36317e46979428d373630c12/time_machine-3.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:a3f4c17fa90f54902a3f8692c75caf67be87edc3429eeb71cb4595da58198f8e", size = 16905, upload-time = "2025-12-17T23:31:56.658Z" }, + { url = "https://files.pythonhosted.org/packages/71/8b/080c8eedcd67921a52ba5bd0e075362062509ab63c86fc1a0442fad241a6/time_machine-3.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc4bee5b0214d7dc4ebc91f4a4c600f1a598e9b5606ac751f42cb6f6740b1dbb", size = 19255, upload-time = "2025-12-17T23:31:58.057Z" }, + { url = "https://files.pythonhosted.org/packages/66/17/0e5291e9eb705bf8a5a1305f826e979af307bbeb79def4ddbf4b3f9a81e0/time_machine-3.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ca036304b4460ae2fdc1b52dd8b1fa7cf1464daa427fc49567413c09aa839c1", size = 15360, upload-time = "2025-12-17T23:31:59.048Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/9ab87b71d2e2b62463b9b058b7ae7ac09fb57f8fcd88729dec169d304340/time_machine-3.2.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5442735b41d7a2abc2f04579b4ca6047ed4698a8338a4fec92c7c9423e7938cb", size = 33029, upload-time = "2025-12-17T23:32:00.413Z" }, + { url = "https://files.pythonhosted.org/packages/4b/26/b5ca19da6f25ea905b3e10a0ea95d697c1aeba0404803a43c68f1af253e6/time_machine-3.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:97da3e971e505cb637079fb07ab0bcd36e33279f8ecac888ff131f45ef1e4d8d", size = 34579, upload-time = "2025-12-17T23:32:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/79/ca/6ac7ad5f10ea18cc1d9de49716ba38c32132c7b64532430d92ef240c116b/time_machine-3.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3cdda6dee4966e38aeb487309bb414c6cb23a81fc500291c77a8fcd3098832e7", size = 35961, upload-time = "2025-12-17T23:32:02.521Z" }, + { url = "https://files.pythonhosted.org/packages/33/67/390dd958bed395ab32d79a9fe61fe111825c0dd4ded54dbba7e867f171e6/time_machine-3.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:33d9efd302a6998bcc8baa4d84f259f8a4081105bd3d7f7af7f1d0abd3b1c8aa", size = 34668, upload-time = "2025-12-17T23:32:03.585Z" }, + { url = "https://files.pythonhosted.org/packages/da/57/c88fff034a4e9538b3ae7c68c9cfb283670b14d17522c5a8bc17d29f9a4b/time_machine-3.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3a0b0a33971f14145853c9bd95a6ab0353cf7e0019fa2a7aa1ae9fddfe8eab50", size = 32891, upload-time = "2025-12-17T23:32:04.656Z" }, + { url = "https://files.pythonhosted.org/packages/2d/70/ebbb76022dba0fec8f9156540fc647e4beae1680c787c01b1b6200e56d70/time_machine-3.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2d0be9e5f22c38082d247a2cdcd8a936504e9db60b7b3606855fb39f299e9548", size = 34080, upload-time = "2025-12-17T23:32:06.146Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/2ca9e7af3df540dc1c79e3de588adeddb7dcc2107829248e6969c4f14167/time_machine-3.2.0-cp312-cp312-win32.whl", hash = "sha256:3f74623648b936fdce5f911caf386c0a0b579456410975de8c0dfeaaffece1d8", size = 17371, upload-time = "2025-12-17T23:32:07.164Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ce/21d23efc9c2151939af1b7ee4e60d86d661b74ef32b8eaa148f6fe8c899c/time_machine-3.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:34e26a41d994b5e4b205136a90e9578470386749cc9a2ecf51ca18f83ce25e23", size = 18132, upload-time = "2025-12-17T23:32:08.447Z" }, + { url = "https://files.pythonhosted.org/packages/2f/34/c2b70be483accf6db9e5d6c3139bce3c38fe51f898ccf64e8d3fe14fbf4d/time_machine-3.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:0615d3d82c418d6293f271c348945c5091a71f37e37173653d5c26d0e74b13a8", size = 16930, upload-time = "2025-12-17T23:32:09.477Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cd/43ad5efc88298af3c59b66769cea7f055567a85071579ed40536188530c1/time_machine-3.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c421a8eb85a4418a7675a41bf8660224318c46cc62e4751c8f1ceca752059090", size = 19318, upload-time = "2025-12-17T23:32:10.518Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f6/084010ef7f4a3f38b5a4900923d7c85b29e797655c4f6ee4ce54d903cca8/time_machine-3.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4e758f7727d0058c4950c66b58200c187072122d6f7a98b610530a4233ea7b", size = 15390, upload-time = "2025-12-17T23:32:11.625Z" }, + { url = "https://files.pythonhosted.org/packages/25/aa/1cabb74134f492270dc6860cb7865859bf40ecf828be65972827646e91ad/time_machine-3.2.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:154bd3f75c81f70218b2585cc12b60762fb2665c507eec5ec5037d8756d9b4e0", size = 33115, upload-time = "2025-12-17T23:32:13.219Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/78c5d7dfa366924eb4dbfcc3fc917c39a4280ca234b12819cc1f16c03d88/time_machine-3.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d50cfe5ebea422c896ad8d278af9648412b7533b8ea6adeeee698a3fd9b1d3b7", size = 34705, upload-time = "2025-12-17T23:32:14.29Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/d5e877c24541f674c6869ff6e9c56833369796010190252e92c9d7ae5f0f/time_machine-3.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:636576501724bd6a9124e69d86e5aef263479e89ef739c5db361469f0463a0a1", size = 36104, upload-time = "2025-12-17T23:32:15.354Z" }, + { url = "https://files.pythonhosted.org/packages/22/1c/d4bae72f388f67efc9609f89b012e434bb19d9549c7a7b47d6c7d9e5c55d/time_machine-3.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40e6f40c57197fcf7ec32d2c563f4df0a82c42cdcc3cab27f688e98f6060df10", size = 34765, upload-time = "2025-12-17T23:32:16.434Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c3/ac378cf301d527d8dfad2f0db6bad0dfb1ab73212eaa56d6b96ee5d9d20b/time_machine-3.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a1bcf0b846bbfc19a79bc19e3fa04d8c7b1e8101c1b70340ffdb689cd801ea53", size = 33010, upload-time = "2025-12-17T23:32:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/06/35/7ce897319accda7a6970b288a9a8c52d25227342a7508505a2b3d235b649/time_machine-3.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ae55a56c179f4fe7a62575ad5148b6ed82f6c7e5cf2f9a9ec65f2f5b067db5f5", size = 34185, upload-time = "2025-12-17T23:32:18.566Z" }, + { url = "https://files.pythonhosted.org/packages/bf/28/f922022269749cb02eee2b62919671153c4088994fa955a6b0e50327ff81/time_machine-3.2.0-cp313-cp313-win32.whl", hash = "sha256:a66fe55a107e46916007a391d4030479df8864ec6ad6f6a6528221befc5c886e", size = 17397, upload-time = "2025-12-17T23:32:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/fd87cde397f4a7bea493152f0aca8fd569ec709cad9e0f2ca7011eb8c7f7/time_machine-3.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:30c9ce57165df913e4f74e285a8ab829ff9b7aa3e5ec0973f88f642b9a7b3d15", size = 18139, upload-time = "2025-12-17T23:32:20.991Z" }, + { url = "https://files.pythonhosted.org/packages/75/81/b8ce58233addc5d7d54d2fabc49dcbc02d79e3f079d150aa1bec3d5275ef/time_machine-3.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:89cad7e179e9bdcc84dcf09efe52af232c4cc7a01b3de868356bbd59d95bd9b8", size = 16964, upload-time = "2025-12-17T23:32:22.075Z" }, + { url = "https://files.pythonhosted.org/packages/67/e7/487f0ba5fe6c58186a5e1af2a118dfa2c160fedb37ef53a7e972d410408e/time_machine-3.2.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:59d71545e62525a4b85b6de9ab5c02ee3c61110fd7f636139914a2335dcbfc9c", size = 20000, upload-time = "2025-12-17T23:32:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/e1/17/eb2c0054c8d44dd42df84ccd434539249a9c7d0b8eb53f799be2102500ab/time_machine-3.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:999672c621c35362bc28e03ca0c7df21500195540773c25993421fd8d6cc5003", size = 15657, upload-time = "2025-12-17T23:32:24.125Z" }, + { url = "https://files.pythonhosted.org/packages/43/21/93443b5d1dd850f8bb9442e90d817a9033dcce6bfbdd3aabbb9786251c80/time_machine-3.2.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5faf7397f0580c7b9d67288522c8d7863e85f0cffadc0f1fccdb2c3dfce5783e", size = 39216, upload-time = "2025-12-17T23:32:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/9f/9e/18544cf8acc72bb1dc03762231c82ecc259733f4bb6770a7bbe5cd138603/time_machine-3.2.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3dd886ec49f1fa5a00e844f5947e5c0f98ce574750c24b7424c6f77fc1c3e87", size = 40764, upload-time = "2025-12-17T23:32:26.643Z" }, + { url = "https://files.pythonhosted.org/packages/27/f7/9fe9ce2795636a3a7467307af6bdf38bb613ddb701a8a5cd50ec713beb5e/time_machine-3.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da0ecd96bc7bbe450acaaabe569d84e81688f1be8ad58d1470e42371d145fb53", size = 43526, upload-time = "2025-12-17T23:32:27.693Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/a93e975ba9dec22e87ec92d18c28e67d36bd536f9119ffa439b2892b0c9c/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:158220e946c1c4fb8265773a0282c88c35a7e3bb5d78e3561214e3b3231166f3", size = 41727, upload-time = "2025-12-17T23:32:28.985Z" }, + { url = "https://files.pythonhosted.org/packages/5f/fb/e3633e5a6bbed1c76bb2e9810dabc2f8467532ffcd29b9aed404b473061a/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c1aee29bc54356f248d5d7dfdd131e12ca825e850a08c0ebdb022266d073013", size = 38952, upload-time = "2025-12-17T23:32:30.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/3d/02e9fb2526b3d6b1b45bc8e4d912d95d1cd699d1a3f6df985817d37a0600/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c8ed2224f09d25b1c2fc98683613aca12f90f682a427eabb68fc824d27014e4a", size = 39829, upload-time = "2025-12-17T23:32:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/85/c8/c14265212436da8e0814c45463987b3f57de3eca4de023cc2eabb0c62ef3/time_machine-3.2.0-cp313-cp313t-win32.whl", hash = "sha256:3498719f8dab51da76d29a20c1b5e52ee7db083dddf3056af7fa69c1b94e1fe6", size = 17852, upload-time = "2025-12-17T23:32:32.079Z" }, + { url = "https://files.pythonhosted.org/packages/1d/bc/8acb13cf6149f47508097b158a9a8bec9ec4530a70cb406124e8023581f5/time_machine-3.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e0d90bee170b219e1d15e6a58164aa808f5170090e4f090bd0670303e34181b1", size = 18918, upload-time = "2025-12-17T23:32:33.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/87/c443ee508c2708fd2514ccce9052f5e48888783ce690506919629ebc8eb0/time_machine-3.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:051de220fdb6e20d648111bbad423d9506fdbb2e44d4429cef3dc0382abf1fc2", size = 17261, upload-time = "2025-12-17T23:32:34.446Z" }, + { url = "https://files.pythonhosted.org/packages/61/70/b4b980d126ed155c78d1879c50d60c8dcbd47bd11cb14ee7be50e0dfc07f/time_machine-3.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:1398980c017fe5744d66f419e0115ee48a53b00b146d738e1416c225eb610b82", size = 19303, upload-time = "2025-12-17T23:32:35.796Z" }, + { url = "https://files.pythonhosted.org/packages/73/73/eaa33603c69a68fe2b6f54f9dd75481693d62f1d29676531002be06e2d1c/time_machine-3.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4f8f4e35f4191ef70c2ab8ff490761ee9051b891afce2bf86dde3918eb7b537b", size = 15431, upload-time = "2025-12-17T23:32:37.244Z" }, + { url = "https://files.pythonhosted.org/packages/76/10/b81e138e86cc7bab40cdb59d294b341e172201f4a6c84bb0ec080407977a/time_machine-3.2.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6db498686ecf6163c5aa8cf0bcd57bbe0f4081184f247edf3ee49a2612b584f9", size = 33206, upload-time = "2025-12-17T23:32:38.713Z" }, + { url = "https://files.pythonhosted.org/packages/d3/72/4deab446b579e8bd5dca91de98595c5d6bd6a17ce162abf5c5f2ce40d3d8/time_machine-3.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:027c1807efb74d0cd58ad16524dec94212fbe900115d70b0123399883657ac0f", size = 34792, upload-time = "2025-12-17T23:32:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/2c/39/439c6b587ddee76d533fe972289d0646e0a5520e14dc83d0a30aeb5565f7/time_machine-3.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92432610c05676edd5e6946a073c6f0c926923123ce7caee1018dc10782c713d", size = 36187, upload-time = "2025-12-17T23:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/4b/db/2da4368db15180989bab83746a857bde05ad16e78f326801c142bb747a06/time_machine-3.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c25586b62480eb77ef3d953fba273209478e1ef49654592cd6a52a68dfe56a67", size = 34855, upload-time = "2025-12-17T23:32:42.817Z" }, + { url = "https://files.pythonhosted.org/packages/88/84/120a431fee50bc4c241425bee4d3a4910df4923b7ab5f7dff1bf0c772f08/time_machine-3.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6bf3a2fa738d15e0b95d14469a0b8ea42635467408d8b490e263d5d45c9a177f", size = 33222, upload-time = "2025-12-17T23:32:43.94Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ea/89cfda82bb8c57ff91bb9a26751aa234d6d90e9b4d5ab0ad9dce0f9f0329/time_machine-3.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ce76b82276d7ad2a66cdc85dad4df19d1422b69183170a34e8fbc4c3f35502f7", size = 34270, upload-time = "2025-12-17T23:32:45.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/aa/235357da4f69a51a8d35fcbfcfa77cdc7dc24f62ae54025006570bda7e2d/time_machine-3.2.0-cp314-cp314-win32.whl", hash = "sha256:14d6778273c543441863dff712cd1d7803dee946b18de35921eb8df10714539d", size = 17544, upload-time = "2025-12-17T23:32:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/6c8405a7276be79693b792cff22ce41067ec05db26a7d02f2d5b06324434/time_machine-3.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbf821da96dbc80d349fa9e7c36e670b41d68a878d28c8850057992fed430eef", size = 18423, upload-time = "2025-12-17T23:32:47.468Z" }, + { url = "https://files.pythonhosted.org/packages/d9/03/a3cf419e20c35fc203c6e4fed48b5b667c1a2b4da456d9971e605f73ecef/time_machine-3.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:71c75d71f8e68abc8b669bca26ed2ddd558430a6c171e32b8620288565f18c0e", size = 17050, upload-time = "2025-12-17T23:32:48.91Z" }, + { url = "https://files.pythonhosted.org/packages/86/a1/142de946dc4393f910bf4564b5c3ba819906e1f49b06c9cb557519c849e4/time_machine-3.2.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4e374779021446fc2b5c29d80457ec9a3b1a5df043dc2aae07d7c1415d52323c", size = 19991, upload-time = "2025-12-17T23:32:49.933Z" }, + { url = "https://files.pythonhosted.org/packages/ee/62/7f17def6289901f94726921811a16b9adce46e666362c75d45730c60274f/time_machine-3.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:122310a6af9c36e9a636da32830e591e7923e8a07bdd0a43276c3a36c6821c90", size = 15707, upload-time = "2025-12-17T23:32:50.969Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d3/3502fb9bd3acb159c18844b26c43220201a0d4a622c0c853785d07699a92/time_machine-3.2.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ba3eeb0f018cc362dd8128befa3426696a2e16dd223c3fb695fde184892d4d8c", size = 39207, upload-time = "2025-12-17T23:32:52.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/be/8b27f4aa296fda14a5a2ad7f588ddd450603c33415ab3f8e85b2f1a44678/time_machine-3.2.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:77d38ba664b381a7793f8786efc13b5004f0d5f672dae814430445b8202a67a6", size = 40764, upload-time = "2025-12-17T23:32:53.167Z" }, + { url = "https://files.pythonhosted.org/packages/42/cd/fe4c4e5c8ab6d48fab3624c32be9116fb120173a35fe67e482e5cf68b3d2/time_machine-3.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f09abeb8f03f044d72712207e0489a62098ad3ad16dac38927fcf80baca4d6a7", size = 43508, upload-time = "2025-12-17T23:32:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/28/5a3ba2fce85b97655a425d6bb20a441550acd2b304c96b2c19d3839f721a/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6b28367ce4f73987a55e230e1d30a57a3af85da8eb1a140074eb6e8c7e6ef19f", size = 41712, upload-time = "2025-12-17T23:32:55.781Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/e38084be7fdabb4835db68a3a47e58c34182d79fc35df1ecbe0db2c5359f/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:903c7751c904581da9f7861c3015bed7cdc40047321291d3694a3cdc783bbca3", size = 38939, upload-time = "2025-12-17T23:32:56.867Z" }, + { url = "https://files.pythonhosted.org/packages/40/d0/ad3feb0a392ef4e0c08bc32024950373ddc0669002cbdcbb9f3bf0c2d114/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:528217cad85ede5f85c8bc78b0341868d3c3cfefc6ecb5b622e1cacb6c73247b", size = 39837, upload-time = "2025-12-17T23:32:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/5b/9e/5f4b2ea63b267bd78f3245e76f5528836611b5f2d30b5e7300a722fe4428/time_machine-3.2.0-cp314-cp314t-win32.whl", hash = "sha256:75724762ffd517e7e80aaec1fad1ff5a7414bd84e2b3ee7a0bacfeb67c14926e", size = 18091, upload-time = "2025-12-17T23:32:59.403Z" }, + { url = "https://files.pythonhosted.org/packages/39/6f/456b1f4d2700ae02b19eba830f870596a4b89b74bac3b6c80666f1b108c5/time_machine-3.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2526abbd053c5bca898d1b3e7898eec34626b12206718d8c7ce88fd12c1c9c5c", size = 19208, upload-time = "2025-12-17T23:33:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2f/22/8063101427ecd3d2652aada4d21d0876b07a3dc789125bca2ee858fec3ed/time_machine-3.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7f2fb6784b414edbe2c0b558bfaab0c251955ba27edd62946cce4a01675a992c", size = 17359, upload-time = "2025-12-17T23:33:01.54Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "extra == 'group-6-blooio-pydantic-v2' or extra != 'group-6-blooio-pydantic-v1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/43/a2204825342f37c337f5edb6637040fa14e365b2fcc2346960201d457579/yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", size = 140517, upload-time = "2025-10-06T14:08:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/674f3e6f02266428c56f704cd2501c22f78e8b2eeb23f153117cc86fb28a/yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", size = 93495, upload-time = "2025-10-06T14:08:46.2Z" }, + { url = "https://files.pythonhosted.org/packages/b8/12/5b274d8a0f30c07b91b2f02cba69152600b47830fcfb465c108880fcee9c/yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", size = 94400, upload-time = "2025-10-06T14:08:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7f/df1b6949b1fa1aa9ff6de6e2631876ad4b73c4437822026e85d8acb56bb1/yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", size = 347545, upload-time = "2025-10-06T14:08:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/84/09/f92ed93bd6cd77872ab6c3462df45ca45cd058d8f1d0c9b4f54c1704429f/yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", size = 319598, upload-time = "2025-10-06T14:08:51.215Z" }, + { url = "https://files.pythonhosted.org/packages/c3/97/ac3f3feae7d522cf7ccec3d340bb0b2b61c56cb9767923df62a135092c6b/yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", size = 363893, upload-time = "2025-10-06T14:08:53.144Z" }, + { url = "https://files.pythonhosted.org/packages/06/49/f3219097403b9c84a4d079b1d7bda62dd9b86d0d6e4428c02d46ab2c77fc/yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", size = 371240, upload-time = "2025-10-06T14:08:55.036Z" }, + { url = "https://files.pythonhosted.org/packages/35/9f/06b765d45c0e44e8ecf0fe15c9eacbbde342bb5b7561c46944f107bfb6c3/yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", size = 346965, upload-time = "2025-10-06T14:08:56.722Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/599e7cea8d0fcb1694323b0db0dda317fa3162f7b90166faddecf532166f/yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", size = 342026, upload-time = "2025-10-06T14:08:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/95/6f/9dfd12c8bc90fea9eab39832ee32ea48f8e53d1256252a77b710c065c89f/yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", size = 335637, upload-time = "2025-10-06T14:09:00.506Z" }, + { url = "https://files.pythonhosted.org/packages/57/2e/34c5b4eb9b07e16e873db5b182c71e5f06f9b5af388cdaa97736d79dd9a6/yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", size = 359082, upload-time = "2025-10-06T14:09:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/31/71/fa7e10fb772d273aa1f096ecb8ab8594117822f683bab7d2c5a89914c92a/yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", size = 357811, upload-time = "2025-10-06T14:09:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/26/da/11374c04e8e1184a6a03cf9c8f5688d3e5cec83ed6f31ad3481b3207f709/yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", size = 351223, upload-time = "2025-10-06T14:09:05.401Z" }, + { url = "https://files.pythonhosted.org/packages/82/8f/e2d01f161b0c034a30410e375e191a5d27608c1f8693bab1a08b089ca096/yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", size = 82118, upload-time = "2025-10-06T14:09:11.148Z" }, + { url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", size = 86852, upload-time = "2025-10-06T14:09:12.958Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", size = 82012, upload-time = "2025-10-06T14:09:14.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, + { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/94/fd/6480106702a79bcceda5fd9c63cb19a04a6506bd5ce7fd8d9b63742f0021/yarl-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748", size = 141301, upload-time = "2025-10-06T14:12:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/42/e1/6d95d21b17a93e793e4ec420a925fe1f6a9342338ca7a563ed21129c0990/yarl-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859", size = 93864, upload-time = "2025-10-06T14:12:21.05Z" }, + { url = "https://files.pythonhosted.org/packages/32/58/b8055273c203968e89808413ea4c984988b6649baabf10f4522e67c22d2f/yarl-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9", size = 94706, upload-time = "2025-10-06T14:12:23.287Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/d7bfbc28a88c2895ecd0da6a874def0c147de78afc52c773c28e1aa233a3/yarl-1.22.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054", size = 347100, upload-time = "2025-10-06T14:12:28.527Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e8/37a1e7b99721c0564b1fc7b0a4d1f595ef6fb8060d82ca61775b644185f7/yarl-1.22.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b", size = 318902, upload-time = "2025-10-06T14:12:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ef/34724449d7ef2db4f22df644f2dac0b8a275d20f585e526937b3ae47b02d/yarl-1.22.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60", size = 363302, upload-time = "2025-10-06T14:12:32.295Z" }, + { url = "https://files.pythonhosted.org/packages/8a/04/88a39a5dad39889f192cce8d66cc4c58dbeca983e83f9b6bf23822a7ed91/yarl-1.22.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890", size = 370816, upload-time = "2025-10-06T14:12:34.01Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1f/5e895e547129413f56c76be2c3ce4b96c797d2d0ff3e16a817d9269b12e6/yarl-1.22.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba", size = 346465, upload-time = "2025-10-06T14:12:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/11/13/a750e9fd6f9cc9ed3a52a70fe58ffe505322f0efe0d48e1fd9ffe53281f5/yarl-1.22.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca", size = 341506, upload-time = "2025-10-06T14:12:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/3c/67/bb6024de76e7186611ebe626aec5b71a2d2ecf9453e795f2dbd80614784c/yarl-1.22.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba", size = 335030, upload-time = "2025-10-06T14:12:39.775Z" }, + { url = "https://files.pythonhosted.org/packages/a2/be/50b38447fd94a7992996a62b8b463d0579323fcfc08c61bdba949eef8a5d/yarl-1.22.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b", size = 358560, upload-time = "2025-10-06T14:12:41.547Z" }, + { url = "https://files.pythonhosted.org/packages/e2/89/c020b6f547578c4e3dbb6335bf918f26e2f34ad0d1e515d72fd33ac0c635/yarl-1.22.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e", size = 357290, upload-time = "2025-10-06T14:12:43.861Z" }, + { url = "https://files.pythonhosted.org/packages/8c/52/c49a619ee35a402fa3a7019a4fa8d26878fec0d1243f6968bbf516789578/yarl-1.22.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8", size = 350700, upload-time = "2025-10-06T14:12:46.868Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c9/f5042d87777bf6968435f04a2bbb15466b2f142e6e47fa4f34d1a3f32f0c/yarl-1.22.0-cp39-cp39-win32.whl", hash = "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b", size = 82323, upload-time = "2025-10-06T14:12:48.633Z" }, + { url = "https://files.pythonhosted.org/packages/fd/58/d00f7cad9eba20c4eefac2682f34661d1d1b3a942fc0092eb60e78cfb733/yarl-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed", size = 87145, upload-time = "2025-10-06T14:12:50.241Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a3/70904f365080780d38b919edd42d224b8c4ce224a86950d2eaa2a24366ad/yarl-1.22.0-cp39-cp39-win_arm64.whl", hash = "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2", size = 82173, upload-time = "2025-10-06T14:12:51.869Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]