From 20785b127663e4972d2b2910ec9ee86e68c1829c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 07:19:22 +0000 Subject: [PATCH 01/10] feat(api): manual updates --- .stats.yml | 4 ++-- src/blooio/types/chats/message_send_params.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index 84cbe08..c528af3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-da8c56bcab4e4ad076aacfdd5f7d5650de8ad6c3a4bb4be17ee3dca2e14724bc.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-9977c183d7c79cffceaa822571a2d33ea0b8f3b7522afb3e6037641b0d9ab843.yml openapi_spec_hash: 11d9b0407bffe03ac9ca3a91f11b2c3d -config_hash: e54325f70a68da7960475794affd2d67 +config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/src/blooio/types/chats/message_send_params.py b/src/blooio/types/chats/message_send_params.py index 7268fce..35c4d1c 100644 --- a/src/blooio/types/chats/message_send_params.py +++ b/src/blooio/types/chats/message_send_params.py @@ -9,7 +9,7 @@ from ..._utils import PropertyInfo from .link_preview_param import LinkPreviewParam -__all__ = ["MessageSendParams", "Attachment", "AttachmentUnionMember1", "Part"] +__all__ = ["MessageSendParams", "Attachment", "AttachmentUnionObjectVariant1", "Part"] class MessageSendParams(TypedDict, total=False): @@ -64,13 +64,13 @@ class MessageSendParams(TypedDict, total=False): idempotency_key: Annotated[str, PropertyInfo(alias="Idempotency-Key")] -class AttachmentUnionMember1(TypedDict, total=False): +class AttachmentUnionObjectVariant1(TypedDict, total=False): url: Required[str] name: str -Attachment: TypeAlias = Union[str, AttachmentUnionMember1] +Attachment: TypeAlias = Union[str, AttachmentUnionObjectVariant1] class Part(TypedDict, total=False): From a0d34cfa8a353c08ff314e3bbc2fb15d6e1ec10d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 02:26:23 +0000 Subject: [PATCH 02/10] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index c528af3..6af5371 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-9977c183d7c79cffceaa822571a2d33ea0b8f3b7522afb3e6037641b0d9ab843.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-d20a6894e43fc5f309725d0a11b326a41749ec949fdff6d3f33267358e40d4a7.yml openapi_spec_hash: 11d9b0407bffe03ac9ca3a91f11b2c3d config_hash: 839e191496287972d18d39dcab8b19b7 From 07380c6a86dda7dbb1d633a4bb3e032e9b869102 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 02:29:22 +0000 Subject: [PATCH 03/10] chore(internal): reformat pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9172b37..d958dc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -154,7 +154,7 @@ show_error_codes = true # # We also exclude our `tests` as mypy doesn't always infer # types correctly and Pyright will still catch any type errors. -exclude = ['src/blooio/_files.py', '_dev/.*.py', 'tests/.*'] +exclude = ["src/blooio/_files.py", "_dev/.*.py", "tests/.*"] strict_equality = true implicit_reexport = true From 6126d3d6cbf2f395a465df8dac3edda83933248a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 02:02:18 +0000 Subject: [PATCH 04/10] fix(client): add missing f-string prefix in file type error message --- src/blooio/_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blooio/_files.py b/src/blooio/_files.py index 903ab5d..6344011 100644 --- a/src/blooio/_files.py +++ b/src/blooio/_files.py @@ -99,7 +99,7 @@ async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles elif is_sequence_t(files): files = [(key, await _async_transform_file(file)) for key, file in files] else: - raise TypeError("Unexpected file type input {type(files)}, expected mapping or sequence") + raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") return files From 7ab7fb636a69da2d1dcd67dca110ff627d380723 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 02:12:28 +0000 Subject: [PATCH 05/10] feat(api): api update --- .stats.yml | 4 ++-- src/blooio/resources/chats/typing.py | 16 ++++++++++++++++ src/blooio/types/chats/typing_response.py | 7 +++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 6af5371..59c867a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-d20a6894e43fc5f309725d0a11b326a41749ec949fdff6d3f33267358e40d4a7.yml -openapi_spec_hash: 11d9b0407bffe03ac9ca3a91f11b2c3d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-efd0b1a3742ab66cdefbcadb5a11628de9aa18d356e3730c666628ed8f4ba8fa.yml +openapi_spec_hash: 90f50b5ef3d3e845cc2c20aba392119e config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/src/blooio/resources/chats/typing.py b/src/blooio/resources/chats/typing.py index ac08cb3..f9576e2 100644 --- a/src/blooio/resources/chats/typing.py +++ b/src/blooio/resources/chats/typing.py @@ -58,6 +58,10 @@ def start( The indicator shows the recipient that you are typing. + **RCS limitation:** typing indicators are only delivered for iMessage chats — + the RCS protocol does not carry composing state. Calls against RCS-routed chats + return 200 with a `warning` field and have no visible effect on the recipient. + Args: extra_headers: Send extra headers @@ -91,6 +95,10 @@ def stop( """ Stop the typing indicator for a chat. + **RCS limitation:** typing indicators are only delivered for iMessage chats — + the RCS protocol does not carry composing state. Calls against RCS-routed chats + return 200 with a `warning` field and have no visible effect on the recipient. + Args: extra_headers: Send extra headers @@ -149,6 +157,10 @@ async def start( The indicator shows the recipient that you are typing. + **RCS limitation:** typing indicators are only delivered for iMessage chats — + the RCS protocol does not carry composing state. Calls against RCS-routed chats + return 200 with a `warning` field and have no visible effect on the recipient. + Args: extra_headers: Send extra headers @@ -182,6 +194,10 @@ async def stop( """ Stop the typing indicator for a chat. + **RCS limitation:** typing indicators are only delivered for iMessage chats — + the RCS protocol does not carry composing state. Calls against RCS-routed chats + return 200 with a `warning` field and have no visible effect on the recipient. + Args: extra_headers: Send extra headers diff --git a/src/blooio/types/chats/typing_response.py b/src/blooio/types/chats/typing_response.py index 3350bee..6326aaf 100644 --- a/src/blooio/types/chats/typing_response.py +++ b/src/blooio/types/chats/typing_response.py @@ -19,3 +19,10 @@ class TypingResponse(BaseModel): typing: Optional[bool] = None """Whether typing indicator is active""" + + warning: Optional[str] = None + """Present when the request was accepted but the indicator could not be delivered. + + The most common reason is that the chat last routed via RCS, which does not + carry composing state. + """ From 09e67720ecd10fdddada3bbd59fcd06546c3c29f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 02:01:42 +0000 Subject: [PATCH 06/10] feat(internal/types): support eagerly validating pydantic iterators --- src/blooio/_models.py | 80 +++++++++++++++++++++++++++++++++++++++++++ tests/test_models.py | 60 ++++++++++++++++++++++++++++++-- 2 files changed, 137 insertions(+), 3 deletions(-) diff --git a/src/blooio/_models.py b/src/blooio/_models.py index e22dd2a..69f41a6 100644 --- a/src/blooio/_models.py +++ b/src/blooio/_models.py @@ -25,7 +25,9 @@ ClassVar, Protocol, Required, + Annotated, ParamSpec, + TypeAlias, TypedDict, TypeGuard, final, @@ -79,7 +81,15 @@ from ._constants import RAW_RESPONSE_HEADER if TYPE_CHECKING: + from pydantic import GetCoreSchemaHandler, ValidatorFunctionWrapHandler + from pydantic_core import CoreSchema, core_schema from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema +else: + try: + from pydantic_core import CoreSchema, core_schema + except ImportError: + CoreSchema = None + core_schema = None __all__ = ["BaseModel", "GenericModel"] @@ -396,6 +406,76 @@ def model_dump_json( ) +class _EagerIterable(list[_T], Generic[_T]): + """ + Accepts any Iterable[T] input (including generators), consumes it + eagerly, and validates all items upfront. + + Validation preserves the original container type where possible + (e.g. a set[T] stays a set[T]). Serialization (model_dump / JSON) + always emits a list — round-tripping through model_dump() will not + restore the original container type. + """ + + @classmethod + def __get_pydantic_core_schema__( + cls, + source_type: Any, + handler: GetCoreSchemaHandler, + ) -> CoreSchema: + (item_type,) = get_args(source_type) or (Any,) + item_schema: CoreSchema = handler.generate_schema(item_type) + list_of_items_schema: CoreSchema = core_schema.list_schema(item_schema) + + return core_schema.no_info_wrap_validator_function( + cls._validate, + list_of_items_schema, + serialization=core_schema.plain_serializer_function_ser_schema( + cls._serialize, + info_arg=False, + ), + ) + + @staticmethod + def _validate(v: Iterable[_T], handler: "ValidatorFunctionWrapHandler") -> Any: + original_type: type[Any] = type(v) + + # Normalize to list so list_schema can validate each item + if isinstance(v, list): + items: list[_T] = v + else: + try: + items = list(v) + except TypeError as e: + raise TypeError("Value is not iterable") from e + + # Validate items against the inner schema + validated: list[_T] = handler(items) + + # Reconstruct original container type + if original_type is list: + return validated + # str(list) produces the list's repr, not a string built from items, + # so skip reconstruction for str and its subclasses. + if issubclass(original_type, str): + return validated + try: + return original_type(validated) + except (TypeError, ValueError): + # If the type cannot be reconstructed, just return the validated list + return validated + + @staticmethod + def _serialize(v: Iterable[_T]) -> list[_T]: + """Always serialize as a list so Pydantic's JSON encoder is happy.""" + if isinstance(v, list): + return v + return list(v) + + +EagerIterable: TypeAlias = Annotated[Iterable[_T], _EagerIterable] + + def _construct_field(value: object, field: FieldInfo, key: str) -> object: if value is None: return field_get_default(field) diff --git a/tests/test_models.py b/tests/test_models.py index 8c43b50..87a295e 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,8 @@ import json -from typing import TYPE_CHECKING, Any, Dict, List, Union, Optional, cast +from typing import TYPE_CHECKING, Any, Dict, List, Union, Iterable, Optional, cast from datetime import datetime, timezone -from typing_extensions import Literal, Annotated, TypeAliasType +from collections import deque +from typing_extensions import Literal, Annotated, TypedDict, TypeAliasType import pytest import pydantic @@ -9,7 +10,7 @@ from blooio._utils import PropertyInfo from blooio._compat import PYDANTIC_V1, parse_obj, model_dump, model_json -from blooio._models import DISCRIMINATOR_CACHE, BaseModel, construct_type +from blooio._models import DISCRIMINATOR_CACHE, BaseModel, EagerIterable, construct_type class BasicModel(BaseModel): @@ -961,3 +962,56 @@ def __getattr__(self, attr: str) -> Item: ... assert model.a.prop == 1 assert isinstance(model.a, Item) assert model.other == "foo" + + +# NOTE: Workaround for Pydantic Iterable behavior. +# Iterable fields are replaced with a ValidatorIterator and may be consumed +# during serialization, which can cause subsequent dumps to return empty data. +# See: https://github.com/pydantic/pydantic/issues/9541 +@pytest.mark.parametrize( + "data, expected_validated", + [ + ([1, 2, 3], [1, 2, 3]), + ((1, 2, 3), (1, 2, 3)), + (set([1, 2, 3]), set([1, 2, 3])), + (iter([1, 2, 3]), [1, 2, 3]), + ([], []), + ((x for x in [1, 2, 3]), [1, 2, 3]), + (map(lambda x: x, [1, 2, 3]), [1, 2, 3]), + (frozenset([1, 2, 3]), frozenset([1, 2, 3])), + (deque([1, 2, 3]), deque([1, 2, 3])), + ], + ids=["list", "tuple", "set", "iterator", "empty", "generator", "map", "frozenset", "deque"], +) +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2") +def test_iterable_construction(data: Iterable[int], expected_validated: Iterable[int]) -> None: + class TypeWithIterable(TypedDict): + items: EagerIterable[int] + + class Model(BaseModel): + data: TypeWithIterable + + m = Model.model_validate({"data": {"items": data}}) + assert m.data["items"] == expected_validated + + # Verify repeated dumps don't lose data (the original bug) + assert m.model_dump()["data"]["items"] == list(expected_validated) + assert m.model_dump()["data"]["items"] == list(expected_validated) + + +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2") +def test_iterable_construction_str_falls_back_to_list() -> None: + # str is iterable (over chars), but str(list_of_chars) produces the list's repr + # rather than reconstructing a string from items. We special-case str to fall + # back to list instead of attempting reconstruction. + class TypeWithIterable(TypedDict): + items: EagerIterable[str] + + class Model(BaseModel): + data: TypeWithIterable + + m = Model.model_validate({"data": {"items": "hello"}}) + + # falls back to list of chars rather than calling str(["h", "e", "l", "l", "o"]) + assert m.data["items"] == ["h", "e", "l", "l", "o"] + assert m.model_dump()["data"]["items"] == ["h", "e", "l", "l", "o"] From 792b695493cc225da6a6e4b66c828e1a070e736b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 12 May 2026 10:12:27 +0000 Subject: [PATCH 07/10] feat(api): api update --- .stats.yml | 4 ++-- src/blooio/types/me/number_list_response.py | 14 ++++++++++++++ src/blooio/types/me_retrieve_response.py | 6 ++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 59c867a..d764538 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-efd0b1a3742ab66cdefbcadb5a11628de9aa18d356e3730c666628ed8f4ba8fa.yml -openapi_spec_hash: 90f50b5ef3d3e845cc2c20aba392119e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-2a244944dcd51b84f0f71eb8a8ce822821725b2c343b688a46ea7f82c7a95b6c.yml +openapi_spec_hash: 2a5b40ad2fa43fa3e64a14ac56ff58d8 config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/src/blooio/types/me/number_list_response.py b/src/blooio/types/me/number_list_response.py index 48deca9..8ef38d6 100644 --- a/src/blooio/types/me/number_list_response.py +++ b/src/blooio/types/me/number_list_response.py @@ -2,6 +2,7 @@ from typing import List, Optional from datetime import datetime +from typing_extensions import Literal from ..._models import BaseModel @@ -15,6 +16,19 @@ class Number(BaseModel): phone_number: Optional[str] = None + plan_kind: Optional[Literal["shared", "dedicated", "inbound", "trial", "2fa"]] = None + """Plan type the underlying allocation runs on. + + Sourced directly from `allocation_pool.type` — the enum mirrors the DB `CHECK` + constraint (see migration 2026-05-09-inbound-plan.sql), so any value here is + also a valid type stored in the database. `inbound` numbers are reply-only — + outbound to a recipient (a contact for 1:1 chats, the group for group chats) + requires that recipient to have messaged the number first (otherwise the send + returns `403 inbound_only_no_prior_inbound`). `null` indicates the underlying + allocation predates the type column or is unattributed; clients should treat + `null` the same as `dedicated` for routing decisions. + """ + class NumberListResponse(BaseModel): numbers: Optional[List[Number]] = None diff --git a/src/blooio/types/me_retrieve_response.py b/src/blooio/types/me_retrieve_response.py index aedcf64..c04aad5 100644 --- a/src/blooio/types/me_retrieve_response.py +++ b/src/blooio/types/me_retrieve_response.py @@ -16,6 +16,12 @@ class Device(BaseModel): phone_number: Optional[str] = None """Phone number assigned to this device (E.164 format)""" + plan_kind: Optional[Literal["shared", "dedicated", "inbound", "trial", "2fa"]] = None + """Plan type the underlying allocation runs on. + + `inbound` numbers are reply-only — see `/me/numbers` for details. + """ + class Organization(BaseModel): country_code: Optional[str] = None From ff3efd7e7c699ca6069b9e3443f332e85901d61a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 02:01:55 +0000 Subject: [PATCH 08/10] ci: pin GitHub Actions to commit SHAs Pin all GitHub Actions referenced in generated workflows (both first-party `actions/*` and third-party) to immutable commit SHAs. Updating pinned actions is now a deliberate codegen-side bump rather than implicit on every workflow run. --- .github/workflows/ci.yml | 14 +++++++------- .github/workflows/publish-pypi.yml | 4 ++-- .github/workflows/release-doctor.yml | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1713129..3e3308d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,10 +21,10 @@ 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) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: version: '0.10.2' @@ -43,10 +43,10 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/blooio-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: version: '0.10.2' @@ -61,7 +61,7 @@ jobs: github.repository == 'stainless-sdks/blooio-python' && !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: script: core.setOutput('github_token', await core.getIDToken()); @@ -81,10 +81,10 @@ 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@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: version: '0.10.2' diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 0fad8c4..720f4a6 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -17,10 +17,10 @@ jobs: id-token: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: version: '0.9.13' diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index d16ee7b..f8f8425 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@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Check release environment run: | From 5c4426ca2a3cc9b946114a760736b3065fa32f12 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 23:18:48 +0000 Subject: [PATCH 09/10] feat(api): api update --- .stats.yml | 4 +- src/blooio/resources/chats/messages.py | 118 ++++++++++++++++++ src/blooio/types/chats/message_send_params.py | 53 +++++++- tests/api_resources/chats/test_messages.py | 2 + 4 files changed, 174 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index d764538..000c763 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-2a244944dcd51b84f0f71eb8a8ce822821725b2c343b688a46ea7f82c7a95b6c.yml -openapi_spec_hash: 2a5b40ad2fa43fa3e64a14ac56ff58d8 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-bce90b321b6782841d7834e0e0689b1812ef1f10f8f2823cd9908185d9f306e6.yml +openapi_spec_hash: a90c7586ed5e83a938d72a286efa9f8e config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/src/blooio/resources/chats/messages.py b/src/blooio/resources/chats/messages.py index 61336a7..8f844d3 100644 --- a/src/blooio/resources/chats/messages.py +++ b/src/blooio/resources/chats/messages.py @@ -257,6 +257,24 @@ def send( chat_id: str, *, attachments: SequenceNotStr[message_send_params.Attachment] | Omit = omit, + effect: Optional[ + Literal[ + "slam", + "loud", + "gentle", + "invisible-ink", + "echo", + "spotlight", + "balloons", + "confetti", + "love", + "lasers", + "fireworks", + "celebration", + "none", + ] + ] + | Omit = omit, from_number: str | Omit = omit, link_preview: Optional[LinkPreviewParam] | Omit = omit, parts: Iterable[message_send_params.Part] | Omit = omit, @@ -279,9 +297,49 @@ def send( created or reused if the exact participant combination already exists. For explicit groups, the group must be linked to an existing iMessage chat. + **iMessage send-with-effect:** set the optional `effect` field to attach an + Apple expressive send (slam, loud, gentle, invisible-ink) or screen effect + (echo, spotlight, balloons, confetti, love, lasers, fireworks, celebration). + Effects are an iMessage-only feature — when the recipient is on SMS/RCS the + message is delivered without the animation. Effects are not supported in + multipart (`parts`) mode. + Args: attachments: Array of attachment URLs or objects with url/name + effect: Optional. Attach an iMessage send-with-effect to the outgoing message. + + **Bubble effects** (apply to a single text bubble): + + - `slam` — Slam + - `loud` — Loud + - `gentle` — Gentle + - `invisible-ink` — Invisible Ink + + **Screen effects** (full-screen animation in the recipient's chat): + + - `echo` — Echo + - `spotlight` — Spotlight + - `balloons` — Balloons + - `confetti` — Confetti + - `love` — Love (heart) + - `lasers` — Lasers + - `fireworks` — Fireworks + - `celebration` — Celebration (sparkles) + + Values are case-insensitive and accept either dashes or spaces + (`"Invisible Ink"` and `"invisible-ink"` both work). Pass `"none"` or omit the + field to send without an effect. + + **Limitations:** + + - iMessage-only — when the chat is delivered as SMS or RCS the message is sent + without an animation. + - Not supported alongside the `parts` array (multipart bubbles cannot carry an + effect). Use the top-level `text` field instead. + - When `text` is an array, every message in the array is sent with the same + effect. + 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 @@ -329,6 +387,7 @@ def send( body=maybe_transform( { "attachments": attachments, + "effect": effect, "from_number": from_number, "link_preview": link_preview, "parts": parts, @@ -573,6 +632,24 @@ async def send( chat_id: str, *, attachments: SequenceNotStr[message_send_params.Attachment] | Omit = omit, + effect: Optional[ + Literal[ + "slam", + "loud", + "gentle", + "invisible-ink", + "echo", + "spotlight", + "balloons", + "confetti", + "love", + "lasers", + "fireworks", + "celebration", + "none", + ] + ] + | Omit = omit, from_number: str | Omit = omit, link_preview: Optional[LinkPreviewParam] | Omit = omit, parts: Iterable[message_send_params.Part] | Omit = omit, @@ -595,9 +672,49 @@ async def send( created or reused if the exact participant combination already exists. For explicit groups, the group must be linked to an existing iMessage chat. + **iMessage send-with-effect:** set the optional `effect` field to attach an + Apple expressive send (slam, loud, gentle, invisible-ink) or screen effect + (echo, spotlight, balloons, confetti, love, lasers, fireworks, celebration). + Effects are an iMessage-only feature — when the recipient is on SMS/RCS the + message is delivered without the animation. Effects are not supported in + multipart (`parts`) mode. + Args: attachments: Array of attachment URLs or objects with url/name + effect: Optional. Attach an iMessage send-with-effect to the outgoing message. + + **Bubble effects** (apply to a single text bubble): + + - `slam` — Slam + - `loud` — Loud + - `gentle` — Gentle + - `invisible-ink` — Invisible Ink + + **Screen effects** (full-screen animation in the recipient's chat): + + - `echo` — Echo + - `spotlight` — Spotlight + - `balloons` — Balloons + - `confetti` — Confetti + - `love` — Love (heart) + - `lasers` — Lasers + - `fireworks` — Fireworks + - `celebration` — Celebration (sparkles) + + Values are case-insensitive and accept either dashes or spaces + (`"Invisible Ink"` and `"invisible-ink"` both work). Pass `"none"` or omit the + field to send without an effect. + + **Limitations:** + + - iMessage-only — when the chat is delivered as SMS or RCS the message is sent + without an animation. + - Not supported alongside the `parts` array (multipart bubbles cannot carry an + effect). Use the top-level `text` field instead. + - When `text` is an array, every message in the array is sent with the same + effect. + 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 @@ -645,6 +762,7 @@ async def send( body=await async_maybe_transform( { "attachments": attachments, + "effect": effect, "from_number": from_number, "link_preview": link_preview, "parts": parts, diff --git a/src/blooio/types/chats/message_send_params.py b/src/blooio/types/chats/message_send_params.py index 35c4d1c..b2b935b 100644 --- a/src/blooio/types/chats/message_send_params.py +++ b/src/blooio/types/chats/message_send_params.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Union, Iterable, Optional -from typing_extensions import Required, Annotated, TypeAlias, TypedDict +from typing_extensions import Literal, Required, Annotated, TypeAlias, TypedDict from ..._types import SequenceNotStr from ..._utils import PropertyInfo @@ -16,6 +16,57 @@ class MessageSendParams(TypedDict, total=False): attachments: SequenceNotStr[Attachment] """Array of attachment URLs or objects with url/name""" + effect: Optional[ + Literal[ + "slam", + "loud", + "gentle", + "invisible-ink", + "echo", + "spotlight", + "balloons", + "confetti", + "love", + "lasers", + "fireworks", + "celebration", + "none", + ] + ] + """Optional. Attach an iMessage send-with-effect to the outgoing message. + + **Bubble effects** (apply to a single text bubble): + + - `slam` — Slam + - `loud` — Loud + - `gentle` — Gentle + - `invisible-ink` — Invisible Ink + + **Screen effects** (full-screen animation in the recipient's chat): + + - `echo` — Echo + - `spotlight` — Spotlight + - `balloons` — Balloons + - `confetti` — Confetti + - `love` — Love (heart) + - `lasers` — Lasers + - `fireworks` — Fireworks + - `celebration` — Celebration (sparkles) + + Values are case-insensitive and accept either dashes or spaces + (`"Invisible Ink"` and `"invisible-ink"` both work). Pass `"none"` or omit the + field to send without an effect. + + **Limitations:** + + - iMessage-only — when the chat is delivered as SMS or RCS the message is sent + without an animation. + - Not supported alongside the `parts` array (multipart bubbles cannot carry an + effect). Use the top-level `text` field instead. + - When `text` is an array, every message in the array is sent with the same + effect. + """ + from_number: str """E.164 phone number to send from. diff --git a/tests/api_resources/chats/test_messages.py b/tests/api_resources/chats/test_messages.py index 9034d44..1ca5b92 100644 --- a/tests/api_resources/chats/test_messages.py +++ b/tests/api_resources/chats/test_messages.py @@ -265,6 +265,7 @@ def test_method_send_with_all_params(self, client: Blooio) -> None: message = client.chats.messages.send( chat_id="chatId", attachments=["string"], + effect="slam", from_number="from_number", link_preview={ "image_url": "https://example.com", @@ -571,6 +572,7 @@ async def test_method_send_with_all_params(self, async_client: AsyncBlooio) -> N message = await async_client.chats.messages.send( chat_id="chatId", attachments=["string"], + effect="slam", from_number="from_number", link_preview={ "image_url": "https://example.com", From 36cc7bc69f25ac223877eb753b343814ef8a12a3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 23:19:16 +0000 Subject: [PATCH 10/10] release: 1.2.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 22 ++++++++++++++++++++++ pyproject.toml | 2 +- src/blooio/_version.py | 2 +- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b55c11f..d0ab664 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.1.1" + ".": "1.2.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 95e82a1..ba8d78c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 1.2.0 (2026-05-14) + +Full Changelog: [v1.1.1...v1.2.0](https://github.com/Blooio/blooio-python-sdk/compare/v1.1.1...v1.2.0) + +### Features + +* **api:** api update ([5c4426c](https://github.com/Blooio/blooio-python-sdk/commit/5c4426ca2a3cc9b946114a760736b3065fa32f12)) +* **api:** api update ([792b695](https://github.com/Blooio/blooio-python-sdk/commit/792b695493cc225da6a6e4b66c828e1a070e736b)) +* **api:** api update ([7ab7fb6](https://github.com/Blooio/blooio-python-sdk/commit/7ab7fb636a69da2d1dcd67dca110ff627d380723)) +* **api:** manual updates ([20785b1](https://github.com/Blooio/blooio-python-sdk/commit/20785b127663e4972d2b2910ec9ee86e68c1829c)) +* **internal/types:** support eagerly validating pydantic iterators ([09e6772](https://github.com/Blooio/blooio-python-sdk/commit/09e67720ecd10fdddada3bbd59fcd06546c3c29f)) + + +### Bug Fixes + +* **client:** add missing f-string prefix in file type error message ([6126d3d](https://github.com/Blooio/blooio-python-sdk/commit/6126d3d6cbf2f395a465df8dac3edda83933248a)) + + +### Chores + +* **internal:** reformat pyproject.toml ([07380c6](https://github.com/Blooio/blooio-python-sdk/commit/07380c6a86dda7dbb1d633a4bb3e032e9b869102)) + ## 1.1.1 (2026-04-30) Full Changelog: [v1.1.0...v1.1.1](https://github.com/Blooio/blooio-python-sdk/compare/v1.1.0...v1.1.1) diff --git a/pyproject.toml b/pyproject.toml index d958dc6..4c6c403 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "blooio" -version = "1.1.1" +version = "1.2.0" description = "The official Python library for the blooio API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/blooio/_version.py b/src/blooio/_version.py index a124b3c..2fd6bd0 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.1.1" # x-release-please-version +__version__ = "1.2.0" # x-release-please-version