From 98724ee95eeaf2dffd5e725b212d223b9db1096a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 23:29:48 +0000 Subject: [PATCH 01/44] chore(internal): codegen related update --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index ba8f9b0..d98b386 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: MacOS", From 2e208bed47293b621d244dcb03cbbcf14e5d45b2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:20:16 +0000 Subject: [PATCH 02/44] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 84e29ab..8bdc970 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-ce72fff9b44a47ab7e0425e496f09c61cde5b4258feb20cb5dbef6fa615a57e4.yml -openapi_spec_hash: 3054ea299cf43dc89b68266818fecfe4 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-60e333080a18dc6a20bda4d60601c71e4f40562863a0f535a6d85a7c25b6e4dd.yml +openapi_spec_hash: 4e57cba6e21cdf7e63267e03ee4c6005 config_hash: 2b42d138d85c524e65fa7e205d36cc4a From 359f6a8742fe0e9b52b71dc3277f96b0081e25b7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 23:15:45 +0000 Subject: [PATCH 03/44] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8bdc970..84e29ab 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-60e333080a18dc6a20bda4d60601c71e4f40562863a0f535a6d85a7c25b6e4dd.yml -openapi_spec_hash: 4e57cba6e21cdf7e63267e03ee4c6005 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-ce72fff9b44a47ab7e0425e496f09c61cde5b4258feb20cb5dbef6fa615a57e4.yml +openapi_spec_hash: 3054ea299cf43dc89b68266818fecfe4 config_hash: 2b42d138d85c524e65fa7e205d36cc4a From a4ee7c50c69236c33d186c9b22ac33f85cf010b6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:05:21 +0000 Subject: [PATCH 04/44] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 84e29ab..8bdc970 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-ce72fff9b44a47ab7e0425e496f09c61cde5b4258feb20cb5dbef6fa615a57e4.yml -openapi_spec_hash: 3054ea299cf43dc89b68266818fecfe4 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-60e333080a18dc6a20bda4d60601c71e4f40562863a0f535a6d85a7c25b6e4dd.yml +openapi_spec_hash: 4e57cba6e21cdf7e63267e03ee4c6005 config_hash: 2b42d138d85c524e65fa7e205d36cc4a From 824ac3f77dd2d95c51cef4d321a09e84db49d8cf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 21:05:58 +0000 Subject: [PATCH 05/44] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8bdc970..84e29ab 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-60e333080a18dc6a20bda4d60601c71e4f40562863a0f535a6d85a7c25b6e4dd.yml -openapi_spec_hash: 4e57cba6e21cdf7e63267e03ee4c6005 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-ce72fff9b44a47ab7e0425e496f09c61cde5b4258feb20cb5dbef6fa615a57e4.yml +openapi_spec_hash: 3054ea299cf43dc89b68266818fecfe4 config_hash: 2b42d138d85c524e65fa7e205d36cc4a From b6c112a3a70d68aa6190d90ebe508a15cf160a6e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 22:45:56 +0000 Subject: [PATCH 06/44] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 84e29ab..8bdc970 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-ce72fff9b44a47ab7e0425e496f09c61cde5b4258feb20cb5dbef6fa615a57e4.yml -openapi_spec_hash: 3054ea299cf43dc89b68266818fecfe4 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-60e333080a18dc6a20bda4d60601c71e4f40562863a0f535a6d85a7c25b6e4dd.yml +openapi_spec_hash: 4e57cba6e21cdf7e63267e03ee4c6005 config_hash: 2b42d138d85c524e65fa7e205d36cc4a From 007d337f07c9de1883d0580209aa743384e24925 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 16:53:17 +0000 Subject: [PATCH 07/44] fix: ensure streams are always closed --- src/knockapi/_streaming.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/knockapi/_streaming.py b/src/knockapi/_streaming.py index 01eb112..5a41eae 100644 --- a/src/knockapi/_streaming.py +++ b/src/knockapi/_streaming.py @@ -54,11 +54,12 @@ def __stream__(self) -> Iterator[_T]: process_data = self._client._process_response_data iterator = self._iter_events() - for sse in iterator: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - - # As we might not fully consume the response stream, we need to close it explicitly - response.close() + try: + for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + response.close() def __enter__(self) -> Self: return self @@ -117,11 +118,12 @@ async def __stream__(self) -> AsyncIterator[_T]: process_data = self._client._process_response_data iterator = self._iter_events() - async for sse in iterator: - yield process_data(data=sse.json(), cast_to=cast_to, response=response) - - # As we might not fully consume the response stream, we need to close it explicitly - await response.aclose() + try: + async for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + await response.aclose() async def __aenter__(self) -> Self: return self From 227e51fa9f367e8aa67f9fe9ce96c844c3925b9e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 27 Nov 2025 18:49:54 +0000 Subject: [PATCH 08/44] chore(deps): mypy 1.18.1 has a regression, pin to 1.17 --- pyproject.toml | 2 +- requirements-dev.lock | 4 +++- requirements.lock | 8 ++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d98b386..494afae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ managed = true # version pins are in requirements-dev.lock dev-dependencies = [ "pyright==1.1.399", - "mypy", + "mypy==1.17", "respx", "pytest", "pytest-asyncio", diff --git a/requirements-dev.lock b/requirements-dev.lock index 7348fc4..7838465 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -72,7 +72,7 @@ mdurl==0.1.2 multidict==6.4.4 # via aiohttp # via yarl -mypy==1.14.1 +mypy==1.17.0 mypy-extensions==1.0.0 # via mypy nodeenv==1.8.0 @@ -81,6 +81,8 @@ nox==2023.4.22 packaging==23.2 # via nox # via pytest +pathspec==0.12.1 + # via mypy platformdirs==3.11.0 # via virtualenv pluggy==1.5.0 diff --git a/requirements.lock b/requirements.lock index e35bc1f..299577f 100644 --- a/requirements.lock +++ b/requirements.lock @@ -55,21 +55,21 @@ multidict==6.4.4 propcache==0.3.1 # via aiohttp # via yarl -pydantic==2.11.9 +pydantic==2.12.5 # via knockapi -pydantic-core==2.33.2 +pydantic-core==2.41.5 # via pydantic sniffio==1.3.0 # via anyio # via knockapi -typing-extensions==4.12.2 +typing-extensions==4.15.0 # via anyio # via knockapi # via multidict # via pydantic # via pydantic-core # via typing-inspection -typing-inspection==0.4.1 +typing-inspection==0.4.2 # via pydantic yarl==1.20.0 # via aiohttp From 420776a17f1576841ad7cf89aeaf6c2ce80d2466 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 16:56:08 +0000 Subject: [PATCH 09/44] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8bdc970..84e29ab 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-60e333080a18dc6a20bda4d60601c71e4f40562863a0f535a6d85a7c25b6e4dd.yml -openapi_spec_hash: 4e57cba6e21cdf7e63267e03ee4c6005 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-ce72fff9b44a47ab7e0425e496f09c61cde5b4258feb20cb5dbef6fa615a57e4.yml +openapi_spec_hash: 3054ea299cf43dc89b68266818fecfe4 config_hash: 2b42d138d85c524e65fa7e205d36cc4a From 48e85d7d0cdd44f342abd95e1517dc4f51f1d67d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:09:41 +0000 Subject: [PATCH 10/44] chore: update lockfile --- pyproject.toml | 14 +++--- requirements-dev.lock | 108 +++++++++++++++++++++++------------------- requirements.lock | 31 ++++++------ 3 files changed, 83 insertions(+), 70 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 494afae..230502a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,14 +7,16 @@ license = "Apache-2.0" authors = [ { name = "Knock", email = "support@knock.app" }, ] + dependencies = [ - "httpx>=0.23.0, <1", - "pydantic>=1.9.0, <3", - "typing-extensions>=4.10, <5", - "anyio>=3.5.0, <5", - "distro>=1.7.0, <2", - "sniffio", + "httpx>=0.23.0, <1", + "pydantic>=1.9.0, <3", + "typing-extensions>=4.10, <5", + "anyio>=3.5.0, <5", + "distro>=1.7.0, <2", + "sniffio", ] + requires-python = ">= 3.9" classifiers = [ "Typing :: Typed", diff --git a/requirements-dev.lock b/requirements-dev.lock index 7838465..fc891d5 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -12,40 +12,45 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.8 +aiohttp==3.13.2 # via httpx-aiohttp # via knockapi -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.0 # via httpx # via knockapi -argcomplete==3.1.2 +argcomplete==3.6.3 # via nox async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp -certifi==2023.7.22 + # via nox +backports-asyncio-runner==1.2.0 + # via pytest-asyncio +certifi==2025.11.12 # via httpcore # via httpx -colorlog==6.7.0 +colorlog==6.10.1 + # via nox +dependency-groups==1.3.1 # via nox -dirty-equals==0.6.0 -distlib==0.3.7 +dirty-equals==0.11 +distlib==0.4.0 # via virtualenv -distro==1.8.0 +distro==1.9.0 # via knockapi -exceptiongroup==1.2.2 +exceptiongroup==1.3.1 # via anyio # via pytest -execnet==2.1.1 +execnet==2.1.2 # via pytest-xdist -filelock==3.12.4 +filelock==3.19.1 # via virtualenv -frozenlist==1.6.2 +frozenlist==1.8.0 # via aiohttp # via aiosignal h11==0.16.0 @@ -58,82 +63,87 @@ httpx==0.28.1 # via respx httpx-aiohttp==0.1.9 # via knockapi -idna==3.4 +humanize==4.13.0 + # via nox +idna==3.11 # via anyio # via httpx # via yarl -importlib-metadata==7.0.0 -iniconfig==2.0.0 +importlib-metadata==8.7.0 +iniconfig==2.1.0 # via pytest markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -multidict==6.4.4 +multidict==6.7.0 # via aiohttp # via yarl mypy==1.17.0 -mypy-extensions==1.0.0 +mypy-extensions==1.1.0 # via mypy -nodeenv==1.8.0 +nodeenv==1.9.1 # via pyright -nox==2023.4.22 -packaging==23.2 +nox==2025.11.12 +packaging==25.0 + # via dependency-groups # via nox # via pytest pathspec==0.12.1 # via mypy -platformdirs==3.11.0 +platformdirs==4.4.0 # via virtualenv -pluggy==1.5.0 +pluggy==1.6.0 # via pytest -propcache==0.3.1 +propcache==0.4.1 # via aiohttp # via yarl -pydantic==2.11.9 +pydantic==2.12.5 # via knockapi -pydantic-core==2.33.2 +pydantic-core==2.41.5 # via pydantic -pygments==2.18.0 +pygments==2.19.2 + # via pytest # via rich pyright==1.1.399 -pytest==8.3.3 +pytest==8.4.2 # via pytest-asyncio # via pytest-xdist -pytest-asyncio==0.24.0 -pytest-xdist==3.7.0 -python-dateutil==2.8.2 +pytest-asyncio==1.2.0 +pytest-xdist==3.8.0 +python-dateutil==2.9.0.post0 # via time-machine -pytz==2023.3.post1 - # via dirty-equals respx==0.22.0 -rich==13.7.1 -ruff==0.9.4 -setuptools==68.2.2 - # via nodeenv -six==1.16.0 +rich==14.2.0 +ruff==0.14.7 +six==1.17.0 # via python-dateutil -sniffio==1.3.0 - # via anyio +sniffio==1.3.1 # via knockapi -time-machine==2.9.0 -tomli==2.0.2 +time-machine==2.19.0 +tomli==2.3.0 + # via dependency-groups # via mypy + # via nox # via pytest -typing-extensions==4.12.2 +typing-extensions==4.15.0 + # via aiosignal # via anyio + # via exceptiongroup # via knockapi # via multidict # via mypy # via pydantic # via pydantic-core # via pyright + # via pytest-asyncio # via typing-inspection -typing-inspection==0.4.1 + # via virtualenv +typing-inspection==0.4.2 # via pydantic -virtualenv==20.24.5 +virtualenv==20.35.4 # via nox -yarl==1.20.0 +yarl==1.22.0 # via aiohttp -zipp==3.17.0 +zipp==3.23.0 # via importlib-metadata diff --git a/requirements.lock b/requirements.lock index 299577f..20d4ecf 100644 --- a/requirements.lock +++ b/requirements.lock @@ -12,28 +12,28 @@ -e file:. aiohappyeyeballs==2.6.1 # via aiohttp -aiohttp==3.12.8 +aiohttp==3.13.2 # via httpx-aiohttp # via knockapi -aiosignal==1.3.2 +aiosignal==1.4.0 # via aiohttp -annotated-types==0.6.0 +annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.0 # via httpx # via knockapi async-timeout==5.0.1 # via aiohttp -attrs==25.3.0 +attrs==25.4.0 # via aiohttp -certifi==2023.7.22 +certifi==2025.11.12 # via httpcore # via httpx -distro==1.8.0 +distro==1.9.0 # via knockapi -exceptiongroup==1.2.2 +exceptiongroup==1.3.1 # via anyio -frozenlist==1.6.2 +frozenlist==1.8.0 # via aiohttp # via aiosignal h11==0.16.0 @@ -45,25 +45,26 @@ httpx==0.28.1 # via knockapi httpx-aiohttp==0.1.9 # via knockapi -idna==3.4 +idna==3.11 # via anyio # via httpx # via yarl -multidict==6.4.4 +multidict==6.7.0 # via aiohttp # via yarl -propcache==0.3.1 +propcache==0.4.1 # via aiohttp # via yarl pydantic==2.12.5 # via knockapi pydantic-core==2.41.5 # via pydantic -sniffio==1.3.0 - # via anyio +sniffio==1.3.1 # via knockapi typing-extensions==4.15.0 + # via aiosignal # via anyio + # via exceptiongroup # via knockapi # via multidict # via pydantic @@ -71,5 +72,5 @@ typing-extensions==4.15.0 # via typing-inspection typing-inspection==0.4.2 # via pydantic -yarl==1.20.0 +yarl==1.22.0 # via aiohttp From 832d353b04daf4b474ac08be91320ef95919a1db Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:06:51 +0000 Subject: [PATCH 11/44] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 84e29ab..d0cbc9f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-ce72fff9b44a47ab7e0425e496f09c61cde5b4258feb20cb5dbef6fa615a57e4.yml -openapi_spec_hash: 3054ea299cf43dc89b68266818fecfe4 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-4119078af6d6f8c9686aeb7cd59a877f2d2df63ea9219f092c97b962f208ea04.yml +openapi_spec_hash: 205b0fbe042c32fcd411cd727007deb1 config_hash: 2b42d138d85c524e65fa7e205d36cc4a From 89f283596c21f14a316593dd8542c0ae8b872afc Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 21:25:40 +0000 Subject: [PATCH 12/44] chore(docs): use environment variables for authentication in code snippets --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b3962b5..169ec8e 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ pip install knockapi[aiohttp] Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: ```python +import os import asyncio from knockapi import DefaultAioHttpClient from knockapi import AsyncKnock @@ -94,7 +95,7 @@ from knockapi import AsyncKnock async def main() -> None: async with AsyncKnock( - api_key="My API Key", + api_key=os.environ.get("KNOCK_API_KEY"), # This is the default and can be omitted http_client=DefaultAioHttpClient(), ) as client: response = await client.workflows.trigger( From a702b9dfbe382840abf863874286f8ecc1b1bbd3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 19:37:11 +0000 Subject: [PATCH 13/44] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index d0cbc9f..84e29ab 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-4119078af6d6f8c9686aeb7cd59a877f2d2df63ea9219f092c97b962f208ea04.yml -openapi_spec_hash: 205b0fbe042c32fcd411cd727007deb1 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-ce72fff9b44a47ab7e0425e496f09c61cde5b4258feb20cb5dbef6fa615a57e4.yml +openapi_spec_hash: 3054ea299cf43dc89b68266818fecfe4 config_hash: 2b42d138d85c524e65fa7e205d36cc4a From 64edc3c928261138d0a1e0548704f709d2971797 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 3 Dec 2025 20:18:12 +0000 Subject: [PATCH 14/44] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 84e29ab..d0cbc9f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-ce72fff9b44a47ab7e0425e496f09c61cde5b4258feb20cb5dbef6fa615a57e4.yml -openapi_spec_hash: 3054ea299cf43dc89b68266818fecfe4 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-4119078af6d6f8c9686aeb7cd59a877f2d2df63ea9219f092c97b962f208ea04.yml +openapi_spec_hash: 205b0fbe042c32fcd411cd727007deb1 config_hash: 2b42d138d85c524e65fa7e205d36cc4a From f84db64e51691b081704cdab08e10084813e2995 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 4 Dec 2025 21:00:41 +0000 Subject: [PATCH 15/44] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index d0cbc9f..84e29ab 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-4119078af6d6f8c9686aeb7cd59a877f2d2df63ea9219f092c97b962f208ea04.yml -openapi_spec_hash: 205b0fbe042c32fcd411cd727007deb1 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-ce72fff9b44a47ab7e0425e496f09c61cde5b4258feb20cb5dbef6fa615a57e4.yml +openapi_spec_hash: 3054ea299cf43dc89b68266818fecfe4 config_hash: 2b42d138d85c524e65fa7e205d36cc4a From 47806ce73d09bedb21d6ded55c68a5401d50abbe Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 4 Dec 2025 23:52:22 +0000 Subject: [PATCH 16/44] feat(api): api update --- .stats.yml | 4 ++-- src/knockapi/types/message.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 84e29ab..faedbdb 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-ce72fff9b44a47ab7e0425e496f09c61cde5b4258feb20cb5dbef6fa615a57e4.yml -openapi_spec_hash: 3054ea299cf43dc89b68266818fecfe4 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-b4f3f8094215f4027ce5c50b44b4ba289b96bfe071deba53497eac31f6bd44ec.yml +openapi_spec_hash: da078194fc3bbcae05af20ad6dee4adc config_hash: 2b42d138d85c524e65fa7e205d36cc4a diff --git a/src/knockapi/types/message.py b/src/knockapi/types/message.py index dc8a8bc..3bf24aa 100644 --- a/src/knockapi/types/message.py +++ b/src/knockapi/types/message.py @@ -30,6 +30,19 @@ class Source(BaseModel): type: Optional[Literal["broadcast", "workflow", "guide"]] = None """Whether this message was generated from a workflow, broadcast, or guide.""" + workflow_recipient_run_id: Optional[str] = None + """The unique identifier for the workflow recipient run that generated this + message. + + Only present for workflow/broadcast messages. + """ + + workflow_run_id: Optional[str] = None + """The unique identifier for the workflow run that generated this message. + + Only present for workflow/broadcast messages. + """ + class Channel(BaseModel): id: str From ab076a9225fe2b2d013603b7136c7a76ea0f3300 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:41:26 +0000 Subject: [PATCH 17/44] fix(types): allow pyright to infer TypedDict types within SequenceNotStr --- src/knockapi/_types.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/knockapi/_types.py b/src/knockapi/_types.py index da9efb8..9d73533 100644 --- a/src/knockapi/_types.py +++ b/src/knockapi/_types.py @@ -243,6 +243,9 @@ class HttpxSendArgs(TypedDict, total=False): if TYPE_CHECKING: # This works because str.__contains__ does not accept object (either in typeshed or at runtime) # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285 + # + # Note: index() and count() methods are intentionally omitted to allow pyright to properly + # infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr. class SequenceNotStr(Protocol[_T_co]): @overload def __getitem__(self, index: SupportsIndex, /) -> _T_co: ... @@ -251,8 +254,6 @@ def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ... def __contains__(self, value: object, /) -> bool: ... def __len__(self) -> int: ... def __iter__(self) -> Iterator[_T_co]: ... - def index(self, value: Any, start: int = 0, stop: int = ..., /) -> int: ... - def count(self, value: Any, /) -> int: ... def __reversed__(self) -> Iterator[_T_co]: ... else: # just point this to a normal `Sequence` at runtime to avoid having to special case From dad2da6573ee829b4ac6c44d480e7becd19a25e0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:31:29 +0000 Subject: [PATCH 18/44] chore: add missing docstrings --- src/knockapi/types/activity.py | 5 +++++ .../types/audience_add_members_params.py | 4 ++++ .../types/audience_list_members_response.py | 2 ++ src/knockapi/types/audience_member.py | 2 ++ .../types/audience_remove_members_params.py | 4 ++++ src/knockapi/types/bulk_operation.py | 2 ++ .../inline_identify_user_request_param.py | 5 +++++ .../types/inline_object_request_param.py | 2 ++ src/knockapi/types/message.py | 8 +++++++ src/knockapi/types/message_delivery_log.py | 8 +++++++ src/knockapi/types/message_event.py | 5 +++++ .../types/message_get_content_response.py | 22 +++++++++++++++++++ .../messages/batch_get_content_response.py | 22 +++++++++++++++++++ src/knockapi/types/object.py | 2 ++ .../types/object_list_subscriptions_params.py | 2 ++ .../types/object_set_preferences_params.py | 8 +++++++ .../objects/bulk_add_subscriptions_params.py | 2 ++ .../bulk_delete_subscriptions_params.py | 2 ++ src/knockapi/types/objects/bulk_set_params.py | 2 ++ .../providers/ms_team_check_auth_response.py | 4 ++++ .../ms_team_list_channels_response.py | 4 ++++ .../ms_team_revoke_access_response.py | 2 ++ .../providers/slack_check_auth_response.py | 4 ++++ .../providers/slack_list_channels_response.py | 2 ++ .../providers/slack_revoke_access_response.py | 2 ++ src/knockapi/types/recipient_reference.py | 2 ++ .../types/recipient_reference_param.py | 2 ++ ...ns_push_channel_data_devices_only_param.py | 2 ++ ...ush_channel_data_target_arns_only_param.py | 2 ++ src/knockapi/types/recipients/channel_data.py | 6 +++++ .../types/recipients/discord_channel_data.py | 8 +++++++ .../recipients/discord_channel_data_param.py | 8 +++++++ .../types/recipients/ms_teams_channel_data.py | 8 +++++++ .../recipients/ms_teams_channel_data_param.py | 8 +++++++ ...one_signal_channel_data_player_ids_only.py | 2 ++ ...gnal_channel_data_player_ids_only_param.py | 2 ++ .../types/recipients/preference_set.py | 12 ++++++++++ .../preference_set_channel_setting.py | 5 +++++ .../preference_set_channel_setting_param.py | 5 +++++ .../preference_set_channel_type_setting.py | 5 +++++ ...eference_set_channel_type_setting_param.py | 5 +++++ .../preference_set_channel_types.py | 2 ++ .../preference_set_channel_types_param.py | 2 ++ .../preference_set_request_param.py | 10 +++++++++ .../push_channel_data_devices_only_param.py | 2 ++ .../push_channel_data_tokens_only_param.py | 2 ++ .../types/recipients/slack_channel_data.py | 8 +++++++ .../recipients/slack_channel_data_param.py | 8 +++++++ src/knockapi/types/recipients/subscription.py | 2 ++ src/knockapi/types/schedule.py | 2 ++ src/knockapi/types/schedule_repeat_rule.py | 2 ++ .../types/schedule_repeat_rule_param.py | 2 ++ .../types/schedules/bulk_create_params.py | 2 ++ src/knockapi/types/shared/condition.py | 2 ++ src/knockapi/types/shared/page_info.py | 2 ++ src/knockapi/types/shared_params/condition.py | 2 ++ src/knockapi/types/tenant.py | 6 +++++ src/knockapi/types/tenant_request_param.py | 9 ++++++++ src/knockapi/types/tenant_set_params.py | 4 ++++ src/knockapi/types/user.py | 4 ++++ .../types/user_set_preferences_params.py | 8 +++++++ .../types/users/feed_get_settings_response.py | 4 ++++ .../types/users/feed_list_items_response.py | 10 +++++++++ .../types/users/guide_get_channel_response.py | 2 ++ ...guide_mark_message_as_archived_response.py | 2 ++ ...ide_mark_message_as_interacted_response.py | 2 ++ .../guide_mark_message_as_seen_response.py | 2 ++ .../types/workflow_trigger_response.py | 2 ++ 68 files changed, 314 insertions(+) diff --git a/src/knockapi/types/activity.py b/src/knockapi/types/activity.py index 4f1d6e3..9108451 100644 --- a/src/knockapi/types/activity.py +++ b/src/knockapi/types/activity.py @@ -12,6 +12,11 @@ class Activity(BaseModel): + """An activity associated with a workflow trigger request. + + Messages produced after a [batch step](/designing-workflows/batch-function) can be associated with one or more activities. Non-batched messages will always be associated with a single activity. + """ + id: Optional[str] = None """Unique identifier for the activity.""" diff --git a/src/knockapi/types/audience_add_members_params.py b/src/knockapi/types/audience_add_members_params.py index 0c23568..6461285 100644 --- a/src/knockapi/types/audience_add_members_params.py +++ b/src/knockapi/types/audience_add_members_params.py @@ -14,11 +14,15 @@ class AudienceAddMembersParams(TypedDict, total=False): class MemberUser(TypedDict, total=False): + """An object containing the user's ID.""" + id: str """The unique identifier of the user.""" class Member(TypedDict, total=False): + """An audience member.""" + user: Required[MemberUser] """An object containing the user's ID.""" diff --git a/src/knockapi/types/audience_list_members_response.py b/src/knockapi/types/audience_list_members_response.py index fc2da4a..049dbab 100644 --- a/src/knockapi/types/audience_list_members_response.py +++ b/src/knockapi/types/audience_list_members_response.py @@ -10,6 +10,8 @@ class AudienceListMembersResponse(BaseModel): + """A paginated list of audience members.""" + entries: List[AudienceMember] """A list of audience members.""" diff --git a/src/knockapi/types/audience_member.py b/src/knockapi/types/audience_member.py index d7eac53..6c6120e 100644 --- a/src/knockapi/types/audience_member.py +++ b/src/knockapi/types/audience_member.py @@ -12,6 +12,8 @@ class AudienceMember(BaseModel): + """An audience member.""" + api_typename: str = FieldInfo(alias="__typename") """The typename of the schema.""" diff --git a/src/knockapi/types/audience_remove_members_params.py b/src/knockapi/types/audience_remove_members_params.py index 393c10c..d33f0a3 100644 --- a/src/knockapi/types/audience_remove_members_params.py +++ b/src/knockapi/types/audience_remove_members_params.py @@ -14,11 +14,15 @@ class AudienceRemoveMembersParams(TypedDict, total=False): class MemberUser(TypedDict, total=False): + """An object containing the user's ID.""" + id: str """The unique identifier of the user.""" class Member(TypedDict, total=False): + """An audience member.""" + user: Required[MemberUser] """An object containing the user's ID.""" diff --git a/src/knockapi/types/bulk_operation.py b/src/knockapi/types/bulk_operation.py index 58d6e6b..0c3aef1 100644 --- a/src/knockapi/types/bulk_operation.py +++ b/src/knockapi/types/bulk_operation.py @@ -20,6 +20,8 @@ class ErrorItem(BaseModel): class BulkOperation(BaseModel): + """A bulk operation entity.""" + id: str """Unique identifier for the bulk operation.""" diff --git a/src/knockapi/types/inline_identify_user_request_param.py b/src/knockapi/types/inline_identify_user_request_param.py index c18cc0b..9254781 100644 --- a/src/knockapi/types/inline_identify_user_request_param.py +++ b/src/knockapi/types/inline_identify_user_request_param.py @@ -14,6 +14,11 @@ class InlineIdentifyUserRequestParamTyped(TypedDict, total=False): + """A set of parameters to inline-identify a user with. + + Inline identifying the user will ensure that the user is available before the request is executed in Knock. It will perform an upsert for the user you're supplying, replacing any properties specified. + """ + id: Required[str] """The unique identifier of the user.""" diff --git a/src/knockapi/types/inline_object_request_param.py b/src/knockapi/types/inline_object_request_param.py index 59e4d59..de8a0ec 100644 --- a/src/knockapi/types/inline_object_request_param.py +++ b/src/knockapi/types/inline_object_request_param.py @@ -14,6 +14,8 @@ class InlineObjectRequestParamTyped(TypedDict, total=False): + """A custom [Object](/concepts/objects) entity which belongs to a collection.""" + id: Required[str] """Unique identifier for the object.""" diff --git a/src/knockapi/types/message.py b/src/knockapi/types/message.py index 3bf24aa..b368ffd 100644 --- a/src/knockapi/types/message.py +++ b/src/knockapi/types/message.py @@ -13,6 +13,8 @@ class Source(BaseModel): + """The workflow or guide that triggered the message.""" + api_typename: str = FieldInfo(alias="__typename") categories: List[str] @@ -45,6 +47,8 @@ class Source(BaseModel): class Channel(BaseModel): + """A configured channel, which is a way to route messages to a provider.""" + id: str """The unique identifier for the channel.""" @@ -68,6 +72,10 @@ class Channel(BaseModel): class Message(BaseModel): + """ + Represents a single message that was generated by a workflow for a given channel. + """ + id: str """The unique identifier for the message.""" diff --git a/src/knockapi/types/message_delivery_log.py b/src/knockapi/types/message_delivery_log.py index 49273a6..a27ceed 100644 --- a/src/knockapi/types/message_delivery_log.py +++ b/src/knockapi/types/message_delivery_log.py @@ -11,6 +11,8 @@ class Request(BaseModel): + """A message delivery log request.""" + body: Union[str, Dict[str, object], None] = None """The body content that was sent with the request.""" @@ -31,6 +33,8 @@ class Request(BaseModel): class Response(BaseModel): + """A message delivery log response.""" + body: Union[str, Dict[str, object], None] = None """The body content that was received with the response.""" @@ -42,6 +46,10 @@ class Response(BaseModel): class MessageDeliveryLog(BaseModel): + """ + A message delivery log contains a `request` from Knock to a downstream provider and the `response` that was returned. + """ + id: str """The unique identifier for the message delivery log.""" diff --git a/src/knockapi/types/message_event.py b/src/knockapi/types/message_event.py index dba46e2..0bf16aa 100644 --- a/src/knockapi/types/message_event.py +++ b/src/knockapi/types/message_event.py @@ -13,6 +13,11 @@ class MessageEvent(BaseModel): + """A message event. + + Occurs when a message [delivery or engagement status](/send-notifications/message-statuses) changes. + """ + id: str """The unique identifier for the message event.""" diff --git a/src/knockapi/types/message_get_content_response.py b/src/knockapi/types/message_get_content_response.py index c22339a..8433cf0 100644 --- a/src/knockapi/types/message_get_content_response.py +++ b/src/knockapi/types/message_get_content_response.py @@ -26,6 +26,8 @@ class DataMessageEmailContent(BaseModel): + """The content of an email message.""" + api_typename: str = FieldInfo(alias="__typename") """The typename of the schema.""" @@ -55,6 +57,8 @@ class DataMessageEmailContent(BaseModel): class DataMessageSMSContent(BaseModel): + """The content of an SMS message.""" + api_typename: str = FieldInfo(alias="__typename") """The typename of the schema.""" @@ -66,6 +70,8 @@ class DataMessageSMSContent(BaseModel): class DataMessagePushContent(BaseModel): + """Push channel data.""" + token: str """The device token to send the push notification to.""" @@ -83,6 +89,8 @@ class DataMessagePushContent(BaseModel): class DataMessageChatContentTemplateBlock(BaseModel): + """A block in a message in a chat.""" + content: str """The actual content of the block.""" @@ -94,6 +102,8 @@ class DataMessageChatContentTemplateBlock(BaseModel): class DataMessageChatContentTemplate(BaseModel): + """The template structure for the chat message.""" + blocks: Optional[List[DataMessageChatContentTemplateBlock]] = None """The blocks of the message in a chat.""" @@ -105,6 +115,8 @@ class DataMessageChatContentTemplate(BaseModel): class DataMessageChatContent(BaseModel): + """The content of a chat message.""" + api_typename: str = FieldInfo(alias="__typename") """The typename of the schema.""" @@ -119,6 +131,8 @@ class DataMessageChatContent(BaseModel): class DataMessageInAppFeedContentBlockMessageInAppFeedContentBlock(BaseModel): + """A block in a message in an app feed.""" + content: str """The content of the block in a message in an app feed.""" @@ -133,6 +147,8 @@ class DataMessageInAppFeedContentBlockMessageInAppFeedContentBlock(BaseModel): class DataMessageInAppFeedContentBlockMessageInAppFeedButtonSetBlockButton(BaseModel): + """A button in an in app feed message.""" + action: str """The action to take when the button is clicked.""" @@ -144,6 +160,8 @@ class DataMessageInAppFeedContentBlockMessageInAppFeedButtonSetBlockButton(BaseM class DataMessageInAppFeedContentBlockMessageInAppFeedButtonSetBlock(BaseModel): + """A button set block in a message in an app feed.""" + buttons: List[DataMessageInAppFeedContentBlockMessageInAppFeedButtonSetBlockButton] """A list of buttons in an in app feed message.""" @@ -161,6 +179,8 @@ class DataMessageInAppFeedContentBlockMessageInAppFeedButtonSetBlock(BaseModel): class DataMessageInAppFeedContent(BaseModel): + """The content of an in-app feed message.""" + api_typename: str = FieldInfo(alias="__typename") """The typename of the schema.""" @@ -178,6 +198,8 @@ class DataMessageInAppFeedContent(BaseModel): class MessageGetContentResponse(BaseModel): + """The content of a message.""" + api_typename: str = FieldInfo(alias="__typename") """The typename of the schema.""" diff --git a/src/knockapi/types/messages/batch_get_content_response.py b/src/knockapi/types/messages/batch_get_content_response.py index 1205f7a..b88ebc1 100644 --- a/src/knockapi/types/messages/batch_get_content_response.py +++ b/src/knockapi/types/messages/batch_get_content_response.py @@ -27,6 +27,8 @@ class BatchGetContentResponseItemDataMessageEmailContent(BaseModel): + """The content of an email message.""" + api_typename: str = FieldInfo(alias="__typename") """The typename of the schema.""" @@ -56,6 +58,8 @@ class BatchGetContentResponseItemDataMessageEmailContent(BaseModel): class BatchGetContentResponseItemDataMessageSMSContent(BaseModel): + """The content of an SMS message.""" + api_typename: str = FieldInfo(alias="__typename") """The typename of the schema.""" @@ -67,6 +71,8 @@ class BatchGetContentResponseItemDataMessageSMSContent(BaseModel): class BatchGetContentResponseItemDataMessagePushContent(BaseModel): + """Push channel data.""" + token: str """The device token to send the push notification to.""" @@ -84,6 +90,8 @@ class BatchGetContentResponseItemDataMessagePushContent(BaseModel): class BatchGetContentResponseItemDataMessageChatContentTemplateBlock(BaseModel): + """A block in a message in a chat.""" + content: str """The actual content of the block.""" @@ -95,6 +103,8 @@ class BatchGetContentResponseItemDataMessageChatContentTemplateBlock(BaseModel): class BatchGetContentResponseItemDataMessageChatContentTemplate(BaseModel): + """The template structure for the chat message.""" + blocks: Optional[List[BatchGetContentResponseItemDataMessageChatContentTemplateBlock]] = None """The blocks of the message in a chat.""" @@ -106,6 +116,8 @@ class BatchGetContentResponseItemDataMessageChatContentTemplate(BaseModel): class BatchGetContentResponseItemDataMessageChatContent(BaseModel): + """The content of a chat message.""" + api_typename: str = FieldInfo(alias="__typename") """The typename of the schema.""" @@ -120,6 +132,8 @@ class BatchGetContentResponseItemDataMessageChatContent(BaseModel): class BatchGetContentResponseItemDataMessageInAppFeedContentBlockMessageInAppFeedContentBlock(BaseModel): + """A block in a message in an app feed.""" + content: str """The content of the block in a message in an app feed.""" @@ -134,6 +148,8 @@ class BatchGetContentResponseItemDataMessageInAppFeedContentBlockMessageInAppFee class BatchGetContentResponseItemDataMessageInAppFeedContentBlockMessageInAppFeedButtonSetBlockButton(BaseModel): + """A button in an in app feed message.""" + action: str """The action to take when the button is clicked.""" @@ -145,6 +161,8 @@ class BatchGetContentResponseItemDataMessageInAppFeedContentBlockMessageInAppFee class BatchGetContentResponseItemDataMessageInAppFeedContentBlockMessageInAppFeedButtonSetBlock(BaseModel): + """A button set block in a message in an app feed.""" + buttons: List[BatchGetContentResponseItemDataMessageInAppFeedContentBlockMessageInAppFeedButtonSetBlockButton] """A list of buttons in an in app feed message.""" @@ -162,6 +180,8 @@ class BatchGetContentResponseItemDataMessageInAppFeedContentBlockMessageInAppFee class BatchGetContentResponseItemDataMessageInAppFeedContent(BaseModel): + """The content of an in-app feed message.""" + api_typename: str = FieldInfo(alias="__typename") """The typename of the schema.""" @@ -179,6 +199,8 @@ class BatchGetContentResponseItemDataMessageInAppFeedContent(BaseModel): class BatchGetContentResponseItem(BaseModel): + """The content of a message.""" + api_typename: str = FieldInfo(alias="__typename") """The typename of the schema.""" diff --git a/src/knockapi/types/object.py b/src/knockapi/types/object.py index 3d2ed63..8148f7a 100644 --- a/src/knockapi/types/object.py +++ b/src/knockapi/types/object.py @@ -11,6 +11,8 @@ class Object(BaseModel): + """A custom [Object](/concepts/objects) entity which belongs to a collection.""" + id: str """Unique identifier for the object.""" diff --git a/src/knockapi/types/object_list_subscriptions_params.py b/src/knockapi/types/object_list_subscriptions_params.py index f8a45fc..ea79c1e 100644 --- a/src/knockapi/types/object_list_subscriptions_params.py +++ b/src/knockapi/types/object_list_subscriptions_params.py @@ -39,6 +39,8 @@ class ObjectListSubscriptionsParams(TypedDict, total=False): class Object(TypedDict, total=False): + """A reference to a recipient object.""" + id: str """An identifier for the recipient object.""" diff --git a/src/knockapi/types/object_set_preferences_params.py b/src/knockapi/types/object_set_preferences_params.py index ce0037b..7bfa77e 100644 --- a/src/knockapi/types/object_set_preferences_params.py +++ b/src/knockapi/types/object_set_preferences_params.py @@ -59,6 +59,10 @@ class ObjectSetPreferencesParams(TypedDict, total=False): class CategoriesPreferenceSetWorkflowCategorySettingObject(TypedDict, total=False): + """ + The settings object for a workflow or category, where you can specify channel types or conditions. + """ + channel_types: Optional[PreferenceSetChannelTypesParam] """Channel type preferences.""" @@ -77,6 +81,10 @@ class CategoriesPreferenceSetWorkflowCategorySettingObject(TypedDict, total=Fals class WorkflowsPreferenceSetWorkflowCategorySettingObject(TypedDict, total=False): + """ + The settings object for a workflow or category, where you can specify channel types or conditions. + """ + channel_types: Optional[PreferenceSetChannelTypesParam] """Channel type preferences.""" diff --git a/src/knockapi/types/objects/bulk_add_subscriptions_params.py b/src/knockapi/types/objects/bulk_add_subscriptions_params.py index cb4584e..9f137d7 100644 --- a/src/knockapi/types/objects/bulk_add_subscriptions_params.py +++ b/src/knockapi/types/objects/bulk_add_subscriptions_params.py @@ -17,6 +17,8 @@ class BulkAddSubscriptionsParams(TypedDict, total=False): class Subscription(TypedDict, total=False): + """A list of subscriptions. 1 subscribed-to id, and N subscriber recipients.""" + id: Required[str] """Unique identifier for the object.""" diff --git a/src/knockapi/types/objects/bulk_delete_subscriptions_params.py b/src/knockapi/types/objects/bulk_delete_subscriptions_params.py index ab33214..08c4c54 100644 --- a/src/knockapi/types/objects/bulk_delete_subscriptions_params.py +++ b/src/knockapi/types/objects/bulk_delete_subscriptions_params.py @@ -17,6 +17,8 @@ class BulkDeleteSubscriptionsParams(TypedDict, total=False): class Subscription(TypedDict, total=False): + """A list of subscriptions. 1 subscribed-to id, and N subscriber recipients.""" + id: Required[str] """Unique identifier for the object.""" diff --git a/src/knockapi/types/objects/bulk_set_params.py b/src/knockapi/types/objects/bulk_set_params.py index cebd5f8..27a8da0 100644 --- a/src/knockapi/types/objects/bulk_set_params.py +++ b/src/knockapi/types/objects/bulk_set_params.py @@ -19,6 +19,8 @@ class BulkSetParams(TypedDict, total=False): class ObjectTyped(TypedDict, total=False): + """A custom [Object](/concepts/objects) entity which belongs to a collection.""" + id: Required[str] """Unique identifier for the object.""" diff --git a/src/knockapi/types/providers/ms_team_check_auth_response.py b/src/knockapi/types/providers/ms_team_check_auth_response.py index 154fde3..c1d5e90 100644 --- a/src/knockapi/types/providers/ms_team_check_auth_response.py +++ b/src/knockapi/types/providers/ms_team_check_auth_response.py @@ -8,6 +8,8 @@ class Connection(BaseModel): + """A Microsoft Teams connection object.""" + ok: bool """Whether the Microsoft Teams connection is valid.""" @@ -16,5 +18,7 @@ class Connection(BaseModel): class MsTeamCheckAuthResponse(BaseModel): + """The response from a Microsoft Teams auth check request.""" + connection: Connection """A Microsoft Teams connection object.""" diff --git a/src/knockapi/types/providers/ms_team_list_channels_response.py b/src/knockapi/types/providers/ms_team_list_channels_response.py index 5d4ad75..c7d7451 100644 --- a/src/knockapi/types/providers/ms_team_list_channels_response.py +++ b/src/knockapi/types/providers/ms_team_list_channels_response.py @@ -30,5 +30,9 @@ class MsTeamsChannel(BaseModel): class MsTeamListChannelsResponse(BaseModel): + """ + The response from a Microsoft Teams provider request, containing a list of channels. + """ + ms_teams_channels: List[MsTeamsChannel] """List of Microsoft Teams channels.""" diff --git a/src/knockapi/types/providers/ms_team_revoke_access_response.py b/src/knockapi/types/providers/ms_team_revoke_access_response.py index c9c8a1b..9281e78 100644 --- a/src/knockapi/types/providers/ms_team_revoke_access_response.py +++ b/src/knockapi/types/providers/ms_team_revoke_access_response.py @@ -8,5 +8,7 @@ class MsTeamRevokeAccessResponse(BaseModel): + """A response indicating the operation was successful.""" + ok: Optional[str] = None """OK response.""" diff --git a/src/knockapi/types/providers/slack_check_auth_response.py b/src/knockapi/types/providers/slack_check_auth_response.py index 13b705c..2d07ca1 100644 --- a/src/knockapi/types/providers/slack_check_auth_response.py +++ b/src/knockapi/types/providers/slack_check_auth_response.py @@ -8,6 +8,8 @@ class Connection(BaseModel): + """A Slack connection object.""" + ok: bool """Whether the Slack connection is valid.""" @@ -16,5 +18,7 @@ class Connection(BaseModel): class SlackCheckAuthResponse(BaseModel): + """The response from a Slack auth check request.""" + connection: Connection """A Slack connection object.""" diff --git a/src/knockapi/types/providers/slack_list_channels_response.py b/src/knockapi/types/providers/slack_list_channels_response.py index 35376d2..5d374b5 100644 --- a/src/knockapi/types/providers/slack_list_channels_response.py +++ b/src/knockapi/types/providers/slack_list_channels_response.py @@ -6,6 +6,8 @@ class SlackListChannelsResponse(BaseModel): + """A Slack channel.""" + id: str """A Slack channel ID from the Slack provider.""" diff --git a/src/knockapi/types/providers/slack_revoke_access_response.py b/src/knockapi/types/providers/slack_revoke_access_response.py index 613697b..ad0479d 100644 --- a/src/knockapi/types/providers/slack_revoke_access_response.py +++ b/src/knockapi/types/providers/slack_revoke_access_response.py @@ -8,5 +8,7 @@ class SlackRevokeAccessResponse(BaseModel): + """A response indicating the operation was successful.""" + ok: Optional[str] = None """OK response.""" diff --git a/src/knockapi/types/recipient_reference.py b/src/knockapi/types/recipient_reference.py index 1a85c86..fd4d4c8 100644 --- a/src/knockapi/types/recipient_reference.py +++ b/src/knockapi/types/recipient_reference.py @@ -9,6 +9,8 @@ class ObjectReference(BaseModel): + """A reference to a recipient object.""" + id: Optional[str] = None """An identifier for the recipient object.""" diff --git a/src/knockapi/types/recipient_reference_param.py b/src/knockapi/types/recipient_reference_param.py index ba1e3ae..dec0f9e 100644 --- a/src/knockapi/types/recipient_reference_param.py +++ b/src/knockapi/types/recipient_reference_param.py @@ -9,6 +9,8 @@ class ObjectReference(TypedDict, total=False): + """A reference to a recipient object.""" + id: str """An identifier for the recipient object.""" diff --git a/src/knockapi/types/recipients/aws_sns_push_channel_data_devices_only_param.py b/src/knockapi/types/recipients/aws_sns_push_channel_data_devices_only_param.py index cdeb803..14eb418 100644 --- a/src/knockapi/types/recipients/aws_sns_push_channel_data_devices_only_param.py +++ b/src/knockapi/types/recipients/aws_sns_push_channel_data_devices_only_param.py @@ -33,6 +33,8 @@ class Device(TypedDict, total=False): class AwsSnsPushChannelDataDevicesOnlyParam(TypedDict, total=False): + """AWS SNS push channel data.""" + devices: Required[Iterable[Device]] """A list of devices. diff --git a/src/knockapi/types/recipients/aws_sns_push_channel_data_target_arns_only_param.py b/src/knockapi/types/recipients/aws_sns_push_channel_data_target_arns_only_param.py index 53f9172..7a34dd2 100644 --- a/src/knockapi/types/recipients/aws_sns_push_channel_data_target_arns_only_param.py +++ b/src/knockapi/types/recipients/aws_sns_push_channel_data_target_arns_only_param.py @@ -10,6 +10,8 @@ class AwsSnsPushChannelDataTargetArnsOnlyParam(TypedDict, total=False): + """AWS SNS push channel data.""" + target_arns: Required[SequenceNotStr[str]] """A list of platform endpoint ARNs. diff --git a/src/knockapi/types/recipients/channel_data.py b/src/knockapi/types/recipients/channel_data.py index b9b9a37..6bf8398 100644 --- a/src/knockapi/types/recipients/channel_data.py +++ b/src/knockapi/types/recipients/channel_data.py @@ -42,6 +42,8 @@ class DataPushChannelDataFullDevice(BaseModel): class DataPushChannelDataFull(BaseModel): + """Push channel data.""" + devices: List[DataPushChannelDataFullDevice] """A list of devices. @@ -77,6 +79,8 @@ class DataAwssnsPushChannelDataFullDevice(BaseModel): class DataAwssnsPushChannelDataFull(BaseModel): + """AWS SNS push channel data.""" + devices: List[DataAwssnsPushChannelDataFullDevice] """A list of devices. @@ -102,6 +106,8 @@ class DataAwssnsPushChannelDataFull(BaseModel): class ChannelData(BaseModel): + """Channel data for a given channel type.""" + api_typename: str = FieldInfo(alias="__typename") """The typename of the schema.""" diff --git a/src/knockapi/types/recipients/discord_channel_data.py b/src/knockapi/types/recipients/discord_channel_data.py index 0425a8f..cc00dfc 100644 --- a/src/knockapi/types/recipients/discord_channel_data.py +++ b/src/knockapi/types/recipients/discord_channel_data.py @@ -15,16 +15,22 @@ class ConnectionDiscordChannelConnection(BaseModel): + """Discord channel connection.""" + channel_id: str """Discord channel ID.""" class ConnectionDiscordIncomingWebhookConnectionIncomingWebhook(BaseModel): + """Discord incoming webhook object.""" + url: str """Incoming webhook URL.""" class ConnectionDiscordIncomingWebhookConnection(BaseModel): + """Discord incoming webhook connection.""" + incoming_webhook: ConnectionDiscordIncomingWebhookConnectionIncomingWebhook """Discord incoming webhook object.""" @@ -33,5 +39,7 @@ class ConnectionDiscordIncomingWebhookConnection(BaseModel): class DiscordChannelData(BaseModel): + """Discord channel data.""" + connections: List[Connection] """List of Discord channel connections.""" diff --git a/src/knockapi/types/recipients/discord_channel_data_param.py b/src/knockapi/types/recipients/discord_channel_data_param.py index f621934..f4664a8 100644 --- a/src/knockapi/types/recipients/discord_channel_data_param.py +++ b/src/knockapi/types/recipients/discord_channel_data_param.py @@ -15,16 +15,22 @@ class ConnectionDiscordChannelConnection(TypedDict, total=False): + """Discord channel connection.""" + channel_id: Required[str] """Discord channel ID.""" class ConnectionDiscordIncomingWebhookConnectionIncomingWebhook(TypedDict, total=False): + """Discord incoming webhook object.""" + url: Required[str] """Incoming webhook URL.""" class ConnectionDiscordIncomingWebhookConnection(TypedDict, total=False): + """Discord incoming webhook connection.""" + incoming_webhook: Required[ConnectionDiscordIncomingWebhookConnectionIncomingWebhook] """Discord incoming webhook object.""" @@ -33,5 +39,7 @@ class ConnectionDiscordIncomingWebhookConnection(TypedDict, total=False): class DiscordChannelDataParam(TypedDict, total=False): + """Discord channel data.""" + connections: Required[Iterable[Connection]] """List of Discord channel connections.""" diff --git a/src/knockapi/types/recipients/ms_teams_channel_data.py b/src/knockapi/types/recipients/ms_teams_channel_data.py index 8b259e1..fab64a0 100644 --- a/src/knockapi/types/recipients/ms_teams_channel_data.py +++ b/src/knockapi/types/recipients/ms_teams_channel_data.py @@ -15,6 +15,8 @@ class ConnectionMsTeamsTokenConnection(BaseModel): + """Microsoft Teams token connection.""" + ms_teams_channel_id: Optional[str] = None """Microsoft Teams channel ID.""" @@ -29,11 +31,15 @@ class ConnectionMsTeamsTokenConnection(BaseModel): class ConnectionMsTeamsIncomingWebhookConnectionIncomingWebhook(BaseModel): + """Microsoft Teams incoming webhook.""" + url: str """Microsoft Teams incoming webhook URL.""" class ConnectionMsTeamsIncomingWebhookConnection(BaseModel): + """Microsoft Teams incoming webhook connection.""" + incoming_webhook: ConnectionMsTeamsIncomingWebhookConnectionIncomingWebhook """Microsoft Teams incoming webhook.""" @@ -42,6 +48,8 @@ class ConnectionMsTeamsIncomingWebhookConnection(BaseModel): class MsTeamsChannelData(BaseModel): + """Microsoft Teams channel data.""" + connections: List[Connection] """List of Microsoft Teams connections.""" diff --git a/src/knockapi/types/recipients/ms_teams_channel_data_param.py b/src/knockapi/types/recipients/ms_teams_channel_data_param.py index 6627b69..4648a57 100644 --- a/src/knockapi/types/recipients/ms_teams_channel_data_param.py +++ b/src/knockapi/types/recipients/ms_teams_channel_data_param.py @@ -15,6 +15,8 @@ class ConnectionMsTeamsTokenConnection(TypedDict, total=False): + """Microsoft Teams token connection.""" + ms_teams_channel_id: Optional[str] """Microsoft Teams channel ID.""" @@ -29,11 +31,15 @@ class ConnectionMsTeamsTokenConnection(TypedDict, total=False): class ConnectionMsTeamsIncomingWebhookConnectionIncomingWebhook(TypedDict, total=False): + """Microsoft Teams incoming webhook.""" + url: Required[str] """Microsoft Teams incoming webhook URL.""" class ConnectionMsTeamsIncomingWebhookConnection(TypedDict, total=False): + """Microsoft Teams incoming webhook connection.""" + incoming_webhook: Required[ConnectionMsTeamsIncomingWebhookConnectionIncomingWebhook] """Microsoft Teams incoming webhook.""" @@ -42,6 +48,8 @@ class ConnectionMsTeamsIncomingWebhookConnection(TypedDict, total=False): class MsTeamsChannelDataParam(TypedDict, total=False): + """Microsoft Teams channel data.""" + connections: Required[Iterable[Connection]] """List of Microsoft Teams connections.""" diff --git a/src/knockapi/types/recipients/one_signal_channel_data_player_ids_only.py b/src/knockapi/types/recipients/one_signal_channel_data_player_ids_only.py index e683ff4..f0ba659 100644 --- a/src/knockapi/types/recipients/one_signal_channel_data_player_ids_only.py +++ b/src/knockapi/types/recipients/one_signal_channel_data_player_ids_only.py @@ -8,5 +8,7 @@ class OneSignalChannelDataPlayerIDsOnly(BaseModel): + """OneSignal channel data.""" + player_ids: List[str] """A list of OneSignal player IDs.""" diff --git a/src/knockapi/types/recipients/one_signal_channel_data_player_ids_only_param.py b/src/knockapi/types/recipients/one_signal_channel_data_player_ids_only_param.py index bcc1bfd..80791e9 100644 --- a/src/knockapi/types/recipients/one_signal_channel_data_player_ids_only_param.py +++ b/src/knockapi/types/recipients/one_signal_channel_data_player_ids_only_param.py @@ -10,5 +10,7 @@ class OneSignalChannelDataPlayerIDsOnlyParam(TypedDict, total=False): + """OneSignal channel data.""" + player_ids: Required[SequenceNotStr[str]] """A list of OneSignal player IDs.""" diff --git a/src/knockapi/types/recipients/preference_set.py b/src/knockapi/types/recipients/preference_set.py index b690579..ac8b56a 100644 --- a/src/knockapi/types/recipients/preference_set.py +++ b/src/knockapi/types/recipients/preference_set.py @@ -23,6 +23,10 @@ class CategoriesPreferenceSetWorkflowCategorySettingObject(BaseModel): + """ + The settings object for a workflow or category, where you can specify channel types or conditions. + """ + channel_types: Optional[PreferenceSetChannelTypes] = None """Channel type preferences.""" @@ -41,6 +45,10 @@ class CategoriesPreferenceSetWorkflowCategorySettingObject(BaseModel): class WorkflowsPreferenceSetWorkflowCategorySettingObject(BaseModel): + """ + The settings object for a workflow or category, where you can specify channel types or conditions. + """ + channel_types: Optional[PreferenceSetChannelTypes] = None """Channel type preferences.""" @@ -55,6 +63,10 @@ class WorkflowsPreferenceSetWorkflowCategorySettingObject(BaseModel): class PreferenceSet(BaseModel): + """ + A preference set represents a specific set of notification preferences for a recipient. A recipient can have multiple preference sets. + """ + id: str """Unique identifier for the preference set.""" diff --git a/src/knockapi/types/recipients/preference_set_channel_setting.py b/src/knockapi/types/recipients/preference_set_channel_setting.py index 33388b7..244e32b 100644 --- a/src/knockapi/types/recipients/preference_set_channel_setting.py +++ b/src/knockapi/types/recipients/preference_set_channel_setting.py @@ -9,5 +9,10 @@ class PreferenceSetChannelSetting(BaseModel): + """A set of settings for a specific channel. + + Currently, this can only be a list of conditions to apply. + """ + conditions: List[Condition] """A list of conditions to apply to a specific channel.""" diff --git a/src/knockapi/types/recipients/preference_set_channel_setting_param.py b/src/knockapi/types/recipients/preference_set_channel_setting_param.py index 660d701..6caa7a4 100644 --- a/src/knockapi/types/recipients/preference_set_channel_setting_param.py +++ b/src/knockapi/types/recipients/preference_set_channel_setting_param.py @@ -11,5 +11,10 @@ class PreferenceSetChannelSettingParam(TypedDict, total=False): + """A set of settings for a specific channel. + + Currently, this can only be a list of conditions to apply. + """ + conditions: Required[Iterable[Condition]] """A list of conditions to apply to a specific channel.""" diff --git a/src/knockapi/types/recipients/preference_set_channel_type_setting.py b/src/knockapi/types/recipients/preference_set_channel_type_setting.py index 4b74464..0b2115b 100644 --- a/src/knockapi/types/recipients/preference_set_channel_type_setting.py +++ b/src/knockapi/types/recipients/preference_set_channel_type_setting.py @@ -9,5 +9,10 @@ class PreferenceSetChannelTypeSetting(BaseModel): + """A set of settings for a channel type. + + Currently, this can only be a list of conditions to apply. + """ + conditions: List[Condition] """A list of conditions to apply to a channel type.""" diff --git a/src/knockapi/types/recipients/preference_set_channel_type_setting_param.py b/src/knockapi/types/recipients/preference_set_channel_type_setting_param.py index a058812..d9a0945 100644 --- a/src/knockapi/types/recipients/preference_set_channel_type_setting_param.py +++ b/src/knockapi/types/recipients/preference_set_channel_type_setting_param.py @@ -11,5 +11,10 @@ class PreferenceSetChannelTypeSettingParam(TypedDict, total=False): + """A set of settings for a channel type. + + Currently, this can only be a list of conditions to apply. + """ + conditions: Required[Iterable[Condition]] """A list of conditions to apply to a channel type.""" diff --git a/src/knockapi/types/recipients/preference_set_channel_types.py b/src/knockapi/types/recipients/preference_set_channel_types.py index d0d9543..86fde8f 100644 --- a/src/knockapi/types/recipients/preference_set_channel_types.py +++ b/src/knockapi/types/recipients/preference_set_channel_types.py @@ -22,6 +22,8 @@ class PreferenceSetChannelTypes(BaseModel): + """Channel type preferences.""" + chat: Optional[Chat] = None """Whether the channel type is enabled for the preference set.""" diff --git a/src/knockapi/types/recipients/preference_set_channel_types_param.py b/src/knockapi/types/recipients/preference_set_channel_types_param.py index f9c48f5..8877db6 100644 --- a/src/knockapi/types/recipients/preference_set_channel_types_param.py +++ b/src/knockapi/types/recipients/preference_set_channel_types_param.py @@ -23,6 +23,8 @@ class PreferenceSetChannelTypesParam(TypedDict, total=False): + """Channel type preferences.""" + chat: Chat """Whether the channel type is enabled for the preference set.""" diff --git a/src/knockapi/types/recipients/preference_set_request_param.py b/src/knockapi/types/recipients/preference_set_request_param.py index 268d264..d84aa20 100644 --- a/src/knockapi/types/recipients/preference_set_request_param.py +++ b/src/knockapi/types/recipients/preference_set_request_param.py @@ -25,6 +25,10 @@ class CategoriesPreferenceSetWorkflowCategorySettingObject(TypedDict, total=False): + """ + The settings object for a workflow or category, where you can specify channel types or conditions. + """ + channel_types: Optional[PreferenceSetChannelTypesParam] """Channel type preferences.""" @@ -43,6 +47,10 @@ class CategoriesPreferenceSetWorkflowCategorySettingObject(TypedDict, total=Fals class WorkflowsPreferenceSetWorkflowCategorySettingObject(TypedDict, total=False): + """ + The settings object for a workflow or category, where you can specify channel types or conditions. + """ + channel_types: Optional[PreferenceSetChannelTypesParam] """Channel type preferences.""" @@ -57,6 +65,8 @@ class WorkflowsPreferenceSetWorkflowCategorySettingObject(TypedDict, total=False class PreferenceSetRequestParam(TypedDict, total=False): + """A request to set a preference set for a recipient.""" + _persistence_strategy: Annotated[Literal["merge", "replace"], PropertyInfo(alias="__persistence_strategy__")] """Controls how the preference set is persisted. diff --git a/src/knockapi/types/recipients/push_channel_data_devices_only_param.py b/src/knockapi/types/recipients/push_channel_data_devices_only_param.py index f183ff6..33f7921 100644 --- a/src/knockapi/types/recipients/push_channel_data_devices_only_param.py +++ b/src/knockapi/types/recipients/push_channel_data_devices_only_param.py @@ -29,6 +29,8 @@ class Device(TypedDict, total=False): class PushChannelDataDevicesOnlyParam(TypedDict, total=False): + """Push channel data.""" + devices: Required[Iterable[Device]] """A list of devices. diff --git a/src/knockapi/types/recipients/push_channel_data_tokens_only_param.py b/src/knockapi/types/recipients/push_channel_data_tokens_only_param.py index 77960f1..e7d2aad 100644 --- a/src/knockapi/types/recipients/push_channel_data_tokens_only_param.py +++ b/src/knockapi/types/recipients/push_channel_data_tokens_only_param.py @@ -10,5 +10,7 @@ class PushChannelDataTokensOnlyParam(TypedDict, total=False): + """Push channel data.""" + tokens: Required[SequenceNotStr[str]] """A list of push channel tokens.""" diff --git a/src/knockapi/types/recipients/slack_channel_data.py b/src/knockapi/types/recipients/slack_channel_data.py index 5855872..261631d 100644 --- a/src/knockapi/types/recipients/slack_channel_data.py +++ b/src/knockapi/types/recipients/slack_channel_data.py @@ -15,6 +15,8 @@ class ConnectionSlackTokenConnection(BaseModel): + """A Slack connection token.""" + access_token: Optional[str] = None """A Slack access token.""" @@ -26,6 +28,8 @@ class ConnectionSlackTokenConnection(BaseModel): class ConnectionSlackIncomingWebhookConnection(BaseModel): + """A Slack connection incoming webhook.""" + url: str """The URL of the incoming webhook for a Slack connection.""" @@ -34,11 +38,15 @@ class ConnectionSlackIncomingWebhookConnection(BaseModel): class Token(BaseModel): + """A Slack connection token.""" + access_token: Optional[str] = None """A Slack access token.""" class SlackChannelData(BaseModel): + """Slack channel data.""" + connections: List[Connection] """List of Slack channel connections.""" diff --git a/src/knockapi/types/recipients/slack_channel_data_param.py b/src/knockapi/types/recipients/slack_channel_data_param.py index 3943fa6..0100c10 100644 --- a/src/knockapi/types/recipients/slack_channel_data_param.py +++ b/src/knockapi/types/recipients/slack_channel_data_param.py @@ -15,6 +15,8 @@ class ConnectionSlackTokenConnection(TypedDict, total=False): + """A Slack connection token.""" + access_token: Optional[str] """A Slack access token.""" @@ -26,6 +28,8 @@ class ConnectionSlackTokenConnection(TypedDict, total=False): class ConnectionSlackIncomingWebhookConnection(TypedDict, total=False): + """A Slack connection incoming webhook.""" + url: Required[str] """The URL of the incoming webhook for a Slack connection.""" @@ -34,11 +38,15 @@ class ConnectionSlackIncomingWebhookConnection(TypedDict, total=False): class Token(TypedDict, total=False): + """A Slack connection token.""" + access_token: Required[Optional[str]] """A Slack access token.""" class SlackChannelDataParam(TypedDict, total=False): + """Slack channel data.""" + connections: Required[Iterable[Connection]] """List of Slack channel connections.""" diff --git a/src/knockapi/types/recipients/subscription.py b/src/knockapi/types/recipients/subscription.py index ff47ce9..c4d8662 100644 --- a/src/knockapi/types/recipients/subscription.py +++ b/src/knockapi/types/recipients/subscription.py @@ -14,6 +14,8 @@ class Subscription(BaseModel): + """A subscription object.""" + api_typename: str = FieldInfo(alias="__typename") """The typename of the schema.""" diff --git a/src/knockapi/types/schedule.py b/src/knockapi/types/schedule.py index 56fd93e..5499496 100644 --- a/src/knockapi/types/schedule.py +++ b/src/knockapi/types/schedule.py @@ -13,6 +13,8 @@ class Schedule(BaseModel): + """A schedule represents a recurring workflow execution.""" + id: str """Unique identifier for the schedule.""" diff --git a/src/knockapi/types/schedule_repeat_rule.py b/src/knockapi/types/schedule_repeat_rule.py index cec89c8..e778297 100644 --- a/src/knockapi/types/schedule_repeat_rule.py +++ b/src/knockapi/types/schedule_repeat_rule.py @@ -11,6 +11,8 @@ class ScheduleRepeatRule(BaseModel): + """The repeat rule for the schedule.""" + frequency: Literal["daily", "weekly", "monthly", "hourly"] """The frequency of the schedule.""" diff --git a/src/knockapi/types/schedule_repeat_rule_param.py b/src/knockapi/types/schedule_repeat_rule_param.py index 08133ee..0c527b7 100644 --- a/src/knockapi/types/schedule_repeat_rule_param.py +++ b/src/knockapi/types/schedule_repeat_rule_param.py @@ -11,6 +11,8 @@ class ScheduleRepeatRuleParam(TypedDict, total=False): + """The repeat rule for the schedule.""" + frequency: Required[Literal["daily", "weekly", "monthly", "hourly"]] """The frequency of the schedule.""" diff --git a/src/knockapi/types/schedules/bulk_create_params.py b/src/knockapi/types/schedules/bulk_create_params.py index 6b59e5c..f09b60e 100644 --- a/src/knockapi/types/schedules/bulk_create_params.py +++ b/src/knockapi/types/schedules/bulk_create_params.py @@ -20,6 +20,8 @@ class BulkCreateParams(TypedDict, total=False): class Schedule(TypedDict, total=False): + """A schedule represents a recurring workflow execution.""" + workflow: Required[str] """The key of the workflow.""" diff --git a/src/knockapi/types/shared/condition.py b/src/knockapi/types/shared/condition.py index 0b57d1c..fe3f5d6 100644 --- a/src/knockapi/types/shared/condition.py +++ b/src/knockapi/types/shared/condition.py @@ -9,6 +9,8 @@ class Condition(BaseModel): + """A condition to be evaluated.""" + argument: Optional[str] = None """The argument value to compare against in the condition.""" diff --git a/src/knockapi/types/shared/page_info.py b/src/knockapi/types/shared/page_info.py index c211dbd..e0b49f3 100644 --- a/src/knockapi/types/shared/page_info.py +++ b/src/knockapi/types/shared/page_info.py @@ -10,6 +10,8 @@ class PageInfo(BaseModel): + """Pagination information for a list of resources.""" + api_typename: str = FieldInfo(alias="__typename") """The typename of the schema.""" diff --git a/src/knockapi/types/shared_params/condition.py b/src/knockapi/types/shared_params/condition.py index 1cbb66c..eb29011 100644 --- a/src/knockapi/types/shared_params/condition.py +++ b/src/knockapi/types/shared_params/condition.py @@ -9,6 +9,8 @@ class Condition(TypedDict, total=False): + """A condition to be evaluated.""" + argument: Required[Optional[str]] """The argument value to compare against in the condition.""" diff --git a/src/knockapi/types/tenant.py b/src/knockapi/types/tenant.py index 5270383..d9fd2c2 100644 --- a/src/knockapi/types/tenant.py +++ b/src/knockapi/types/tenant.py @@ -11,6 +11,8 @@ class SettingsBranding(BaseModel): + """The branding for the tenant.""" + icon_url: Optional[str] = None """The icon URL for the tenant. @@ -31,6 +33,8 @@ class SettingsBranding(BaseModel): class Settings(BaseModel): + """The settings for the tenant. Includes branding and preference set.""" + branding: Optional[SettingsBranding] = None """The branding for the tenant.""" @@ -42,6 +46,8 @@ class Settings(BaseModel): class Tenant(BaseModel): + """A tenant entity.""" + id: str """The unique identifier for the tenant.""" diff --git a/src/knockapi/types/tenant_request_param.py b/src/knockapi/types/tenant_request_param.py index a9c7fb2..0949e57 100644 --- a/src/knockapi/types/tenant_request_param.py +++ b/src/knockapi/types/tenant_request_param.py @@ -13,6 +13,8 @@ class SettingsBranding(TypedDict, total=False): + """The branding for the tenant.""" + icon_url: Optional[str] """The icon URL for the tenant. @@ -33,6 +35,8 @@ class SettingsBranding(TypedDict, total=False): class Settings(TypedDict, total=False): + """The settings for the tenant. Includes branding and preference set.""" + branding: SettingsBranding """The branding for the tenant.""" @@ -41,6 +45,11 @@ class Settings(TypedDict, total=False): class TenantRequestParamTyped(TypedDict, total=False): + """A tenant to be set in the system. + + You can supply any additional properties on the tenant object. + """ + id: Required[str] """The unique identifier for the tenant.""" diff --git a/src/knockapi/types/tenant_set_params.py b/src/knockapi/types/tenant_set_params.py index 760794d..9f69494 100644 --- a/src/knockapi/types/tenant_set_params.py +++ b/src/knockapi/types/tenant_set_params.py @@ -30,6 +30,8 @@ class TenantSetParams(TypedDict, total=False): class SettingsBranding(TypedDict, total=False): + """The branding for the tenant.""" + icon_url: Optional[str] """The icon URL for the tenant. @@ -50,6 +52,8 @@ class SettingsBranding(TypedDict, total=False): class Settings(TypedDict, total=False): + """The settings for the tenant. Includes branding and preference set.""" + branding: SettingsBranding """The branding for the tenant.""" diff --git a/src/knockapi/types/user.py b/src/knockapi/types/user.py index c841676..11cf87c 100644 --- a/src/knockapi/types/user.py +++ b/src/knockapi/types/user.py @@ -11,6 +11,10 @@ class User(BaseModel): + """ + A [User](/concepts/users) represents an individual in your system who can receive notifications through Knock. Users are the most common recipients of notifications and are always referenced by your internal identifier. + """ + id: str """The unique identifier of the user.""" diff --git a/src/knockapi/types/user_set_preferences_params.py b/src/knockapi/types/user_set_preferences_params.py index 511b8a1..9ded001 100644 --- a/src/knockapi/types/user_set_preferences_params.py +++ b/src/knockapi/types/user_set_preferences_params.py @@ -59,6 +59,10 @@ class UserSetPreferencesParams(TypedDict, total=False): class CategoriesPreferenceSetWorkflowCategorySettingObject(TypedDict, total=False): + """ + The settings object for a workflow or category, where you can specify channel types or conditions. + """ + channel_types: Optional[PreferenceSetChannelTypesParam] """Channel type preferences.""" @@ -77,6 +81,10 @@ class CategoriesPreferenceSetWorkflowCategorySettingObject(TypedDict, total=Fals class WorkflowsPreferenceSetWorkflowCategorySettingObject(TypedDict, total=False): + """ + The settings object for a workflow or category, where you can specify channel types or conditions. + """ + channel_types: Optional[PreferenceSetChannelTypesParam] """Channel type preferences.""" diff --git a/src/knockapi/types/users/feed_get_settings_response.py b/src/knockapi/types/users/feed_get_settings_response.py index bcc30f1..c0798f2 100644 --- a/src/knockapi/types/users/feed_get_settings_response.py +++ b/src/knockapi/types/users/feed_get_settings_response.py @@ -6,10 +6,14 @@ class Features(BaseModel): + """Features configuration for the user's feed.""" + branding_required: bool """Whether branding is required for the user's feed.""" class FeedGetSettingsResponse(BaseModel): + """The response for the user's feed settings.""" + features: Features """Features configuration for the user's feed.""" diff --git a/src/knockapi/types/users/feed_list_items_response.py b/src/knockapi/types/users/feed_list_items_response.py index bb57719..eb46158 100644 --- a/src/knockapi/types/users/feed_list_items_response.py +++ b/src/knockapi/types/users/feed_list_items_response.py @@ -20,6 +20,8 @@ class BlockMessageInAppFeedContentBlock(BaseModel): + """A block in a message in an app feed.""" + content: str """The content of the block in a message in an app feed.""" @@ -34,6 +36,8 @@ class BlockMessageInAppFeedContentBlock(BaseModel): class BlockMessageInAppFeedButtonSetBlockButton(BaseModel): + """A button in an in app feed message.""" + action: str """The action to take when the button is clicked.""" @@ -45,6 +49,8 @@ class BlockMessageInAppFeedButtonSetBlockButton(BaseModel): class BlockMessageInAppFeedButtonSetBlock(BaseModel): + """A button set block in a message in an app feed.""" + buttons: List[BlockMessageInAppFeedButtonSetBlockButton] """A list of buttons in an in app feed message.""" @@ -59,6 +65,8 @@ class BlockMessageInAppFeedButtonSetBlock(BaseModel): class Source(BaseModel): + """Source information for the feed item.""" + api_typename: str = FieldInfo(alias="__typename") """The typename of the schema.""" @@ -73,6 +81,8 @@ class Source(BaseModel): class FeedListItemsResponse(BaseModel): + """An in-app feed message in a user's feed.""" + id: str """Unique identifier for the feed.""" diff --git a/src/knockapi/types/users/guide_get_channel_response.py b/src/knockapi/types/users/guide_get_channel_response.py index 7cdb296..d2c8653 100644 --- a/src/knockapi/types/users/guide_get_channel_response.py +++ b/src/knockapi/types/users/guide_get_channel_response.py @@ -124,6 +124,8 @@ class GuideGroup(BaseModel): class GuideGetChannelResponse(BaseModel): + """A response for a list of guides.""" + entries: List[Entry] """A list of guides.""" diff --git a/src/knockapi/types/users/guide_mark_message_as_archived_response.py b/src/knockapi/types/users/guide_mark_message_as_archived_response.py index 3d3bc4b..d6d2012 100644 --- a/src/knockapi/types/users/guide_mark_message_as_archived_response.py +++ b/src/knockapi/types/users/guide_mark_message_as_archived_response.py @@ -6,5 +6,7 @@ class GuideMarkMessageAsArchivedResponse(BaseModel): + """A response for a guide action.""" + status: str """The status of a guide's action.""" diff --git a/src/knockapi/types/users/guide_mark_message_as_interacted_response.py b/src/knockapi/types/users/guide_mark_message_as_interacted_response.py index 1b05083..c9cedac 100644 --- a/src/knockapi/types/users/guide_mark_message_as_interacted_response.py +++ b/src/knockapi/types/users/guide_mark_message_as_interacted_response.py @@ -6,5 +6,7 @@ class GuideMarkMessageAsInteractedResponse(BaseModel): + """A response for a guide action.""" + status: str """The status of a guide's action.""" diff --git a/src/knockapi/types/users/guide_mark_message_as_seen_response.py b/src/knockapi/types/users/guide_mark_message_as_seen_response.py index d2b3204..0f26093 100644 --- a/src/knockapi/types/users/guide_mark_message_as_seen_response.py +++ b/src/knockapi/types/users/guide_mark_message_as_seen_response.py @@ -6,5 +6,7 @@ class GuideMarkMessageAsSeenResponse(BaseModel): + """A response for a guide action.""" + status: str """The status of a guide's action.""" diff --git a/src/knockapi/types/workflow_trigger_response.py b/src/knockapi/types/workflow_trigger_response.py index a5fdcd6..4903df9 100644 --- a/src/knockapi/types/workflow_trigger_response.py +++ b/src/knockapi/types/workflow_trigger_response.py @@ -6,6 +6,8 @@ class WorkflowTriggerResponse(BaseModel): + """The response from triggering a workflow.""" + workflow_run_id: str """ This value allows you to track individual messages associated with this trigger From c8a206a627961a6ece03ff8d9aeb371d73e5be04 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:23:59 +0000 Subject: [PATCH 19/44] feat(api): api update --- .stats.yml | 4 ++-- src/knockapi/resources/tenants/bulk.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.stats.yml b/.stats.yml index faedbdb..2229265 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-b4f3f8094215f4027ce5c50b44b4ba289b96bfe071deba53497eac31f6bd44ec.yml -openapi_spec_hash: da078194fc3bbcae05af20ad6dee4adc +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-e4fa52740f0456b398ba70a2d1a75e2363a8ae73202aea5dfb5959d2cda0f941.yml +openapi_spec_hash: 625e7ae73ab57c5c100e39cc96f3ad0b config_hash: 2b42d138d85c524e65fa7e205d36cc4a diff --git a/src/knockapi/resources/tenants/bulk.py b/src/knockapi/resources/tenants/bulk.py index 9b510d6..e1c1087 100644 --- a/src/knockapi/resources/tenants/bulk.py +++ b/src/knockapi/resources/tenants/bulk.py @@ -54,10 +54,10 @@ def delete( timeout: float | httpx.Timeout | None | NotGiven = not_given, idempotency_key: str | None = None, ) -> BulkOperation: - """Delete up to 100 tenants at a time in a single operation. + """Delete up to 1,000 tenants at a time in a single operation. - This operation cannot - be undone. + This operation + cannot be undone. Args: tenant_ids: The IDs of the tenants to delete. @@ -98,7 +98,7 @@ def set( idempotency_key: str | None = None, ) -> BulkOperation: """ - Set or update up to 100 tenants in a single operation. + Set or update up to 1,000 tenants in a single operation. Args: tenants: The tenants to be upserted. @@ -159,10 +159,10 @@ async def delete( timeout: float | httpx.Timeout | None | NotGiven = not_given, idempotency_key: str | None = None, ) -> BulkOperation: - """Delete up to 100 tenants at a time in a single operation. + """Delete up to 1,000 tenants at a time in a single operation. - This operation cannot - be undone. + This operation + cannot be undone. Args: tenant_ids: The IDs of the tenants to delete. @@ -203,7 +203,7 @@ async def set( idempotency_key: str | None = None, ) -> BulkOperation: """ - Set or update up to 100 tenants in a single operation. + Set or update up to 1,000 tenants in a single operation. Args: tenants: The tenants to be upserted. From 66931f970f1e98fb2152194ba9834e914538d109 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 18:20:39 +0000 Subject: [PATCH 20/44] feat(api): api update --- .stats.yml | 4 ++-- src/knockapi/resources/tenants/bulk.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2229265..faedbdb 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-e4fa52740f0456b398ba70a2d1a75e2363a8ae73202aea5dfb5959d2cda0f941.yml -openapi_spec_hash: 625e7ae73ab57c5c100e39cc96f3ad0b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-b4f3f8094215f4027ce5c50b44b4ba289b96bfe071deba53497eac31f6bd44ec.yml +openapi_spec_hash: da078194fc3bbcae05af20ad6dee4adc config_hash: 2b42d138d85c524e65fa7e205d36cc4a diff --git a/src/knockapi/resources/tenants/bulk.py b/src/knockapi/resources/tenants/bulk.py index e1c1087..9b510d6 100644 --- a/src/knockapi/resources/tenants/bulk.py +++ b/src/knockapi/resources/tenants/bulk.py @@ -54,10 +54,10 @@ def delete( timeout: float | httpx.Timeout | None | NotGiven = not_given, idempotency_key: str | None = None, ) -> BulkOperation: - """Delete up to 1,000 tenants at a time in a single operation. + """Delete up to 100 tenants at a time in a single operation. - This operation - cannot be undone. + This operation cannot + be undone. Args: tenant_ids: The IDs of the tenants to delete. @@ -98,7 +98,7 @@ def set( idempotency_key: str | None = None, ) -> BulkOperation: """ - Set or update up to 1,000 tenants in a single operation. + Set or update up to 100 tenants in a single operation. Args: tenants: The tenants to be upserted. @@ -159,10 +159,10 @@ async def delete( timeout: float | httpx.Timeout | None | NotGiven = not_given, idempotency_key: str | None = None, ) -> BulkOperation: - """Delete up to 1,000 tenants at a time in a single operation. + """Delete up to 100 tenants at a time in a single operation. - This operation - cannot be undone. + This operation cannot + be undone. Args: tenant_ids: The IDs of the tenants to delete. @@ -203,7 +203,7 @@ async def set( idempotency_key: str | None = None, ) -> BulkOperation: """ - Set or update up to 1,000 tenants in a single operation. + Set or update up to 100 tenants in a single operation. Args: tenants: The tenants to be upserted. From 79859d6ca10acd18814ba447bf59d8289f361e74 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 9 Dec 2025 21:20:35 +0000 Subject: [PATCH 21/44] feat(api): api update --- .stats.yml | 4 ++-- src/knockapi/resources/tenants/bulk.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.stats.yml b/.stats.yml index faedbdb..2229265 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-b4f3f8094215f4027ce5c50b44b4ba289b96bfe071deba53497eac31f6bd44ec.yml -openapi_spec_hash: da078194fc3bbcae05af20ad6dee4adc +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-e4fa52740f0456b398ba70a2d1a75e2363a8ae73202aea5dfb5959d2cda0f941.yml +openapi_spec_hash: 625e7ae73ab57c5c100e39cc96f3ad0b config_hash: 2b42d138d85c524e65fa7e205d36cc4a diff --git a/src/knockapi/resources/tenants/bulk.py b/src/knockapi/resources/tenants/bulk.py index 9b510d6..e1c1087 100644 --- a/src/knockapi/resources/tenants/bulk.py +++ b/src/knockapi/resources/tenants/bulk.py @@ -54,10 +54,10 @@ def delete( timeout: float | httpx.Timeout | None | NotGiven = not_given, idempotency_key: str | None = None, ) -> BulkOperation: - """Delete up to 100 tenants at a time in a single operation. + """Delete up to 1,000 tenants at a time in a single operation. - This operation cannot - be undone. + This operation + cannot be undone. Args: tenant_ids: The IDs of the tenants to delete. @@ -98,7 +98,7 @@ def set( idempotency_key: str | None = None, ) -> BulkOperation: """ - Set or update up to 100 tenants in a single operation. + Set or update up to 1,000 tenants in a single operation. Args: tenants: The tenants to be upserted. @@ -159,10 +159,10 @@ async def delete( timeout: float | httpx.Timeout | None | NotGiven = not_given, idempotency_key: str | None = None, ) -> BulkOperation: - """Delete up to 100 tenants at a time in a single operation. + """Delete up to 1,000 tenants at a time in a single operation. - This operation cannot - be undone. + This operation + cannot be undone. Args: tenant_ids: The IDs of the tenants to delete. @@ -203,7 +203,7 @@ async def set( idempotency_key: str | None = None, ) -> BulkOperation: """ - Set or update up to 100 tenants in a single operation. + Set or update up to 1,000 tenants in a single operation. Args: tenants: The tenants to be upserted. From 8737e4a0cd84c85d0887b2e0ce5d9acd034abe87 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:23:23 +0000 Subject: [PATCH 22/44] chore(internal): add missing files argument to base client --- src/knockapi/_base_client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/knockapi/_base_client.py b/src/knockapi/_base_client.py index 07161fc..0d50ef9 100644 --- a/src/knockapi/_base_client.py +++ b/src/knockapi/_base_client.py @@ -1247,9 +1247,12 @@ def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return self.request(cast_to, opts) def put( @@ -1767,9 +1770,12 @@ async def patch( *, cast_to: Type[ResponseT], body: Body | None = None, + files: RequestFiles | None = None, options: RequestOptions = {}, ) -> ResponseT: - opts = FinalRequestOptions.construct(method="patch", url=path, json_data=body, **options) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + ) return await self.request(cast_to, opts) async def put( From f5c9aa6dd753849ac29c12ca41e1723ac77d1892 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:23:27 +0000 Subject: [PATCH 23/44] chore: speedup initial import --- src/knockapi/_client.py | 558 ++++++++++++++++++++++++++++++++-------- 1 file changed, 450 insertions(+), 108 deletions(-) diff --git a/src/knockapi/_client.py b/src/knockapi/_client.py index 8f49844..c7bbfdc 100644 --- a/src/knockapi/_client.py +++ b/src/knockapi/_client.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from typing import Any, Mapping +from typing import TYPE_CHECKING, Any, Mapping from typing_extensions import Self, override import httpx @@ -20,8 +20,8 @@ not_given, ) from ._utils import is_given, get_async_library +from ._compat import cached_property from ._version import __version__ -from .resources import audiences, workflows, bulk_operations from ._streaming import Stream as Stream, AsyncStream as AsyncStream from ._exceptions import KnockError, APIStatusError from ._base_client import ( @@ -29,33 +29,37 @@ SyncAPIClient, AsyncAPIClient, ) -from .resources.users import users -from .resources.objects import objects -from .resources.tenants import tenants -from .resources.channels import channels -from .resources.messages import messages -from .resources.providers import providers -from .resources.schedules import schedules -from .resources.integrations import integrations + +if TYPE_CHECKING: + from .resources import ( + users, + objects, + tenants, + channels, + messages, + audiences, + providers, + schedules, + workflows, + integrations, + bulk_operations, + ) + from .resources.audiences import AudiencesResource, AsyncAudiencesResource + from .resources.workflows import WorkflowsResource, AsyncWorkflowsResource + from .resources.users.users import UsersResource, AsyncUsersResource + from .resources.bulk_operations import BulkOperationsResource, AsyncBulkOperationsResource + from .resources.objects.objects import ObjectsResource, AsyncObjectsResource + from .resources.tenants.tenants import TenantsResource, AsyncTenantsResource + from .resources.channels.channels import ChannelsResource, AsyncChannelsResource + from .resources.messages.messages import MessagesResource, AsyncMessagesResource + from .resources.providers.providers import ProvidersResource, AsyncProvidersResource + from .resources.schedules.schedules import SchedulesResource, AsyncSchedulesResource + from .resources.integrations.integrations import IntegrationsResource, AsyncIntegrationsResource __all__ = ["Timeout", "Transport", "ProxiesTypes", "RequestOptions", "Knock", "AsyncKnock", "Client", "AsyncClient"] class Knock(SyncAPIClient): - users: users.UsersResource - objects: objects.ObjectsResource - tenants: tenants.TenantsResource - bulk_operations: bulk_operations.BulkOperationsResource - messages: messages.MessagesResource - providers: providers.ProvidersResource - integrations: integrations.IntegrationsResource - workflows: workflows.WorkflowsResource - schedules: schedules.SchedulesResource - channels: channels.ChannelsResource - audiences: audiences.AudiencesResource - with_raw_response: KnockWithRawResponse - with_streaming_response: KnockWithStreamedResponse - # client options api_key: str branch: str | None @@ -120,19 +124,79 @@ def __init__( self._idempotency_header = "Idempotency-Key" - self.users = users.UsersResource(self) - self.objects = objects.ObjectsResource(self) - self.tenants = tenants.TenantsResource(self) - self.bulk_operations = bulk_operations.BulkOperationsResource(self) - self.messages = messages.MessagesResource(self) - self.providers = providers.ProvidersResource(self) - self.integrations = integrations.IntegrationsResource(self) - self.workflows = workflows.WorkflowsResource(self) - self.schedules = schedules.SchedulesResource(self) - self.channels = channels.ChannelsResource(self) - self.audiences = audiences.AudiencesResource(self) - self.with_raw_response = KnockWithRawResponse(self) - self.with_streaming_response = KnockWithStreamedResponse(self) + @cached_property + def users(self) -> UsersResource: + from .resources.users import UsersResource + + return UsersResource(self) + + @cached_property + def objects(self) -> ObjectsResource: + from .resources.objects import ObjectsResource + + return ObjectsResource(self) + + @cached_property + def tenants(self) -> TenantsResource: + from .resources.tenants import TenantsResource + + return TenantsResource(self) + + @cached_property + def bulk_operations(self) -> BulkOperationsResource: + from .resources.bulk_operations import BulkOperationsResource + + return BulkOperationsResource(self) + + @cached_property + def messages(self) -> MessagesResource: + from .resources.messages import MessagesResource + + return MessagesResource(self) + + @cached_property + def providers(self) -> ProvidersResource: + from .resources.providers import ProvidersResource + + return ProvidersResource(self) + + @cached_property + def integrations(self) -> IntegrationsResource: + from .resources.integrations import IntegrationsResource + + return IntegrationsResource(self) + + @cached_property + def workflows(self) -> WorkflowsResource: + from .resources.workflows import WorkflowsResource + + return WorkflowsResource(self) + + @cached_property + def schedules(self) -> SchedulesResource: + from .resources.schedules import SchedulesResource + + return SchedulesResource(self) + + @cached_property + def channels(self) -> ChannelsResource: + from .resources.channels import ChannelsResource + + return ChannelsResource(self) + + @cached_property + def audiences(self) -> AudiencesResource: + from .resources.audiences import AudiencesResource + + return AudiencesResource(self) + + @cached_property + def with_raw_response(self) -> KnockWithRawResponse: + return KnockWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> KnockWithStreamedResponse: + return KnockWithStreamedResponse(self) @property @override @@ -243,20 +307,6 @@ def _make_status_error( class AsyncKnock(AsyncAPIClient): - users: users.AsyncUsersResource - objects: objects.AsyncObjectsResource - tenants: tenants.AsyncTenantsResource - bulk_operations: bulk_operations.AsyncBulkOperationsResource - messages: messages.AsyncMessagesResource - providers: providers.AsyncProvidersResource - integrations: integrations.AsyncIntegrationsResource - workflows: workflows.AsyncWorkflowsResource - schedules: schedules.AsyncSchedulesResource - channels: channels.AsyncChannelsResource - audiences: audiences.AsyncAudiencesResource - with_raw_response: AsyncKnockWithRawResponse - with_streaming_response: AsyncKnockWithStreamedResponse - # client options api_key: str branch: str | None @@ -321,19 +371,79 @@ def __init__( self._idempotency_header = "Idempotency-Key" - self.users = users.AsyncUsersResource(self) - self.objects = objects.AsyncObjectsResource(self) - self.tenants = tenants.AsyncTenantsResource(self) - self.bulk_operations = bulk_operations.AsyncBulkOperationsResource(self) - self.messages = messages.AsyncMessagesResource(self) - self.providers = providers.AsyncProvidersResource(self) - self.integrations = integrations.AsyncIntegrationsResource(self) - self.workflows = workflows.AsyncWorkflowsResource(self) - self.schedules = schedules.AsyncSchedulesResource(self) - self.channels = channels.AsyncChannelsResource(self) - self.audiences = audiences.AsyncAudiencesResource(self) - self.with_raw_response = AsyncKnockWithRawResponse(self) - self.with_streaming_response = AsyncKnockWithStreamedResponse(self) + @cached_property + def users(self) -> AsyncUsersResource: + from .resources.users import AsyncUsersResource + + return AsyncUsersResource(self) + + @cached_property + def objects(self) -> AsyncObjectsResource: + from .resources.objects import AsyncObjectsResource + + return AsyncObjectsResource(self) + + @cached_property + def tenants(self) -> AsyncTenantsResource: + from .resources.tenants import AsyncTenantsResource + + return AsyncTenantsResource(self) + + @cached_property + def bulk_operations(self) -> AsyncBulkOperationsResource: + from .resources.bulk_operations import AsyncBulkOperationsResource + + return AsyncBulkOperationsResource(self) + + @cached_property + def messages(self) -> AsyncMessagesResource: + from .resources.messages import AsyncMessagesResource + + return AsyncMessagesResource(self) + + @cached_property + def providers(self) -> AsyncProvidersResource: + from .resources.providers import AsyncProvidersResource + + return AsyncProvidersResource(self) + + @cached_property + def integrations(self) -> AsyncIntegrationsResource: + from .resources.integrations import AsyncIntegrationsResource + + return AsyncIntegrationsResource(self) + + @cached_property + def workflows(self) -> AsyncWorkflowsResource: + from .resources.workflows import AsyncWorkflowsResource + + return AsyncWorkflowsResource(self) + + @cached_property + def schedules(self) -> AsyncSchedulesResource: + from .resources.schedules import AsyncSchedulesResource + + return AsyncSchedulesResource(self) + + @cached_property + def channels(self) -> AsyncChannelsResource: + from .resources.channels import AsyncChannelsResource + + return AsyncChannelsResource(self) + + @cached_property + def audiences(self) -> AsyncAudiencesResource: + from .resources.audiences import AsyncAudiencesResource + + return AsyncAudiencesResource(self) + + @cached_property + def with_raw_response(self) -> AsyncKnockWithRawResponse: + return AsyncKnockWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncKnockWithStreamedResponse: + return AsyncKnockWithStreamedResponse(self) @property @override @@ -444,63 +554,295 @@ def _make_status_error( class KnockWithRawResponse: + _client: Knock + def __init__(self, client: Knock) -> None: - self.users = users.UsersResourceWithRawResponse(client.users) - self.objects = objects.ObjectsResourceWithRawResponse(client.objects) - self.tenants = tenants.TenantsResourceWithRawResponse(client.tenants) - self.bulk_operations = bulk_operations.BulkOperationsResourceWithRawResponse(client.bulk_operations) - self.messages = messages.MessagesResourceWithRawResponse(client.messages) - self.providers = providers.ProvidersResourceWithRawResponse(client.providers) - self.integrations = integrations.IntegrationsResourceWithRawResponse(client.integrations) - self.workflows = workflows.WorkflowsResourceWithRawResponse(client.workflows) - self.schedules = schedules.SchedulesResourceWithRawResponse(client.schedules) - self.channels = channels.ChannelsResourceWithRawResponse(client.channels) - self.audiences = audiences.AudiencesResourceWithRawResponse(client.audiences) + self._client = client + + @cached_property + def users(self) -> users.UsersResourceWithRawResponse: + from .resources.users import UsersResourceWithRawResponse + + return UsersResourceWithRawResponse(self._client.users) + + @cached_property + def objects(self) -> objects.ObjectsResourceWithRawResponse: + from .resources.objects import ObjectsResourceWithRawResponse + + return ObjectsResourceWithRawResponse(self._client.objects) + + @cached_property + def tenants(self) -> tenants.TenantsResourceWithRawResponse: + from .resources.tenants import TenantsResourceWithRawResponse + + return TenantsResourceWithRawResponse(self._client.tenants) + + @cached_property + def bulk_operations(self) -> bulk_operations.BulkOperationsResourceWithRawResponse: + from .resources.bulk_operations import BulkOperationsResourceWithRawResponse + + return BulkOperationsResourceWithRawResponse(self._client.bulk_operations) + + @cached_property + def messages(self) -> messages.MessagesResourceWithRawResponse: + from .resources.messages import MessagesResourceWithRawResponse + + return MessagesResourceWithRawResponse(self._client.messages) + + @cached_property + def providers(self) -> providers.ProvidersResourceWithRawResponse: + from .resources.providers import ProvidersResourceWithRawResponse + + return ProvidersResourceWithRawResponse(self._client.providers) + + @cached_property + def integrations(self) -> integrations.IntegrationsResourceWithRawResponse: + from .resources.integrations import IntegrationsResourceWithRawResponse + + return IntegrationsResourceWithRawResponse(self._client.integrations) + + @cached_property + def workflows(self) -> workflows.WorkflowsResourceWithRawResponse: + from .resources.workflows import WorkflowsResourceWithRawResponse + + return WorkflowsResourceWithRawResponse(self._client.workflows) + + @cached_property + def schedules(self) -> schedules.SchedulesResourceWithRawResponse: + from .resources.schedules import SchedulesResourceWithRawResponse + + return SchedulesResourceWithRawResponse(self._client.schedules) + + @cached_property + def channels(self) -> channels.ChannelsResourceWithRawResponse: + from .resources.channels import ChannelsResourceWithRawResponse + + return ChannelsResourceWithRawResponse(self._client.channels) + + @cached_property + def audiences(self) -> audiences.AudiencesResourceWithRawResponse: + from .resources.audiences import AudiencesResourceWithRawResponse + + return AudiencesResourceWithRawResponse(self._client.audiences) class AsyncKnockWithRawResponse: + _client: AsyncKnock + def __init__(self, client: AsyncKnock) -> None: - self.users = users.AsyncUsersResourceWithRawResponse(client.users) - self.objects = objects.AsyncObjectsResourceWithRawResponse(client.objects) - self.tenants = tenants.AsyncTenantsResourceWithRawResponse(client.tenants) - self.bulk_operations = bulk_operations.AsyncBulkOperationsResourceWithRawResponse(client.bulk_operations) - self.messages = messages.AsyncMessagesResourceWithRawResponse(client.messages) - self.providers = providers.AsyncProvidersResourceWithRawResponse(client.providers) - self.integrations = integrations.AsyncIntegrationsResourceWithRawResponse(client.integrations) - self.workflows = workflows.AsyncWorkflowsResourceWithRawResponse(client.workflows) - self.schedules = schedules.AsyncSchedulesResourceWithRawResponse(client.schedules) - self.channels = channels.AsyncChannelsResourceWithRawResponse(client.channels) - self.audiences = audiences.AsyncAudiencesResourceWithRawResponse(client.audiences) + self._client = client + + @cached_property + def users(self) -> users.AsyncUsersResourceWithRawResponse: + from .resources.users import AsyncUsersResourceWithRawResponse + + return AsyncUsersResourceWithRawResponse(self._client.users) + + @cached_property + def objects(self) -> objects.AsyncObjectsResourceWithRawResponse: + from .resources.objects import AsyncObjectsResourceWithRawResponse + + return AsyncObjectsResourceWithRawResponse(self._client.objects) + + @cached_property + def tenants(self) -> tenants.AsyncTenantsResourceWithRawResponse: + from .resources.tenants import AsyncTenantsResourceWithRawResponse + + return AsyncTenantsResourceWithRawResponse(self._client.tenants) + + @cached_property + def bulk_operations(self) -> bulk_operations.AsyncBulkOperationsResourceWithRawResponse: + from .resources.bulk_operations import AsyncBulkOperationsResourceWithRawResponse + + return AsyncBulkOperationsResourceWithRawResponse(self._client.bulk_operations) + + @cached_property + def messages(self) -> messages.AsyncMessagesResourceWithRawResponse: + from .resources.messages import AsyncMessagesResourceWithRawResponse + + return AsyncMessagesResourceWithRawResponse(self._client.messages) + + @cached_property + def providers(self) -> providers.AsyncProvidersResourceWithRawResponse: + from .resources.providers import AsyncProvidersResourceWithRawResponse + + return AsyncProvidersResourceWithRawResponse(self._client.providers) + + @cached_property + def integrations(self) -> integrations.AsyncIntegrationsResourceWithRawResponse: + from .resources.integrations import AsyncIntegrationsResourceWithRawResponse + + return AsyncIntegrationsResourceWithRawResponse(self._client.integrations) + + @cached_property + def workflows(self) -> workflows.AsyncWorkflowsResourceWithRawResponse: + from .resources.workflows import AsyncWorkflowsResourceWithRawResponse + + return AsyncWorkflowsResourceWithRawResponse(self._client.workflows) + + @cached_property + def schedules(self) -> schedules.AsyncSchedulesResourceWithRawResponse: + from .resources.schedules import AsyncSchedulesResourceWithRawResponse + + return AsyncSchedulesResourceWithRawResponse(self._client.schedules) + + @cached_property + def channels(self) -> channels.AsyncChannelsResourceWithRawResponse: + from .resources.channels import AsyncChannelsResourceWithRawResponse + + return AsyncChannelsResourceWithRawResponse(self._client.channels) + + @cached_property + def audiences(self) -> audiences.AsyncAudiencesResourceWithRawResponse: + from .resources.audiences import AsyncAudiencesResourceWithRawResponse + + return AsyncAudiencesResourceWithRawResponse(self._client.audiences) class KnockWithStreamedResponse: + _client: Knock + def __init__(self, client: Knock) -> None: - self.users = users.UsersResourceWithStreamingResponse(client.users) - self.objects = objects.ObjectsResourceWithStreamingResponse(client.objects) - self.tenants = tenants.TenantsResourceWithStreamingResponse(client.tenants) - self.bulk_operations = bulk_operations.BulkOperationsResourceWithStreamingResponse(client.bulk_operations) - self.messages = messages.MessagesResourceWithStreamingResponse(client.messages) - self.providers = providers.ProvidersResourceWithStreamingResponse(client.providers) - self.integrations = integrations.IntegrationsResourceWithStreamingResponse(client.integrations) - self.workflows = workflows.WorkflowsResourceWithStreamingResponse(client.workflows) - self.schedules = schedules.SchedulesResourceWithStreamingResponse(client.schedules) - self.channels = channels.ChannelsResourceWithStreamingResponse(client.channels) - self.audiences = audiences.AudiencesResourceWithStreamingResponse(client.audiences) + self._client = client + + @cached_property + def users(self) -> users.UsersResourceWithStreamingResponse: + from .resources.users import UsersResourceWithStreamingResponse + + return UsersResourceWithStreamingResponse(self._client.users) + + @cached_property + def objects(self) -> objects.ObjectsResourceWithStreamingResponse: + from .resources.objects import ObjectsResourceWithStreamingResponse + + return ObjectsResourceWithStreamingResponse(self._client.objects) + + @cached_property + def tenants(self) -> tenants.TenantsResourceWithStreamingResponse: + from .resources.tenants import TenantsResourceWithStreamingResponse + + return TenantsResourceWithStreamingResponse(self._client.tenants) + + @cached_property + def bulk_operations(self) -> bulk_operations.BulkOperationsResourceWithStreamingResponse: + from .resources.bulk_operations import BulkOperationsResourceWithStreamingResponse + + return BulkOperationsResourceWithStreamingResponse(self._client.bulk_operations) + + @cached_property + def messages(self) -> messages.MessagesResourceWithStreamingResponse: + from .resources.messages import MessagesResourceWithStreamingResponse + + return MessagesResourceWithStreamingResponse(self._client.messages) + + @cached_property + def providers(self) -> providers.ProvidersResourceWithStreamingResponse: + from .resources.providers import ProvidersResourceWithStreamingResponse + + return ProvidersResourceWithStreamingResponse(self._client.providers) + + @cached_property + def integrations(self) -> integrations.IntegrationsResourceWithStreamingResponse: + from .resources.integrations import IntegrationsResourceWithStreamingResponse + + return IntegrationsResourceWithStreamingResponse(self._client.integrations) + + @cached_property + def workflows(self) -> workflows.WorkflowsResourceWithStreamingResponse: + from .resources.workflows import WorkflowsResourceWithStreamingResponse + + return WorkflowsResourceWithStreamingResponse(self._client.workflows) + + @cached_property + def schedules(self) -> schedules.SchedulesResourceWithStreamingResponse: + from .resources.schedules import SchedulesResourceWithStreamingResponse + + return SchedulesResourceWithStreamingResponse(self._client.schedules) + + @cached_property + def channels(self) -> channels.ChannelsResourceWithStreamingResponse: + from .resources.channels import ChannelsResourceWithStreamingResponse + + return ChannelsResourceWithStreamingResponse(self._client.channels) + + @cached_property + def audiences(self) -> audiences.AudiencesResourceWithStreamingResponse: + from .resources.audiences import AudiencesResourceWithStreamingResponse + + return AudiencesResourceWithStreamingResponse(self._client.audiences) class AsyncKnockWithStreamedResponse: + _client: AsyncKnock + def __init__(self, client: AsyncKnock) -> None: - self.users = users.AsyncUsersResourceWithStreamingResponse(client.users) - self.objects = objects.AsyncObjectsResourceWithStreamingResponse(client.objects) - self.tenants = tenants.AsyncTenantsResourceWithStreamingResponse(client.tenants) - self.bulk_operations = bulk_operations.AsyncBulkOperationsResourceWithStreamingResponse(client.bulk_operations) - self.messages = messages.AsyncMessagesResourceWithStreamingResponse(client.messages) - self.providers = providers.AsyncProvidersResourceWithStreamingResponse(client.providers) - self.integrations = integrations.AsyncIntegrationsResourceWithStreamingResponse(client.integrations) - self.workflows = workflows.AsyncWorkflowsResourceWithStreamingResponse(client.workflows) - self.schedules = schedules.AsyncSchedulesResourceWithStreamingResponse(client.schedules) - self.channels = channels.AsyncChannelsResourceWithStreamingResponse(client.channels) - self.audiences = audiences.AsyncAudiencesResourceWithStreamingResponse(client.audiences) + self._client = client + + @cached_property + def users(self) -> users.AsyncUsersResourceWithStreamingResponse: + from .resources.users import AsyncUsersResourceWithStreamingResponse + + return AsyncUsersResourceWithStreamingResponse(self._client.users) + + @cached_property + def objects(self) -> objects.AsyncObjectsResourceWithStreamingResponse: + from .resources.objects import AsyncObjectsResourceWithStreamingResponse + + return AsyncObjectsResourceWithStreamingResponse(self._client.objects) + + @cached_property + def tenants(self) -> tenants.AsyncTenantsResourceWithStreamingResponse: + from .resources.tenants import AsyncTenantsResourceWithStreamingResponse + + return AsyncTenantsResourceWithStreamingResponse(self._client.tenants) + + @cached_property + def bulk_operations(self) -> bulk_operations.AsyncBulkOperationsResourceWithStreamingResponse: + from .resources.bulk_operations import AsyncBulkOperationsResourceWithStreamingResponse + + return AsyncBulkOperationsResourceWithStreamingResponse(self._client.bulk_operations) + + @cached_property + def messages(self) -> messages.AsyncMessagesResourceWithStreamingResponse: + from .resources.messages import AsyncMessagesResourceWithStreamingResponse + + return AsyncMessagesResourceWithStreamingResponse(self._client.messages) + + @cached_property + def providers(self) -> providers.AsyncProvidersResourceWithStreamingResponse: + from .resources.providers import AsyncProvidersResourceWithStreamingResponse + + return AsyncProvidersResourceWithStreamingResponse(self._client.providers) + + @cached_property + def integrations(self) -> integrations.AsyncIntegrationsResourceWithStreamingResponse: + from .resources.integrations import AsyncIntegrationsResourceWithStreamingResponse + + return AsyncIntegrationsResourceWithStreamingResponse(self._client.integrations) + + @cached_property + def workflows(self) -> workflows.AsyncWorkflowsResourceWithStreamingResponse: + from .resources.workflows import AsyncWorkflowsResourceWithStreamingResponse + + return AsyncWorkflowsResourceWithStreamingResponse(self._client.workflows) + + @cached_property + def schedules(self) -> schedules.AsyncSchedulesResourceWithStreamingResponse: + from .resources.schedules import AsyncSchedulesResourceWithStreamingResponse + + return AsyncSchedulesResourceWithStreamingResponse(self._client.schedules) + + @cached_property + def channels(self) -> channels.AsyncChannelsResourceWithStreamingResponse: + from .resources.channels import AsyncChannelsResourceWithStreamingResponse + + return AsyncChannelsResourceWithStreamingResponse(self._client.channels) + + @cached_property + def audiences(self) -> audiences.AsyncAudiencesResourceWithStreamingResponse: + from .resources.audiences import AsyncAudiencesResourceWithStreamingResponse + + return AsyncAudiencesResourceWithStreamingResponse(self._client.audiences) Client = Knock From 47196d100147ab585fdb4fb3e7e05b92b953c2ec Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 15:50:41 +0000 Subject: [PATCH 24/44] fix: use async_to_httpx_files in patch method --- src/knockapi/_base_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/knockapi/_base_client.py b/src/knockapi/_base_client.py index 0d50ef9..684994b 100644 --- a/src/knockapi/_base_client.py +++ b/src/knockapi/_base_client.py @@ -1774,7 +1774,7 @@ async def patch( options: RequestOptions = {}, ) -> ResponseT: opts = FinalRequestOptions.construct( - method="patch", url=path, json_data=body, files=to_httpx_files(files), **options + method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options ) return await self.request(cast_to, opts) From be20a97526ee33e626576d27f817f186218facb4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 21:55:21 +0000 Subject: [PATCH 25/44] chore(internal): add `--fix` argument to lint script --- scripts/lint | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/lint b/scripts/lint index 4e1035b..dc1ec6f 100755 --- a/scripts/lint +++ b/scripts/lint @@ -4,8 +4,13 @@ set -e cd "$(dirname "$0")/.." -echo "==> Running lints" -rye run lint +if [ "$1" = "--fix" ]; then + echo "==> Running lints with --fix" + rye run fix:ruff +else + echo "==> Running lints" + rye run lint +fi echo "==> Making sure it imports" rye run python -c 'import knockapi' From fb3e366f5c969b8c9956faeccab88f0882d6a263 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 04:08:49 +0000 Subject: [PATCH 26/44] chore(internal): codegen related update --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 07c8d38..ae52933 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2025 Knock + Copyright 2026 Knock Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From a0ff5ec2d3132a0289449df2d5d2995488f79d42 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 18:32:46 +0000 Subject: [PATCH 27/44] feat(client): add support for binary request streaming --- src/knockapi/_base_client.py | 145 ++++++++++++++++++++++++--- src/knockapi/_models.py | 17 +++- src/knockapi/_types.py | 9 ++ tests/test_client.py | 187 ++++++++++++++++++++++++++++++++++- 4 files changed, 344 insertions(+), 14 deletions(-) diff --git a/src/knockapi/_base_client.py b/src/knockapi/_base_client.py index 684994b..ada6cca 100644 --- a/src/knockapi/_base_client.py +++ b/src/knockapi/_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,9 +52,11 @@ ResponseT, AnyMapping, PostParser, + BinaryTypes, RequestFiles, HttpxSendArgs, RequestOptions, + AsyncBinaryTypes, HttpxRequestFiles, ModelBuilderProtocol, not_given, @@ -477,8 +480,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 @@ -532,7 +546,13 @@ 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 @@ -1194,6 +1214,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 +1227,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 +1241,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 +1254,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 +1282,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 +1308,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 +1334,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 +1786,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 +1799,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 +1813,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 +1826,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 +1854,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 +1885,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 +1911,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( diff --git a/src/knockapi/_models.py b/src/knockapi/_models.py index ca9500b..29070e0 100644 --- a/src/knockapi/_models.py +++ b/src/knockapi/_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, @@ -787,6 +800,7 @@ 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 @@ -805,6 +819,7 @@ class FinalRequestOptions(pydantic.BaseModel): post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() follow_redirects: Union[bool, None] = None + 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/knockapi/_types.py b/src/knockapi/_types.py index 9d73533..8059ac4 100644 --- a/src/knockapi/_types.py +++ b/src/knockapi/_types.py @@ -13,9 +13,11 @@ Mapping, TypeVar, Callable, + Iterable, Iterator, Optional, Sequence, + AsyncIterable, ) from typing_extensions import ( Set, @@ -56,6 +58,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, diff --git a/tests/test_client.py b/tests/test_client.py index 771e511..495ff74 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: Knock | AsyncKnock) -> int: transport = client._client._transport assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) @@ -500,6 +553,70 @@ def test_multipart_repeating_array(self, client: Knock) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload(self, respx_mock: MockRouter, client: Knock) -> 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 Knock( + 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: Knock) -> 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: Knock) -> None: class Model1(BaseModel): @@ -1348,6 +1465,72 @@ def test_multipart_repeating_array(self, async_client: AsyncKnock) -> None: b"", ] + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncKnock) -> 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 AsyncKnock( + 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: AsyncKnock + ) -> 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: AsyncKnock) -> None: class Model1(BaseModel): From 7f5b0fc648526fe857fc615a3f583ce9b4c76697 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 22:43:47 +0000 Subject: [PATCH 28/44] feat(api): api update --- .stats.yml | 4 ++-- .../types/users/guide_get_channel_response.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2229265..55110a2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-e4fa52740f0456b398ba70a2d1a75e2363a8ae73202aea5dfb5959d2cda0f941.yml -openapi_spec_hash: 625e7ae73ab57c5c100e39cc96f3ad0b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-138601849c510c1e1c96af745bc2a4c4099a6e69054b454ba69e61082fb60f31.yml +openapi_spec_hash: 4858bf3005cfe4a73eaa5cdc8a4ac939 config_hash: 2b42d138d85c524e65fa7e205d36cc4a diff --git a/src/knockapi/types/users/guide_get_channel_response.py b/src/knockapi/types/users/guide_get_channel_response.py index d2c8653..b0ce43c 100644 --- a/src/knockapi/types/users/guide_get_channel_response.py +++ b/src/knockapi/types/users/guide_get_channel_response.py @@ -2,6 +2,7 @@ from typing import Dict, List, Optional from datetime import datetime +from typing_extensions import Literal from pydantic import Field as FieldInfo @@ -15,6 +16,7 @@ "EntryStep", "EntryStepMessage", "GuideGroup", + "IneligibleGuide", ] @@ -123,6 +125,17 @@ class GuideGroup(BaseModel): updated_at: Optional[datetime] = None +class IneligibleGuide(BaseModel): + key: str + """The guide's key identifier""" + + message: str + """Human-readable explanation of ineligibility""" + + reason: Literal["guide_not_active", "marked_as_archived", "target_conditions_not_met", "not_in_target_audience"] + """Reason code for ineligibility""" + + class GuideGetChannelResponse(BaseModel): """A response for a list of guides.""" @@ -134,3 +147,6 @@ class GuideGetChannelResponse(BaseModel): guide_groups: List[GuideGroup] """A list of guide groups with their display sequences and intervals.""" + + ineligible_guides: List[IneligibleGuide] + """Markers for guides the user is not eligible to see.""" From ef96980eff0e3a1d600df5e777cfdeabb486ce32 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 18:29:06 +0000 Subject: [PATCH 29/44] chore(internal): update `actions/checkout` version --- .github/workflows/ci.yml | 6 +++--- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87d9206..e2e3b35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/knock-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: | @@ -44,7 +44,7 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/knock-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | @@ -81,7 +81,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/knock-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: | diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 03546d7..02565d3 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Rye run: | diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index e76d44c..7c384aa 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'knocklabs/knock-python' && (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: | From b85a63858e283b9b898abd766a40eda69cc96889 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 21:18:20 +0000 Subject: [PATCH 30/44] feat(api): api update --- .stats.yml | 4 ++-- src/knockapi/types/shared/condition.py | 2 +- src/knockapi/types/shared_params/condition.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index 55110a2..8f8211f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-138601849c510c1e1c96af745bc2a4c4099a6e69054b454ba69e61082fb60f31.yml -openapi_spec_hash: 4858bf3005cfe4a73eaa5cdc8a4ac939 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-0c621c06484c8d9a82e2cc70d2984dce46fa82f86185bce773c0bc265df77139.yml +openapi_spec_hash: 8670b393e28985dc4e93a16597690025 config_hash: 2b42d138d85c524e65fa7e205d36cc4a diff --git a/src/knockapi/types/shared/condition.py b/src/knockapi/types/shared/condition.py index fe3f5d6..2f35ded 100644 --- a/src/knockapi/types/shared/condition.py +++ b/src/knockapi/types/shared/condition.py @@ -28,7 +28,7 @@ class Condition(BaseModel): "contains_all", "is_timestamp", "is_not_timestamp", - "is_timestamp_after", + "is_timestamp_on_or_after", "is_timestamp_before", "is_timestamp_between", "is_audience_member", diff --git a/src/knockapi/types/shared_params/condition.py b/src/knockapi/types/shared_params/condition.py index eb29011..383bb19 100644 --- a/src/knockapi/types/shared_params/condition.py +++ b/src/knockapi/types/shared_params/condition.py @@ -29,7 +29,7 @@ class Condition(TypedDict, total=False): "contains_all", "is_timestamp", "is_not_timestamp", - "is_timestamp_after", + "is_timestamp_on_or_after", "is_timestamp_before", "is_timestamp_between", "is_audience_member", From 94f9a57672939b32bb54f5586d778e25add94904 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:51:10 +0000 Subject: [PATCH 31/44] feat(api): api update --- .stats.yml | 4 ++-- src/knockapi/types/shared/condition.py | 2 +- src/knockapi/types/shared_params/condition.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8f8211f..55110a2 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-0c621c06484c8d9a82e2cc70d2984dce46fa82f86185bce773c0bc265df77139.yml -openapi_spec_hash: 8670b393e28985dc4e93a16597690025 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-138601849c510c1e1c96af745bc2a4c4099a6e69054b454ba69e61082fb60f31.yml +openapi_spec_hash: 4858bf3005cfe4a73eaa5cdc8a4ac939 config_hash: 2b42d138d85c524e65fa7e205d36cc4a diff --git a/src/knockapi/types/shared/condition.py b/src/knockapi/types/shared/condition.py index 2f35ded..fe3f5d6 100644 --- a/src/knockapi/types/shared/condition.py +++ b/src/knockapi/types/shared/condition.py @@ -28,7 +28,7 @@ class Condition(BaseModel): "contains_all", "is_timestamp", "is_not_timestamp", - "is_timestamp_on_or_after", + "is_timestamp_after", "is_timestamp_before", "is_timestamp_between", "is_audience_member", diff --git a/src/knockapi/types/shared_params/condition.py b/src/knockapi/types/shared_params/condition.py index 383bb19..eb29011 100644 --- a/src/knockapi/types/shared_params/condition.py +++ b/src/knockapi/types/shared_params/condition.py @@ -29,7 +29,7 @@ class Condition(TypedDict, total=False): "contains_all", "is_timestamp", "is_not_timestamp", - "is_timestamp_on_or_after", + "is_timestamp_after", "is_timestamp_before", "is_timestamp_between", "is_audience_member", From f4573c32948495b568f1201a5fb7404df4aadf51 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 23:36:43 +0000 Subject: [PATCH 32/44] feat(api): api update --- .stats.yml | 4 ++-- src/knockapi/types/shared/condition.py | 4 +++- src/knockapi/types/shared_params/condition.py | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.stats.yml b/.stats.yml index 55110a2..075cb58 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-138601849c510c1e1c96af745bc2a4c4099a6e69054b454ba69e61082fb60f31.yml -openapi_spec_hash: 4858bf3005cfe4a73eaa5cdc8a4ac939 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-5d586855fa3ef8505e3cf0c86ea7903a7f5609853c3697c6a110c88e1c3344c1.yml +openapi_spec_hash: 1afe31150fa4b2ed126b0d809513fc0a config_hash: 2b42d138d85c524e65fa7e205d36cc4a diff --git a/src/knockapi/types/shared/condition.py b/src/knockapi/types/shared/condition.py index fe3f5d6..578e828 100644 --- a/src/knockapi/types/shared/condition.py +++ b/src/knockapi/types/shared/condition.py @@ -28,8 +28,10 @@ class Condition(BaseModel): "contains_all", "is_timestamp", "is_not_timestamp", - "is_timestamp_after", + "is_timestamp_on_or_after", "is_timestamp_before", + "is_timestamp_on_or_after_date", + "is_timestamp_before_date", "is_timestamp_between", "is_audience_member", "is_not_audience_member", diff --git a/src/knockapi/types/shared_params/condition.py b/src/knockapi/types/shared_params/condition.py index eb29011..5173154 100644 --- a/src/knockapi/types/shared_params/condition.py +++ b/src/knockapi/types/shared_params/condition.py @@ -29,8 +29,10 @@ class Condition(TypedDict, total=False): "contains_all", "is_timestamp", "is_not_timestamp", - "is_timestamp_after", + "is_timestamp_on_or_after", "is_timestamp_before", + "is_timestamp_on_or_after_date", + "is_timestamp_before_date", "is_timestamp_between", "is_audience_member", "is_not_audience_member", From c50a8eeadf1e424505f2c112d74f66cadaa1d2cf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 20:39:57 +0000 Subject: [PATCH 33/44] feat(api): api update --- .stats.yml | 4 +-- .../types/audience_add_members_params.py | 20 ++++++------ .../types/audience_remove_members_params.py | 20 ++++++------ tests/api_resources/test_audiences.py | 32 +++++++++---------- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/.stats.yml b/.stats.yml index 075cb58..87f7c8d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-5d586855fa3ef8505e3cf0c86ea7903a7f5609853c3697c6a110c88e1c3344c1.yml -openapi_spec_hash: 1afe31150fa4b2ed126b0d809513fc0a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-1847c4ab086e290a4e3a698f3bc55605fa5c24cbdef663a51a8b84d260312491.yml +openapi_spec_hash: 6ecef3ce2fc44f77781c835fefe1aa82 config_hash: 2b42d138d85c524e65fa7e205d36cc4a diff --git a/src/knockapi/types/audience_add_members_params.py b/src/knockapi/types/audience_add_members_params.py index 6461285..6fef20a 100644 --- a/src/knockapi/types/audience_add_members_params.py +++ b/src/knockapi/types/audience_add_members_params.py @@ -5,7 +5,9 @@ from typing import Iterable, Optional from typing_extensions import Required, TypedDict -__all__ = ["AudienceAddMembersParams", "Member", "MemberUser"] +from .inline_identify_user_request_param import InlineIdentifyUserRequestParam + +__all__ = ["AudienceAddMembersParams", "Member"] class AudienceAddMembersParams(TypedDict, total=False): @@ -13,18 +15,16 @@ class AudienceAddMembersParams(TypedDict, total=False): """A list of audience members to add. Limited to 1,000 members per request.""" -class MemberUser(TypedDict, total=False): - """An object containing the user's ID.""" - - id: str - """The unique identifier of the user.""" - - class Member(TypedDict, total=False): """An audience member.""" - user: Required[MemberUser] - """An object containing the user's ID.""" + user: Required[InlineIdentifyUserRequestParam] + """A set of parameters to inline-identify a user with. + + Inline identifying the user will ensure that the user is available before the + request is executed in Knock. It will perform an upsert for the user you're + supplying, replacing any properties specified. + """ tenant: Optional[str] """The unique identifier for the tenant.""" diff --git a/src/knockapi/types/audience_remove_members_params.py b/src/knockapi/types/audience_remove_members_params.py index d33f0a3..57a7e23 100644 --- a/src/knockapi/types/audience_remove_members_params.py +++ b/src/knockapi/types/audience_remove_members_params.py @@ -5,7 +5,9 @@ from typing import Iterable, Optional from typing_extensions import Required, TypedDict -__all__ = ["AudienceRemoveMembersParams", "Member", "MemberUser"] +from .inline_identify_user_request_param import InlineIdentifyUserRequestParam + +__all__ = ["AudienceRemoveMembersParams", "Member"] class AudienceRemoveMembersParams(TypedDict, total=False): @@ -13,18 +15,16 @@ class AudienceRemoveMembersParams(TypedDict, total=False): """A list of audience members to remove.""" -class MemberUser(TypedDict, total=False): - """An object containing the user's ID.""" - - id: str - """The unique identifier of the user.""" - - class Member(TypedDict, total=False): """An audience member.""" - user: Required[MemberUser] - """An object containing the user's ID.""" + user: Required[InlineIdentifyUserRequestParam] + """A set of parameters to inline-identify a user with. + + Inline identifying the user will ensure that the user is available before the + request is executed in Knock. It will perform an upsert for the user you're + supplying, replacing any properties specified. + """ tenant: Optional[str] """The unique identifier for the tenant.""" diff --git a/tests/api_resources/test_audiences.py b/tests/api_resources/test_audiences.py index 09981e0..b049dd6 100644 --- a/tests/api_resources/test_audiences.py +++ b/tests/api_resources/test_audiences.py @@ -22,7 +22,7 @@ class TestAudiences: def test_method_add_members(self, client: Knock) -> None: audience = client.audiences.add_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert audience is None @@ -31,7 +31,7 @@ def test_method_add_members(self, client: Knock) -> None: def test_raw_response_add_members(self, client: Knock) -> None: response = client.audiences.with_raw_response.add_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert response.is_closed is True @@ -44,7 +44,7 @@ def test_raw_response_add_members(self, client: Knock) -> None: def test_streaming_response_add_members(self, client: Knock) -> None: with client.audiences.with_streaming_response.add_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -60,7 +60,7 @@ def test_path_params_add_members(self, client: Knock) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"): client.audiences.with_raw_response.add_members( key="", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) @pytest.mark.skip(reason="Prism doesn't support callbacks yet") @@ -110,7 +110,7 @@ def test_path_params_list_members(self, client: Knock) -> None: def test_method_remove_members(self, client: Knock) -> None: audience = client.audiences.remove_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert audience is None @@ -119,7 +119,7 @@ def test_method_remove_members(self, client: Knock) -> None: def test_raw_response_remove_members(self, client: Knock) -> None: response = client.audiences.with_raw_response.remove_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert response.is_closed is True @@ -132,7 +132,7 @@ def test_raw_response_remove_members(self, client: Knock) -> None: def test_streaming_response_remove_members(self, client: Knock) -> None: with client.audiences.with_streaming_response.remove_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -148,7 +148,7 @@ def test_path_params_remove_members(self, client: Knock) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"): client.audiences.with_raw_response.remove_members( key="", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) @@ -162,7 +162,7 @@ class TestAsyncAudiences: async def test_method_add_members(self, async_client: AsyncKnock) -> None: audience = await async_client.audiences.add_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert audience is None @@ -171,7 +171,7 @@ async def test_method_add_members(self, async_client: AsyncKnock) -> None: async def test_raw_response_add_members(self, async_client: AsyncKnock) -> None: response = await async_client.audiences.with_raw_response.add_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert response.is_closed is True @@ -184,7 +184,7 @@ async def test_raw_response_add_members(self, async_client: AsyncKnock) -> None: async def test_streaming_response_add_members(self, async_client: AsyncKnock) -> None: async with async_client.audiences.with_streaming_response.add_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -200,7 +200,7 @@ async def test_path_params_add_members(self, async_client: AsyncKnock) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"): await async_client.audiences.with_raw_response.add_members( key="", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) @pytest.mark.skip(reason="Prism doesn't support callbacks yet") @@ -250,7 +250,7 @@ async def test_path_params_list_members(self, async_client: AsyncKnock) -> None: async def test_method_remove_members(self, async_client: AsyncKnock) -> None: audience = await async_client.audiences.remove_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert audience is None @@ -259,7 +259,7 @@ async def test_method_remove_members(self, async_client: AsyncKnock) -> None: async def test_raw_response_remove_members(self, async_client: AsyncKnock) -> None: response = await async_client.audiences.with_raw_response.remove_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert response.is_closed is True @@ -272,7 +272,7 @@ async def test_raw_response_remove_members(self, async_client: AsyncKnock) -> No async def test_streaming_response_remove_members(self, async_client: AsyncKnock) -> None: async with async_client.audiences.with_streaming_response.remove_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -288,5 +288,5 @@ async def test_path_params_remove_members(self, async_client: AsyncKnock) -> Non with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"): await async_client.audiences.with_raw_response.remove_members( key="", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) From 751818aeedc73b4a53e9ddeb9966ff5973cdb3cf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:07:51 +0000 Subject: [PATCH 34/44] chore(ci): upgrade `actions/github-script` --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2e3b35..683bc1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: - name: Get GitHub OIDC Token if: github.repository == 'stainless-sdks/knock-python' id: github-oidc - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); From 3ef2d85dcb10221fb441a7c3869d19391e0504b6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 19:31:15 +0000 Subject: [PATCH 35/44] feat(api): api update --- .stats.yml | 4 +-- .../types/audience_add_members_params.py | 20 ++++++------ .../types/audience_remove_members_params.py | 20 ++++++------ tests/api_resources/test_audiences.py | 32 +++++++++---------- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/.stats.yml b/.stats.yml index 87f7c8d..075cb58 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-1847c4ab086e290a4e3a698f3bc55605fa5c24cbdef663a51a8b84d260312491.yml -openapi_spec_hash: 6ecef3ce2fc44f77781c835fefe1aa82 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-5d586855fa3ef8505e3cf0c86ea7903a7f5609853c3697c6a110c88e1c3344c1.yml +openapi_spec_hash: 1afe31150fa4b2ed126b0d809513fc0a config_hash: 2b42d138d85c524e65fa7e205d36cc4a diff --git a/src/knockapi/types/audience_add_members_params.py b/src/knockapi/types/audience_add_members_params.py index 6fef20a..6461285 100644 --- a/src/knockapi/types/audience_add_members_params.py +++ b/src/knockapi/types/audience_add_members_params.py @@ -5,9 +5,7 @@ from typing import Iterable, Optional from typing_extensions import Required, TypedDict -from .inline_identify_user_request_param import InlineIdentifyUserRequestParam - -__all__ = ["AudienceAddMembersParams", "Member"] +__all__ = ["AudienceAddMembersParams", "Member", "MemberUser"] class AudienceAddMembersParams(TypedDict, total=False): @@ -15,16 +13,18 @@ class AudienceAddMembersParams(TypedDict, total=False): """A list of audience members to add. Limited to 1,000 members per request.""" +class MemberUser(TypedDict, total=False): + """An object containing the user's ID.""" + + id: str + """The unique identifier of the user.""" + + class Member(TypedDict, total=False): """An audience member.""" - user: Required[InlineIdentifyUserRequestParam] - """A set of parameters to inline-identify a user with. - - Inline identifying the user will ensure that the user is available before the - request is executed in Knock. It will perform an upsert for the user you're - supplying, replacing any properties specified. - """ + user: Required[MemberUser] + """An object containing the user's ID.""" tenant: Optional[str] """The unique identifier for the tenant.""" diff --git a/src/knockapi/types/audience_remove_members_params.py b/src/knockapi/types/audience_remove_members_params.py index 57a7e23..d33f0a3 100644 --- a/src/knockapi/types/audience_remove_members_params.py +++ b/src/knockapi/types/audience_remove_members_params.py @@ -5,9 +5,7 @@ from typing import Iterable, Optional from typing_extensions import Required, TypedDict -from .inline_identify_user_request_param import InlineIdentifyUserRequestParam - -__all__ = ["AudienceRemoveMembersParams", "Member"] +__all__ = ["AudienceRemoveMembersParams", "Member", "MemberUser"] class AudienceRemoveMembersParams(TypedDict, total=False): @@ -15,16 +13,18 @@ class AudienceRemoveMembersParams(TypedDict, total=False): """A list of audience members to remove.""" +class MemberUser(TypedDict, total=False): + """An object containing the user's ID.""" + + id: str + """The unique identifier of the user.""" + + class Member(TypedDict, total=False): """An audience member.""" - user: Required[InlineIdentifyUserRequestParam] - """A set of parameters to inline-identify a user with. - - Inline identifying the user will ensure that the user is available before the - request is executed in Knock. It will perform an upsert for the user you're - supplying, replacing any properties specified. - """ + user: Required[MemberUser] + """An object containing the user's ID.""" tenant: Optional[str] """The unique identifier for the tenant.""" diff --git a/tests/api_resources/test_audiences.py b/tests/api_resources/test_audiences.py index b049dd6..09981e0 100644 --- a/tests/api_resources/test_audiences.py +++ b/tests/api_resources/test_audiences.py @@ -22,7 +22,7 @@ class TestAudiences: def test_method_add_members(self, client: Knock) -> None: audience = client.audiences.add_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) assert audience is None @@ -31,7 +31,7 @@ def test_method_add_members(self, client: Knock) -> None: def test_raw_response_add_members(self, client: Knock) -> None: response = client.audiences.with_raw_response.add_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) assert response.is_closed is True @@ -44,7 +44,7 @@ def test_raw_response_add_members(self, client: Knock) -> None: def test_streaming_response_add_members(self, client: Knock) -> None: with client.audiences.with_streaming_response.add_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -60,7 +60,7 @@ def test_path_params_add_members(self, client: Knock) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"): client.audiences.with_raw_response.add_members( key="", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) @pytest.mark.skip(reason="Prism doesn't support callbacks yet") @@ -110,7 +110,7 @@ def test_path_params_list_members(self, client: Knock) -> None: def test_method_remove_members(self, client: Knock) -> None: audience = client.audiences.remove_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) assert audience is None @@ -119,7 +119,7 @@ def test_method_remove_members(self, client: Knock) -> None: def test_raw_response_remove_members(self, client: Knock) -> None: response = client.audiences.with_raw_response.remove_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) assert response.is_closed is True @@ -132,7 +132,7 @@ def test_raw_response_remove_members(self, client: Knock) -> None: def test_streaming_response_remove_members(self, client: Knock) -> None: with client.audiences.with_streaming_response.remove_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -148,7 +148,7 @@ def test_path_params_remove_members(self, client: Knock) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"): client.audiences.with_raw_response.remove_members( key="", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) @@ -162,7 +162,7 @@ class TestAsyncAudiences: async def test_method_add_members(self, async_client: AsyncKnock) -> None: audience = await async_client.audiences.add_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) assert audience is None @@ -171,7 +171,7 @@ async def test_method_add_members(self, async_client: AsyncKnock) -> None: async def test_raw_response_add_members(self, async_client: AsyncKnock) -> None: response = await async_client.audiences.with_raw_response.add_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) assert response.is_closed is True @@ -184,7 +184,7 @@ async def test_raw_response_add_members(self, async_client: AsyncKnock) -> None: async def test_streaming_response_add_members(self, async_client: AsyncKnock) -> None: async with async_client.audiences.with_streaming_response.add_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -200,7 +200,7 @@ async def test_path_params_add_members(self, async_client: AsyncKnock) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"): await async_client.audiences.with_raw_response.add_members( key="", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) @pytest.mark.skip(reason="Prism doesn't support callbacks yet") @@ -250,7 +250,7 @@ async def test_path_params_list_members(self, async_client: AsyncKnock) -> None: async def test_method_remove_members(self, async_client: AsyncKnock) -> None: audience = await async_client.audiences.remove_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) assert audience is None @@ -259,7 +259,7 @@ async def test_method_remove_members(self, async_client: AsyncKnock) -> None: async def test_raw_response_remove_members(self, async_client: AsyncKnock) -> None: response = await async_client.audiences.with_raw_response.remove_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) assert response.is_closed is True @@ -272,7 +272,7 @@ async def test_raw_response_remove_members(self, async_client: AsyncKnock) -> No async def test_streaming_response_remove_members(self, async_client: AsyncKnock) -> None: async with async_client.audiences.with_streaming_response.remove_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -288,5 +288,5 @@ async def test_path_params_remove_members(self, async_client: AsyncKnock) -> Non with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"): await async_client.audiences.with_raw_response.remove_members( key="", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) From 6fa83315060530d14ad296f4d725e1e544d43faf Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 20:03:26 +0000 Subject: [PATCH 36/44] feat(api): api update --- .stats.yml | 4 +-- .../types/audience_add_members_params.py | 20 ++++++------ .../types/audience_remove_members_params.py | 20 ++++++------ tests/api_resources/test_audiences.py | 32 +++++++++---------- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/.stats.yml b/.stats.yml index 075cb58..87f7c8d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-5d586855fa3ef8505e3cf0c86ea7903a7f5609853c3697c6a110c88e1c3344c1.yml -openapi_spec_hash: 1afe31150fa4b2ed126b0d809513fc0a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-1847c4ab086e290a4e3a698f3bc55605fa5c24cbdef663a51a8b84d260312491.yml +openapi_spec_hash: 6ecef3ce2fc44f77781c835fefe1aa82 config_hash: 2b42d138d85c524e65fa7e205d36cc4a diff --git a/src/knockapi/types/audience_add_members_params.py b/src/knockapi/types/audience_add_members_params.py index 6461285..6fef20a 100644 --- a/src/knockapi/types/audience_add_members_params.py +++ b/src/knockapi/types/audience_add_members_params.py @@ -5,7 +5,9 @@ from typing import Iterable, Optional from typing_extensions import Required, TypedDict -__all__ = ["AudienceAddMembersParams", "Member", "MemberUser"] +from .inline_identify_user_request_param import InlineIdentifyUserRequestParam + +__all__ = ["AudienceAddMembersParams", "Member"] class AudienceAddMembersParams(TypedDict, total=False): @@ -13,18 +15,16 @@ class AudienceAddMembersParams(TypedDict, total=False): """A list of audience members to add. Limited to 1,000 members per request.""" -class MemberUser(TypedDict, total=False): - """An object containing the user's ID.""" - - id: str - """The unique identifier of the user.""" - - class Member(TypedDict, total=False): """An audience member.""" - user: Required[MemberUser] - """An object containing the user's ID.""" + user: Required[InlineIdentifyUserRequestParam] + """A set of parameters to inline-identify a user with. + + Inline identifying the user will ensure that the user is available before the + request is executed in Knock. It will perform an upsert for the user you're + supplying, replacing any properties specified. + """ tenant: Optional[str] """The unique identifier for the tenant.""" diff --git a/src/knockapi/types/audience_remove_members_params.py b/src/knockapi/types/audience_remove_members_params.py index d33f0a3..57a7e23 100644 --- a/src/knockapi/types/audience_remove_members_params.py +++ b/src/knockapi/types/audience_remove_members_params.py @@ -5,7 +5,9 @@ from typing import Iterable, Optional from typing_extensions import Required, TypedDict -__all__ = ["AudienceRemoveMembersParams", "Member", "MemberUser"] +from .inline_identify_user_request_param import InlineIdentifyUserRequestParam + +__all__ = ["AudienceRemoveMembersParams", "Member"] class AudienceRemoveMembersParams(TypedDict, total=False): @@ -13,18 +15,16 @@ class AudienceRemoveMembersParams(TypedDict, total=False): """A list of audience members to remove.""" -class MemberUser(TypedDict, total=False): - """An object containing the user's ID.""" - - id: str - """The unique identifier of the user.""" - - class Member(TypedDict, total=False): """An audience member.""" - user: Required[MemberUser] - """An object containing the user's ID.""" + user: Required[InlineIdentifyUserRequestParam] + """A set of parameters to inline-identify a user with. + + Inline identifying the user will ensure that the user is available before the + request is executed in Knock. It will perform an upsert for the user you're + supplying, replacing any properties specified. + """ tenant: Optional[str] """The unique identifier for the tenant.""" diff --git a/tests/api_resources/test_audiences.py b/tests/api_resources/test_audiences.py index 09981e0..b049dd6 100644 --- a/tests/api_resources/test_audiences.py +++ b/tests/api_resources/test_audiences.py @@ -22,7 +22,7 @@ class TestAudiences: def test_method_add_members(self, client: Knock) -> None: audience = client.audiences.add_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert audience is None @@ -31,7 +31,7 @@ def test_method_add_members(self, client: Knock) -> None: def test_raw_response_add_members(self, client: Knock) -> None: response = client.audiences.with_raw_response.add_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert response.is_closed is True @@ -44,7 +44,7 @@ def test_raw_response_add_members(self, client: Knock) -> None: def test_streaming_response_add_members(self, client: Knock) -> None: with client.audiences.with_streaming_response.add_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -60,7 +60,7 @@ def test_path_params_add_members(self, client: Knock) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"): client.audiences.with_raw_response.add_members( key="", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) @pytest.mark.skip(reason="Prism doesn't support callbacks yet") @@ -110,7 +110,7 @@ def test_path_params_list_members(self, client: Knock) -> None: def test_method_remove_members(self, client: Knock) -> None: audience = client.audiences.remove_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert audience is None @@ -119,7 +119,7 @@ def test_method_remove_members(self, client: Knock) -> None: def test_raw_response_remove_members(self, client: Knock) -> None: response = client.audiences.with_raw_response.remove_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert response.is_closed is True @@ -132,7 +132,7 @@ def test_raw_response_remove_members(self, client: Knock) -> None: def test_streaming_response_remove_members(self, client: Knock) -> None: with client.audiences.with_streaming_response.remove_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -148,7 +148,7 @@ def test_path_params_remove_members(self, client: Knock) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"): client.audiences.with_raw_response.remove_members( key="", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) @@ -162,7 +162,7 @@ class TestAsyncAudiences: async def test_method_add_members(self, async_client: AsyncKnock) -> None: audience = await async_client.audiences.add_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert audience is None @@ -171,7 +171,7 @@ async def test_method_add_members(self, async_client: AsyncKnock) -> None: async def test_raw_response_add_members(self, async_client: AsyncKnock) -> None: response = await async_client.audiences.with_raw_response.add_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert response.is_closed is True @@ -184,7 +184,7 @@ async def test_raw_response_add_members(self, async_client: AsyncKnock) -> None: async def test_streaming_response_add_members(self, async_client: AsyncKnock) -> None: async with async_client.audiences.with_streaming_response.add_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -200,7 +200,7 @@ async def test_path_params_add_members(self, async_client: AsyncKnock) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"): await async_client.audiences.with_raw_response.add_members( key="", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) @pytest.mark.skip(reason="Prism doesn't support callbacks yet") @@ -250,7 +250,7 @@ async def test_path_params_list_members(self, async_client: AsyncKnock) -> None: async def test_method_remove_members(self, async_client: AsyncKnock) -> None: audience = await async_client.audiences.remove_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert audience is None @@ -259,7 +259,7 @@ async def test_method_remove_members(self, async_client: AsyncKnock) -> None: async def test_raw_response_remove_members(self, async_client: AsyncKnock) -> None: response = await async_client.audiences.with_raw_response.remove_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert response.is_closed is True @@ -272,7 +272,7 @@ async def test_raw_response_remove_members(self, async_client: AsyncKnock) -> No async def test_streaming_response_remove_members(self, async_client: AsyncKnock) -> None: async with async_client.audiences.with_streaming_response.remove_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -288,5 +288,5 @@ async def test_path_params_remove_members(self, async_client: AsyncKnock) -> Non with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"): await async_client.audiences.with_raw_response.remove_members( key="", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) From 8c88c545b234b0bbf876106d889c6f3ce41ef32d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:57:11 +0000 Subject: [PATCH 37/44] feat(api): api update --- .stats.yml | 4 +-- .../types/audience_add_members_params.py | 20 ++++++------ .../types/audience_remove_members_params.py | 20 ++++++------ tests/api_resources/test_audiences.py | 32 +++++++++---------- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/.stats.yml b/.stats.yml index 87f7c8d..075cb58 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-1847c4ab086e290a4e3a698f3bc55605fa5c24cbdef663a51a8b84d260312491.yml -openapi_spec_hash: 6ecef3ce2fc44f77781c835fefe1aa82 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-5d586855fa3ef8505e3cf0c86ea7903a7f5609853c3697c6a110c88e1c3344c1.yml +openapi_spec_hash: 1afe31150fa4b2ed126b0d809513fc0a config_hash: 2b42d138d85c524e65fa7e205d36cc4a diff --git a/src/knockapi/types/audience_add_members_params.py b/src/knockapi/types/audience_add_members_params.py index 6fef20a..6461285 100644 --- a/src/knockapi/types/audience_add_members_params.py +++ b/src/knockapi/types/audience_add_members_params.py @@ -5,9 +5,7 @@ from typing import Iterable, Optional from typing_extensions import Required, TypedDict -from .inline_identify_user_request_param import InlineIdentifyUserRequestParam - -__all__ = ["AudienceAddMembersParams", "Member"] +__all__ = ["AudienceAddMembersParams", "Member", "MemberUser"] class AudienceAddMembersParams(TypedDict, total=False): @@ -15,16 +13,18 @@ class AudienceAddMembersParams(TypedDict, total=False): """A list of audience members to add. Limited to 1,000 members per request.""" +class MemberUser(TypedDict, total=False): + """An object containing the user's ID.""" + + id: str + """The unique identifier of the user.""" + + class Member(TypedDict, total=False): """An audience member.""" - user: Required[InlineIdentifyUserRequestParam] - """A set of parameters to inline-identify a user with. - - Inline identifying the user will ensure that the user is available before the - request is executed in Knock. It will perform an upsert for the user you're - supplying, replacing any properties specified. - """ + user: Required[MemberUser] + """An object containing the user's ID.""" tenant: Optional[str] """The unique identifier for the tenant.""" diff --git a/src/knockapi/types/audience_remove_members_params.py b/src/knockapi/types/audience_remove_members_params.py index 57a7e23..d33f0a3 100644 --- a/src/knockapi/types/audience_remove_members_params.py +++ b/src/knockapi/types/audience_remove_members_params.py @@ -5,9 +5,7 @@ from typing import Iterable, Optional from typing_extensions import Required, TypedDict -from .inline_identify_user_request_param import InlineIdentifyUserRequestParam - -__all__ = ["AudienceRemoveMembersParams", "Member"] +__all__ = ["AudienceRemoveMembersParams", "Member", "MemberUser"] class AudienceRemoveMembersParams(TypedDict, total=False): @@ -15,16 +13,18 @@ class AudienceRemoveMembersParams(TypedDict, total=False): """A list of audience members to remove.""" +class MemberUser(TypedDict, total=False): + """An object containing the user's ID.""" + + id: str + """The unique identifier of the user.""" + + class Member(TypedDict, total=False): """An audience member.""" - user: Required[InlineIdentifyUserRequestParam] - """A set of parameters to inline-identify a user with. - - Inline identifying the user will ensure that the user is available before the - request is executed in Knock. It will perform an upsert for the user you're - supplying, replacing any properties specified. - """ + user: Required[MemberUser] + """An object containing the user's ID.""" tenant: Optional[str] """The unique identifier for the tenant.""" diff --git a/tests/api_resources/test_audiences.py b/tests/api_resources/test_audiences.py index b049dd6..09981e0 100644 --- a/tests/api_resources/test_audiences.py +++ b/tests/api_resources/test_audiences.py @@ -22,7 +22,7 @@ class TestAudiences: def test_method_add_members(self, client: Knock) -> None: audience = client.audiences.add_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) assert audience is None @@ -31,7 +31,7 @@ def test_method_add_members(self, client: Knock) -> None: def test_raw_response_add_members(self, client: Knock) -> None: response = client.audiences.with_raw_response.add_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) assert response.is_closed is True @@ -44,7 +44,7 @@ def test_raw_response_add_members(self, client: Knock) -> None: def test_streaming_response_add_members(self, client: Knock) -> None: with client.audiences.with_streaming_response.add_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -60,7 +60,7 @@ def test_path_params_add_members(self, client: Knock) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"): client.audiences.with_raw_response.add_members( key="", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) @pytest.mark.skip(reason="Prism doesn't support callbacks yet") @@ -110,7 +110,7 @@ def test_path_params_list_members(self, client: Knock) -> None: def test_method_remove_members(self, client: Knock) -> None: audience = client.audiences.remove_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) assert audience is None @@ -119,7 +119,7 @@ def test_method_remove_members(self, client: Knock) -> None: def test_raw_response_remove_members(self, client: Knock) -> None: response = client.audiences.with_raw_response.remove_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) assert response.is_closed is True @@ -132,7 +132,7 @@ def test_raw_response_remove_members(self, client: Knock) -> None: def test_streaming_response_remove_members(self, client: Knock) -> None: with client.audiences.with_streaming_response.remove_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -148,7 +148,7 @@ def test_path_params_remove_members(self, client: Knock) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"): client.audiences.with_raw_response.remove_members( key="", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) @@ -162,7 +162,7 @@ class TestAsyncAudiences: async def test_method_add_members(self, async_client: AsyncKnock) -> None: audience = await async_client.audiences.add_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) assert audience is None @@ -171,7 +171,7 @@ async def test_method_add_members(self, async_client: AsyncKnock) -> None: async def test_raw_response_add_members(self, async_client: AsyncKnock) -> None: response = await async_client.audiences.with_raw_response.add_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) assert response.is_closed is True @@ -184,7 +184,7 @@ async def test_raw_response_add_members(self, async_client: AsyncKnock) -> None: async def test_streaming_response_add_members(self, async_client: AsyncKnock) -> None: async with async_client.audiences.with_streaming_response.add_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -200,7 +200,7 @@ async def test_path_params_add_members(self, async_client: AsyncKnock) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"): await async_client.audiences.with_raw_response.add_members( key="", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) @pytest.mark.skip(reason="Prism doesn't support callbacks yet") @@ -250,7 +250,7 @@ async def test_path_params_list_members(self, async_client: AsyncKnock) -> None: async def test_method_remove_members(self, async_client: AsyncKnock) -> None: audience = await async_client.audiences.remove_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) assert audience is None @@ -259,7 +259,7 @@ async def test_method_remove_members(self, async_client: AsyncKnock) -> None: async def test_raw_response_remove_members(self, async_client: AsyncKnock) -> None: response = await async_client.audiences.with_raw_response.remove_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) assert response.is_closed is True @@ -272,7 +272,7 @@ async def test_raw_response_remove_members(self, async_client: AsyncKnock) -> No async def test_streaming_response_remove_members(self, async_client: AsyncKnock) -> None: async with async_client.audiences.with_streaming_response.remove_members( key="key", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -288,5 +288,5 @@ async def test_path_params_remove_members(self, async_client: AsyncKnock) -> Non with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"): await async_client.audiences.with_raw_response.remove_members( key="", - members=[{"user": {"id": "dr_sattler"}}], + members=[{"user": {}}], ) From de9af6473687644be3598673a1aedd37d170b329 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 20:58:46 +0000 Subject: [PATCH 38/44] feat(api): api update --- .stats.yml | 4 +-- src/knockapi/resources/users/feeds.py | 6 ++++ .../types/audience_add_members_params.py | 20 ++++++------ .../types/audience_remove_members_params.py | 20 ++++++------ tests/api_resources/test_audiences.py | 32 +++++++++---------- 5 files changed, 44 insertions(+), 38 deletions(-) diff --git a/.stats.yml b/.stats.yml index 075cb58..f7645e9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-5d586855fa3ef8505e3cf0c86ea7903a7f5609853c3697c6a110c88e1c3344c1.yml -openapi_spec_hash: 1afe31150fa4b2ed126b0d809513fc0a +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-051fb64fc98086ac7435196316e0aee760dce287ac9e44eed11ea243893498c9.yml +openapi_spec_hash: c3cea367a962be6faea1e8fba430ed3c config_hash: 2b42d138d85c524e65fa7e205d36cc4a diff --git a/src/knockapi/resources/users/feeds.py b/src/knockapi/resources/users/feeds.py index 2014f7f..e23d3d5 100644 --- a/src/knockapi/resources/users/feeds.py +++ b/src/knockapi/resources/users/feeds.py @@ -119,6 +119,9 @@ def list_items( along with a user token. - This endpoint’s rate limit is always scoped per-user and per-environment. This is true even for requests made without a signed user token. + - Any [attachments](/integrations/email/attachments) present in trigger data are + automatically excluded from both the `data` and `activities` fields of + `UserInAppFeedResponse`. Args: after: The cursor to fetch entries after. @@ -281,6 +284,9 @@ def list_items( along with a user token. - This endpoint’s rate limit is always scoped per-user and per-environment. This is true even for requests made without a signed user token. + - Any [attachments](/integrations/email/attachments) present in trigger data are + automatically excluded from both the `data` and `activities` fields of + `UserInAppFeedResponse`. Args: after: The cursor to fetch entries after. diff --git a/src/knockapi/types/audience_add_members_params.py b/src/knockapi/types/audience_add_members_params.py index 6461285..6fef20a 100644 --- a/src/knockapi/types/audience_add_members_params.py +++ b/src/knockapi/types/audience_add_members_params.py @@ -5,7 +5,9 @@ from typing import Iterable, Optional from typing_extensions import Required, TypedDict -__all__ = ["AudienceAddMembersParams", "Member", "MemberUser"] +from .inline_identify_user_request_param import InlineIdentifyUserRequestParam + +__all__ = ["AudienceAddMembersParams", "Member"] class AudienceAddMembersParams(TypedDict, total=False): @@ -13,18 +15,16 @@ class AudienceAddMembersParams(TypedDict, total=False): """A list of audience members to add. Limited to 1,000 members per request.""" -class MemberUser(TypedDict, total=False): - """An object containing the user's ID.""" - - id: str - """The unique identifier of the user.""" - - class Member(TypedDict, total=False): """An audience member.""" - user: Required[MemberUser] - """An object containing the user's ID.""" + user: Required[InlineIdentifyUserRequestParam] + """A set of parameters to inline-identify a user with. + + Inline identifying the user will ensure that the user is available before the + request is executed in Knock. It will perform an upsert for the user you're + supplying, replacing any properties specified. + """ tenant: Optional[str] """The unique identifier for the tenant.""" diff --git a/src/knockapi/types/audience_remove_members_params.py b/src/knockapi/types/audience_remove_members_params.py index d33f0a3..57a7e23 100644 --- a/src/knockapi/types/audience_remove_members_params.py +++ b/src/knockapi/types/audience_remove_members_params.py @@ -5,7 +5,9 @@ from typing import Iterable, Optional from typing_extensions import Required, TypedDict -__all__ = ["AudienceRemoveMembersParams", "Member", "MemberUser"] +from .inline_identify_user_request_param import InlineIdentifyUserRequestParam + +__all__ = ["AudienceRemoveMembersParams", "Member"] class AudienceRemoveMembersParams(TypedDict, total=False): @@ -13,18 +15,16 @@ class AudienceRemoveMembersParams(TypedDict, total=False): """A list of audience members to remove.""" -class MemberUser(TypedDict, total=False): - """An object containing the user's ID.""" - - id: str - """The unique identifier of the user.""" - - class Member(TypedDict, total=False): """An audience member.""" - user: Required[MemberUser] - """An object containing the user's ID.""" + user: Required[InlineIdentifyUserRequestParam] + """A set of parameters to inline-identify a user with. + + Inline identifying the user will ensure that the user is available before the + request is executed in Knock. It will perform an upsert for the user you're + supplying, replacing any properties specified. + """ tenant: Optional[str] """The unique identifier for the tenant.""" diff --git a/tests/api_resources/test_audiences.py b/tests/api_resources/test_audiences.py index 09981e0..b049dd6 100644 --- a/tests/api_resources/test_audiences.py +++ b/tests/api_resources/test_audiences.py @@ -22,7 +22,7 @@ class TestAudiences: def test_method_add_members(self, client: Knock) -> None: audience = client.audiences.add_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert audience is None @@ -31,7 +31,7 @@ def test_method_add_members(self, client: Knock) -> None: def test_raw_response_add_members(self, client: Knock) -> None: response = client.audiences.with_raw_response.add_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert response.is_closed is True @@ -44,7 +44,7 @@ def test_raw_response_add_members(self, client: Knock) -> None: def test_streaming_response_add_members(self, client: Knock) -> None: with client.audiences.with_streaming_response.add_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -60,7 +60,7 @@ def test_path_params_add_members(self, client: Knock) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"): client.audiences.with_raw_response.add_members( key="", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) @pytest.mark.skip(reason="Prism doesn't support callbacks yet") @@ -110,7 +110,7 @@ def test_path_params_list_members(self, client: Knock) -> None: def test_method_remove_members(self, client: Knock) -> None: audience = client.audiences.remove_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert audience is None @@ -119,7 +119,7 @@ def test_method_remove_members(self, client: Knock) -> None: def test_raw_response_remove_members(self, client: Knock) -> None: response = client.audiences.with_raw_response.remove_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert response.is_closed is True @@ -132,7 +132,7 @@ def test_raw_response_remove_members(self, client: Knock) -> None: def test_streaming_response_remove_members(self, client: Knock) -> None: with client.audiences.with_streaming_response.remove_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -148,7 +148,7 @@ def test_path_params_remove_members(self, client: Knock) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"): client.audiences.with_raw_response.remove_members( key="", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) @@ -162,7 +162,7 @@ class TestAsyncAudiences: async def test_method_add_members(self, async_client: AsyncKnock) -> None: audience = await async_client.audiences.add_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert audience is None @@ -171,7 +171,7 @@ async def test_method_add_members(self, async_client: AsyncKnock) -> None: async def test_raw_response_add_members(self, async_client: AsyncKnock) -> None: response = await async_client.audiences.with_raw_response.add_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert response.is_closed is True @@ -184,7 +184,7 @@ async def test_raw_response_add_members(self, async_client: AsyncKnock) -> None: async def test_streaming_response_add_members(self, async_client: AsyncKnock) -> None: async with async_client.audiences.with_streaming_response.add_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -200,7 +200,7 @@ async def test_path_params_add_members(self, async_client: AsyncKnock) -> None: with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"): await async_client.audiences.with_raw_response.add_members( key="", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) @pytest.mark.skip(reason="Prism doesn't support callbacks yet") @@ -250,7 +250,7 @@ async def test_path_params_list_members(self, async_client: AsyncKnock) -> None: async def test_method_remove_members(self, async_client: AsyncKnock) -> None: audience = await async_client.audiences.remove_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert audience is None @@ -259,7 +259,7 @@ async def test_method_remove_members(self, async_client: AsyncKnock) -> None: async def test_raw_response_remove_members(self, async_client: AsyncKnock) -> None: response = await async_client.audiences.with_raw_response.remove_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) assert response.is_closed is True @@ -272,7 +272,7 @@ async def test_raw_response_remove_members(self, async_client: AsyncKnock) -> No async def test_streaming_response_remove_members(self, async_client: AsyncKnock) -> None: async with async_client.audiences.with_streaming_response.remove_members( key="key", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -288,5 +288,5 @@ async def test_path_params_remove_members(self, async_client: AsyncKnock) -> Non with pytest.raises(ValueError, match=r"Expected a non-empty value for `key` but received ''"): await async_client.audiences.with_raw_response.remove_members( key="", - members=[{"user": {}}], + members=[{"user": {"id": "dr_sattler"}}], ) From 53f67f56d7aef82576bb80c1e26ad44c2f1a1d85 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 20:24:43 +0000 Subject: [PATCH 39/44] feat(api): api update --- .stats.yml | 4 ++-- src/knockapi/resources/users/feeds.py | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.stats.yml b/.stats.yml index f7645e9..87f7c8d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-051fb64fc98086ac7435196316e0aee760dce287ac9e44eed11ea243893498c9.yml -openapi_spec_hash: c3cea367a962be6faea1e8fba430ed3c +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-1847c4ab086e290a4e3a698f3bc55605fa5c24cbdef663a51a8b84d260312491.yml +openapi_spec_hash: 6ecef3ce2fc44f77781c835fefe1aa82 config_hash: 2b42d138d85c524e65fa7e205d36cc4a diff --git a/src/knockapi/resources/users/feeds.py b/src/knockapi/resources/users/feeds.py index e23d3d5..2014f7f 100644 --- a/src/knockapi/resources/users/feeds.py +++ b/src/knockapi/resources/users/feeds.py @@ -119,9 +119,6 @@ def list_items( along with a user token. - This endpoint’s rate limit is always scoped per-user and per-environment. This is true even for requests made without a signed user token. - - Any [attachments](/integrations/email/attachments) present in trigger data are - automatically excluded from both the `data` and `activities` fields of - `UserInAppFeedResponse`. Args: after: The cursor to fetch entries after. @@ -284,9 +281,6 @@ def list_items( along with a user token. - This endpoint’s rate limit is always scoped per-user and per-environment. This is true even for requests made without a signed user token. - - Any [attachments](/integrations/email/attachments) present in trigger data are - automatically excluded from both the `data` and `activities` fields of - `UserInAppFeedResponse`. Args: after: The cursor to fetch entries after. From 89068157d348f7579adcd21e2514781a784f7bef Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 21:45:58 +0000 Subject: [PATCH 40/44] feat(api): api update --- .stats.yml | 4 ++-- src/knockapi/resources/users/feeds.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 87f7c8d..f7645e9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-1847c4ab086e290a4e3a698f3bc55605fa5c24cbdef663a51a8b84d260312491.yml -openapi_spec_hash: 6ecef3ce2fc44f77781c835fefe1aa82 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-051fb64fc98086ac7435196316e0aee760dce287ac9e44eed11ea243893498c9.yml +openapi_spec_hash: c3cea367a962be6faea1e8fba430ed3c config_hash: 2b42d138d85c524e65fa7e205d36cc4a diff --git a/src/knockapi/resources/users/feeds.py b/src/knockapi/resources/users/feeds.py index 2014f7f..e23d3d5 100644 --- a/src/knockapi/resources/users/feeds.py +++ b/src/knockapi/resources/users/feeds.py @@ -119,6 +119,9 @@ def list_items( along with a user token. - This endpoint’s rate limit is always scoped per-user and per-environment. This is true even for requests made without a signed user token. + - Any [attachments](/integrations/email/attachments) present in trigger data are + automatically excluded from both the `data` and `activities` fields of + `UserInAppFeedResponse`. Args: after: The cursor to fetch entries after. @@ -281,6 +284,9 @@ def list_items( along with a user token. - This endpoint’s rate limit is always scoped per-user and per-environment. This is true even for requests made without a signed user token. + - Any [attachments](/integrations/email/attachments) present in trigger data are + automatically excluded from both the `data` and `activities` fields of + `UserInAppFeedResponse`. Args: after: The cursor to fetch entries after. From 0472a35893d9d01adb3fcf1d272ebf57ee9e0fef Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:58:25 +0000 Subject: [PATCH 41/44] feat(client): add custom JSON encoder for extended type support --- src/knockapi/_base_client.py | 7 +- src/knockapi/_compat.py | 6 +- src/knockapi/_utils/_json.py | 35 ++++++++++ tests/test_utils/test_json.py | 126 ++++++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 src/knockapi/_utils/_json.py create mode 100644 tests/test_utils/test_json.py diff --git a/src/knockapi/_base_client.py b/src/knockapi/_base_client.py index ada6cca..406fc8a 100644 --- a/src/knockapi/_base_client.py +++ b/src/knockapi/_base_client.py @@ -86,6 +86,7 @@ APIConnectionError, APIResponseValidationError, ) +from ._utils._json import openapi_dumps log: logging.Logger = logging.getLogger(__name__) @@ -554,8 +555,10 @@ def _build_request( 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) diff --git a/src/knockapi/_compat.py b/src/knockapi/_compat.py index bdef67f..786ff42 100644 --- a/src/knockapi/_compat.py +++ b/src/knockapi/_compat.py @@ -139,6 +139,7 @@ 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"): return model.model_dump( @@ -148,13 +149,12 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, + by_alias=by_alias, ) 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/knockapi/_utils/_json.py b/src/knockapi/_utils/_json.py new file mode 100644 index 0000000..6058421 --- /dev/null +++ b/src/knockapi/_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/tests/test_utils/test_json.py b/tests/test_utils/test_json.py new file mode 100644 index 0000000..c46db52 --- /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 knockapi import _compat +from knockapi._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}}' From 119c5866c95ab580fa3650d83fba133a6d51aa1f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 19:29:24 +0000 Subject: [PATCH 42/44] feat(api): api update --- .stats.yml | 4 ++-- src/knockapi/types/users/guide_get_channel_response.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index f7645e9..65a6aaf 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-051fb64fc98086ac7435196316e0aee760dce287ac9e44eed11ea243893498c9.yml -openapi_spec_hash: c3cea367a962be6faea1e8fba430ed3c +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-6458a8bd2021d6d33af7b8b477bd0bdef7270dd2f78fa7891048d519f0d65b22.yml +openapi_spec_hash: 37319ec82d920729dab3ab05ff75f4fc config_hash: 2b42d138d85c524e65fa7e205d36cc4a diff --git a/src/knockapi/types/users/guide_get_channel_response.py b/src/knockapi/types/users/guide_get_channel_response.py index b0ce43c..ef9efb1 100644 --- a/src/knockapi/types/users/guide_get_channel_response.py +++ b/src/knockapi/types/users/guide_get_channel_response.py @@ -27,13 +27,16 @@ class EntryActivationURLPattern(BaseModel): pathname: Optional[str] = None """The pathname pattern to match (supports wildcards like /\\**)""" + search: Optional[str] = None + """The search query params to match""" + class EntryActivationURLRule(BaseModel): argument: Optional[str] = None """The value to compare against""" directive: Optional[str] = None - """The directive for the URL pattern ('allow' or 'block')""" + """The directive for the URL rule ('allow' or 'block')""" operator: Optional[str] = None """The comparison operator ('contains' or 'equal_to')""" From a27fbbadfa3928f36d6fa7c0b824bc9bfd9dbde0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 22:23:44 +0000 Subject: [PATCH 43/44] feat(api): api update --- .stats.yml | 4 ++-- src/knockapi/resources/users/feeds.py | 12 ++++++++++++ src/knockapi/types/users/feed_list_items_params.py | 7 +++++++ tests/api_resources/users/test_feeds.py | 2 ++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 65a6aaf..2bf5662 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 90 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-6458a8bd2021d6d33af7b8b477bd0bdef7270dd2f78fa7891048d519f0d65b22.yml -openapi_spec_hash: 37319ec82d920729dab3ab05ff75f4fc +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-4b4fbdc099ed979179d6bd7ddca62288d6ccfebc6df8cabd9fb9b99431c60670.yml +openapi_spec_hash: 3a56e00132d1b0a5a4d5968466ac374b config_hash: 2b42d138d85c524e65fa7e205d36cc4a diff --git a/src/knockapi/resources/users/feeds.py b/src/knockapi/resources/users/feeds.py index e23d3d5..1b497a2 100644 --- a/src/knockapi/resources/users/feeds.py +++ b/src/knockapi/resources/users/feeds.py @@ -89,6 +89,7 @@ def list_items( after: str | Omit = omit, archived: Literal["exclude", "include", "only"] | Omit = omit, before: str | Omit = omit, + exclude: str | Omit = omit, has_tenant: bool | Omit = omit, locale: str | Omit = omit, page_size: int | Omit = omit, @@ -130,6 +131,10 @@ def list_items( before: The cursor to fetch entries before. + exclude: Comma-separated list of field paths to exclude from the response. Use dot + notation for nested fields (e.g., `entries.archived_at`). Limited to 3 levels + deep. + has_tenant: Whether the feed items have a tenant. locale: The locale to render the feed items in. Must be in the IETF 5646 format (e.g. @@ -174,6 +179,7 @@ def list_items( "after": after, "archived": archived, "before": before, + "exclude": exclude, "has_tenant": has_tenant, "locale": locale, "page_size": page_size, @@ -254,6 +260,7 @@ def list_items( after: str | Omit = omit, archived: Literal["exclude", "include", "only"] | Omit = omit, before: str | Omit = omit, + exclude: str | Omit = omit, has_tenant: bool | Omit = omit, locale: str | Omit = omit, page_size: int | Omit = omit, @@ -295,6 +302,10 @@ def list_items( before: The cursor to fetch entries before. + exclude: Comma-separated list of field paths to exclude from the response. Use dot + notation for nested fields (e.g., `entries.archived_at`). Limited to 3 levels + deep. + has_tenant: Whether the feed items have a tenant. locale: The locale to render the feed items in. Must be in the IETF 5646 format (e.g. @@ -339,6 +350,7 @@ def list_items( "after": after, "archived": archived, "before": before, + "exclude": exclude, "has_tenant": has_tenant, "locale": locale, "page_size": page_size, diff --git a/src/knockapi/types/users/feed_list_items_params.py b/src/knockapi/types/users/feed_list_items_params.py index a6c61d7..260060d 100644 --- a/src/knockapi/types/users/feed_list_items_params.py +++ b/src/knockapi/types/users/feed_list_items_params.py @@ -19,6 +19,13 @@ class FeedListItemsParams(TypedDict, total=False): before: str """The cursor to fetch entries before.""" + exclude: str + """Comma-separated list of field paths to exclude from the response. + + Use dot notation for nested fields (e.g., `entries.archived_at`). Limited to 3 + levels deep. + """ + has_tenant: bool """Whether the feed items have a tenant.""" diff --git a/tests/api_resources/users/test_feeds.py b/tests/api_resources/users/test_feeds.py index 7f1988d..d165273 100644 --- a/tests/api_resources/users/test_feeds.py +++ b/tests/api_resources/users/test_feeds.py @@ -88,6 +88,7 @@ def test_method_list_items_with_all_params(self, client: Knock) -> None: after="after", archived="exclude", before="before", + exclude="exclude", has_tenant=True, locale="locale", page_size=0, @@ -218,6 +219,7 @@ async def test_method_list_items_with_all_params(self, async_client: AsyncKnock) after="after", archived="exclude", before="before", + exclude="exclude", has_tenant=True, locale="locale", page_size=0, From fe2cd6cc86b54a6547a9dce1e2e19272e5f61541 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 22:24:14 +0000 Subject: [PATCH 44/44] release: 1.20.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 48 +++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/knockapi/_version.py | 2 +- 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index de44c40..69eb19a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.19.0" + ".": "1.20.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8228415..ee96688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,53 @@ # Changelog +## 1.20.0 (2026-01-29) + +Full Changelog: [v1.19.0...v1.20.0](https://github.com/knocklabs/knock-python/compare/v1.19.0...v1.20.0) + +### Features + +* **api:** api update ([a27fbba](https://github.com/knocklabs/knock-python/commit/a27fbbadfa3928f36d6fa7c0b824bc9bfd9dbde0)) +* **api:** api update ([119c586](https://github.com/knocklabs/knock-python/commit/119c5866c95ab580fa3650d83fba133a6d51aa1f)) +* **api:** api update ([8906815](https://github.com/knocklabs/knock-python/commit/89068157d348f7579adcd21e2514781a784f7bef)) +* **api:** api update ([53f67f5](https://github.com/knocklabs/knock-python/commit/53f67f56d7aef82576bb80c1e26ad44c2f1a1d85)) +* **api:** api update ([de9af64](https://github.com/knocklabs/knock-python/commit/de9af6473687644be3598673a1aedd37d170b329)) +* **api:** api update ([8c88c54](https://github.com/knocklabs/knock-python/commit/8c88c545b234b0bbf876106d889c6f3ce41ef32d)) +* **api:** api update ([6fa8331](https://github.com/knocklabs/knock-python/commit/6fa83315060530d14ad296f4d725e1e544d43faf)) +* **api:** api update ([3ef2d85](https://github.com/knocklabs/knock-python/commit/3ef2d85dcb10221fb441a7c3869d19391e0504b6)) +* **api:** api update ([c50a8ee](https://github.com/knocklabs/knock-python/commit/c50a8eeadf1e424505f2c112d74f66cadaa1d2cf)) +* **api:** api update ([f4573c3](https://github.com/knocklabs/knock-python/commit/f4573c32948495b568f1201a5fb7404df4aadf51)) +* **api:** api update ([94f9a57](https://github.com/knocklabs/knock-python/commit/94f9a57672939b32bb54f5586d778e25add94904)) +* **api:** api update ([b85a638](https://github.com/knocklabs/knock-python/commit/b85a63858e283b9b898abd766a40eda69cc96889)) +* **api:** api update ([7f5b0fc](https://github.com/knocklabs/knock-python/commit/7f5b0fc648526fe857fc615a3f583ce9b4c76697)) +* **api:** api update ([79859d6](https://github.com/knocklabs/knock-python/commit/79859d6ca10acd18814ba447bf59d8289f361e74)) +* **api:** api update ([66931f9](https://github.com/knocklabs/knock-python/commit/66931f970f1e98fb2152194ba9834e914538d109)) +* **api:** api update ([c8a206a](https://github.com/knocklabs/knock-python/commit/c8a206a627961a6ece03ff8d9aeb371d73e5be04)) +* **api:** api update ([47806ce](https://github.com/knocklabs/knock-python/commit/47806ce73d09bedb21d6ded55c68a5401d50abbe)) +* **client:** add custom JSON encoder for extended type support ([0472a35](https://github.com/knocklabs/knock-python/commit/0472a35893d9d01adb3fcf1d272ebf57ee9e0fef)) +* **client:** add support for binary request streaming ([a0ff5ec](https://github.com/knocklabs/knock-python/commit/a0ff5ec2d3132a0289449df2d5d2995488f79d42)) + + +### Bug Fixes + +* ensure streams are always closed ([007d337](https://github.com/knocklabs/knock-python/commit/007d337f07c9de1883d0580209aa743384e24925)) +* **types:** allow pyright to infer TypedDict types within SequenceNotStr ([ab076a9](https://github.com/knocklabs/knock-python/commit/ab076a9225fe2b2d013603b7136c7a76ea0f3300)) +* use async_to_httpx_files in patch method ([47196d1](https://github.com/knocklabs/knock-python/commit/47196d100147ab585fdb4fb3e7e05b92b953c2ec)) + + +### Chores + +* add missing docstrings ([dad2da6](https://github.com/knocklabs/knock-python/commit/dad2da6573ee829b4ac6c44d480e7becd19a25e0)) +* **ci:** upgrade `actions/github-script` ([751818a](https://github.com/knocklabs/knock-python/commit/751818aeedc73b4a53e9ddeb9966ff5973cdb3cf)) +* **deps:** mypy 1.18.1 has a regression, pin to 1.17 ([227e51f](https://github.com/knocklabs/knock-python/commit/227e51fa9f367e8aa67f9fe9ce96c844c3925b9e)) +* **docs:** use environment variables for authentication in code snippets ([89f2835](https://github.com/knocklabs/knock-python/commit/89f283596c21f14a316593dd8542c0ae8b872afc)) +* **internal:** add `--fix` argument to lint script ([be20a97](https://github.com/knocklabs/knock-python/commit/be20a97526ee33e626576d27f817f186218facb4)) +* **internal:** add missing files argument to base client ([8737e4a](https://github.com/knocklabs/knock-python/commit/8737e4a0cd84c85d0887b2e0ce5d9acd034abe87)) +* **internal:** codegen related update ([fb3e366](https://github.com/knocklabs/knock-python/commit/fb3e366f5c969b8c9956faeccab88f0882d6a263)) +* **internal:** codegen related update ([98724ee](https://github.com/knocklabs/knock-python/commit/98724ee95eeaf2dffd5e725b212d223b9db1096a)) +* **internal:** update `actions/checkout` version ([ef96980](https://github.com/knocklabs/knock-python/commit/ef96980eff0e3a1d600df5e777cfdeabb486ce32)) +* speedup initial import ([f5c9aa6](https://github.com/knocklabs/knock-python/commit/f5c9aa6dd753849ac29c12ca41e1723ac77d1892)) +* update lockfile ([48e85d7](https://github.com/knocklabs/knock-python/commit/48e85d7d0cdd44f342abd95e1517dc4f51f1d67d)) + ## 1.19.0 (2025-11-20) Full Changelog: [v1.18.1...v1.19.0](https://github.com/knocklabs/knock-python/compare/v1.18.1...v1.19.0) diff --git a/pyproject.toml b/pyproject.toml index 230502a..d593fac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "knockapi" -version = "1.19.0" +version = "1.20.0" description = "The official Python library for the knock API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/knockapi/_version.py b/src/knockapi/_version.py index 0abf59d..2b28f91 100644 --- a/src/knockapi/_version.py +++ b/src/knockapi/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "knockapi" -__version__ = "1.19.0" # x-release-please-version +__version__ = "1.20.0" # x-release-please-version