Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "1.25.0"
".": "1.26.0"
}
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 94
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock%2Fknock-3e8f3a4664d48b3d546339018b451a356f8e20c223a2d21e7c3824fad4cddc7b.yml
openapi_spec_hash: c2b6637451a63e39c1f1042c6a7cc7f7
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/knock/knock-9574f16123ffa4f4b89e9ab4ff2f3276938d1f985c73aebfce8a74f2e07778a9.yml
openapi_spec_hash: b886eafe6bb5a3b78bb1e74b3f1ddda6
config_hash: 625db64572b7ee0ee1dd00546e53fc5f
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Changelog

## 1.26.0 (2026-05-07)

Full Changelog: [v1.25.0...v1.26.0](https://github.com/knocklabs/knock-python/compare/v1.25.0...v1.26.0)

### Features

* **api:** api update ([39e6add](https://github.com/knocklabs/knock-python/commit/39e6add4ed39335399c26b1c87968bf68709330f))
* **api:** api update ([0c514bf](https://github.com/knocklabs/knock-python/commit/0c514bf839ee8134227869b00cf52fb07deeeae6))
* **api:** api update ([c3f2f4a](https://github.com/knocklabs/knock-python/commit/c3f2f4a2e93139c627d3745ea4eb7f5142f3153b))
* support setting headers via env ([e22462b](https://github.com/knocklabs/knock-python/commit/e22462bd64af8c7ddaa8e84c76aca28e411d9734))


### Bug Fixes

* use correct field name format for multipart file arrays ([4d33037](https://github.com/knocklabs/knock-python/commit/4d33037881f97e2b3e99892b31af059ae22c4bb8))


### Chores

* **internal:** reformat pyproject.toml ([8e64806](https://github.com/knocklabs/knock-python/commit/8e6480650f67a83f923f45485ff2f0d79e719d0b))

## 1.25.0 (2026-04-23)

Full Changelog: [v1.24.1...v1.25.0](https://github.com/knocklabs/knock-python/compare/v1.24.1...v1.25.0)
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "knockapi"
version = "1.25.0"
version = "1.26.0"
description = "The official Python library for the knock API"
dynamic = ["readme"]
license = "Apache-2.0"
Expand Down Expand Up @@ -168,7 +168,7 @@ show_error_codes = true
#
# We also exclude our `tests` as mypy doesn't always infer
# types correctly and Pyright will still catch any type errors.
exclude = ['src/knockapi/_files.py', '_dev/.*.py', 'tests/.*']
exclude = ["src/knockapi/_files.py", "_dev/.*.py", "tests/.*"]

strict_equality = true
implicit_reexport = true
Expand Down
24 changes: 23 additions & 1 deletion src/knockapi/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
RequestOptions,
not_given,
)
from ._utils import is_given, get_async_library
from ._utils import (
is_given,
is_mapping_t,
get_async_library,
)
from ._compat import cached_property
from ._version import __version__
from ._streaming import Stream as Stream, AsyncStream as AsyncStream
Expand Down Expand Up @@ -113,6 +117,15 @@ def __init__(
if base_url is None:
base_url = f"https://api.knock.app"

custom_headers_env = os.environ.get("KNOCK_CUSTOM_HEADERS")
if custom_headers_env is not None:
parsed: dict[str, str] = {}
for line in custom_headers_env.split("\n"):
colon = line.find(":")
if colon >= 0:
parsed[line[:colon].strip()] = line[colon + 1 :].strip()
default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})}

super().__init__(
version=__version__,
base_url=base_url,
Expand Down Expand Up @@ -388,6 +401,15 @@ def __init__(
if base_url is None:
base_url = f"https://api.knock.app"

custom_headers_env = os.environ.get("KNOCK_CUSTOM_HEADERS")
if custom_headers_env is not None:
parsed: dict[str, str] = {}
for line in custom_headers_env.split("\n"):
colon = line.find(":")
if colon >= 0:
parsed[line[:colon].strip()] = line[colon + 1 :].strip()
default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})}

super().__init__(
version=__version__,
base_url=base_url,
Expand Down
8 changes: 2 additions & 6 deletions src/knockapi/_qs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,13 @@

from typing import Any, List, Tuple, Union, Mapping, TypeVar
from urllib.parse import parse_qs, urlencode
from typing_extensions import Literal, get_args
from typing_extensions import get_args

from ._types import NotGiven, not_given
from ._types import NotGiven, ArrayFormat, NestedFormat, not_given
from ._utils import flatten

_T = TypeVar("_T")


ArrayFormat = Literal["comma", "repeat", "indices", "brackets"]
NestedFormat = Literal["dots", "brackets"]

PrimitiveData = Union[str, int, float, bool, None]
# this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"]
# https://github.com/microsoft/pyright/issues/3555
Expand Down
3 changes: 3 additions & 0 deletions src/knockapi/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
ModelT = TypeVar("ModelT", bound=pydantic.BaseModel)
_T = TypeVar("_T")

ArrayFormat = Literal["comma", "repeat", "indices", "brackets"]
NestedFormat = Literal["dots", "brackets"]


# Approximates httpx internal ProxiesTypes and RequestFiles types
# while adding support for `PathLike` instances
Expand Down
42 changes: 34 additions & 8 deletions src/knockapi/_utils/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
)
from pathlib import Path
from datetime import date, datetime
from typing_extensions import TypeGuard
from typing_extensions import TypeGuard, get_args

import sniffio

from .._types import Omit, NotGiven, FileTypes, HeadersLike
from .._types import Omit, NotGiven, FileTypes, ArrayFormat, HeadersLike

_T = TypeVar("_T")
_TupleT = TypeVar("_TupleT", bound=Tuple[object, ...])
Expand All @@ -40,25 +40,45 @@ def extract_files(
query: Mapping[str, object],
*,
paths: Sequence[Sequence[str]],
array_format: ArrayFormat = "brackets",
) -> list[tuple[str, FileTypes]]:
"""Recursively extract files from the given dictionary based on specified paths.

A path may look like this ['foo', 'files', '<array>', 'data'].

``array_format`` controls how ``<array>`` segments contribute to the emitted
field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and
``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``).

Note: this mutates the given dictionary.
"""
files: list[tuple[str, FileTypes]] = []
for path in paths:
files.extend(_extract_items(query, path, index=0, flattened_key=None))
files.extend(_extract_items(query, path, index=0, flattened_key=None, array_format=array_format))
return files


def _array_suffix(array_format: ArrayFormat, array_index: int) -> str:
if array_format == "brackets":
return "[]"
if array_format == "indices":
return f"[{array_index}]"
if array_format == "repeat" or array_format == "comma":
# Both repeat the bare field name for each file part; there is no
# meaningful way to comma-join binary parts.
return ""
raise NotImplementedError(
f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}"
)


def _extract_items(
obj: object,
path: Sequence[str],
*,
index: int,
flattened_key: str | None,
array_format: ArrayFormat,
) -> list[tuple[str, FileTypes]]:
try:
key = path[index]
Expand All @@ -75,9 +95,11 @@ def _extract_items(

if is_list(obj):
files: list[tuple[str, FileTypes]] = []
for entry in obj:
assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "")
files.append((flattened_key + "[]", cast(FileTypes, entry)))
for array_index, entry in enumerate(obj):
suffix = _array_suffix(array_format, array_index)
emitted_key = (flattened_key + suffix) if flattened_key else suffix
assert_is_file_content(entry, key=emitted_key)
files.append((emitted_key, cast(FileTypes, entry)))
return files

assert_is_file_content(obj, key=flattened_key)
Expand Down Expand Up @@ -106,6 +128,7 @@ def _extract_items(
path,
index=index,
flattened_key=flattened_key,
array_format=array_format,
)
elif is_list(obj):
if key != "<array>":
Expand All @@ -117,9 +140,12 @@ def _extract_items(
item,
path,
index=index,
flattened_key=flattened_key + "[]" if flattened_key is not None else "[]",
flattened_key=(
(flattened_key if flattened_key is not None else "") + _array_suffix(array_format, array_index)
),
array_format=array_format,
)
for item in obj
for array_index, item in enumerate(obj)
]
)

Expand Down
2 changes: 1 addition & 1 deletion src/knockapi/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

__title__ = "knockapi"
__version__ = "1.25.0" # x-release-please-version
__version__ = "1.26.0" # x-release-please-version
40 changes: 20 additions & 20 deletions src/knockapi/resources/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ def cancel(
pair. Can optionally be provided one or more recipients to scope the request to.

Args:
cancellation_key: An optional key that is used to reference a specific workflow trigger request
when issuing a [workflow cancellation](/send-notifications/canceling-workflows)
request. Must be provided while triggering a workflow in order to enable
subsequent cancellation. Should be unique across trigger requests to avoid
unintentional cancellations.
cancellation_key: A key that is used to reference a specific workflow trigger request when issuing
a [workflow cancellation](/send-notifications/canceling-workflows) request. Must
be provided while triggering a workflow in order to enable subsequent
cancellation. Should be unique across trigger requests to avoid unintentional
cancellations.

recipients: A list of recipients to cancel the notification for. If omitted, cancels for all
recipients associated with the cancellation key.
Expand Down Expand Up @@ -143,11 +143,11 @@ def trigger(
(string), an inline user request (object), or an inline object request, which is
determined by the presence of a `collection` property.

cancellation_key: An optional key that is used to reference a specific workflow trigger request
when issuing a [workflow cancellation](/send-notifications/canceling-workflows)
request. Must be provided while triggering a workflow in order to enable
subsequent cancellation. Should be unique across trigger requests to avoid
unintentional cancellations.
cancellation_key: A key that is used to reference a specific workflow trigger request when issuing
a [workflow cancellation](/send-notifications/canceling-workflows) request. Must
be provided while triggering a workflow in order to enable subsequent
cancellation. Should be unique across trigger requests to avoid unintentional
cancellations.

data: An optional map of data to pass into the workflow execution. There is a 10MB
limit on the size of the full `data` payload. Any individual string value
Expand Down Expand Up @@ -235,11 +235,11 @@ async def cancel(
pair. Can optionally be provided one or more recipients to scope the request to.

Args:
cancellation_key: An optional key that is used to reference a specific workflow trigger request
when issuing a [workflow cancellation](/send-notifications/canceling-workflows)
request. Must be provided while triggering a workflow in order to enable
subsequent cancellation. Should be unique across trigger requests to avoid
unintentional cancellations.
cancellation_key: A key that is used to reference a specific workflow trigger request when issuing
a [workflow cancellation](/send-notifications/canceling-workflows) request. Must
be provided while triggering a workflow in order to enable subsequent
cancellation. Should be unique across trigger requests to avoid unintentional
cancellations.

recipients: A list of recipients to cancel the notification for. If omitted, cancels for all
recipients associated with the cancellation key.
Expand Down Expand Up @@ -308,11 +308,11 @@ async def trigger(
(string), an inline user request (object), or an inline object request, which is
determined by the presence of a `collection` property.

cancellation_key: An optional key that is used to reference a specific workflow trigger request
when issuing a [workflow cancellation](/send-notifications/canceling-workflows)
request. Must be provided while triggering a workflow in order to enable
subsequent cancellation. Should be unique across trigger requests to avoid
unintentional cancellations.
cancellation_key: A key that is used to reference a specific workflow trigger request when issuing
a [workflow cancellation](/send-notifications/canceling-workflows) request. Must
be provided while triggering a workflow in order to enable subsequent
cancellation. Should be unique across trigger requests to avoid unintentional
cancellations.

data: An optional map of data to pass into the workflow execution. There is a 10MB
limit on the size of the full `data` payload. Any individual string value
Expand Down
22 changes: 11 additions & 11 deletions src/knockapi/types/message_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,22 @@ class MessageEvent(BaseModel):
"""

type: Literal[
"message.archived",
"message.bounced",
"message.read",
"message.sent",
"message.seen",
"message.created",
"message.queued",
"message.delivered",
"message.bounced",
"message.undelivered",
"message.not_sent",
"message.delivery_attempted",
"message.interacted",
"message.archived",
"message.link_clicked",
"message.not_sent",
"message.queued",
"message.read",
"message.seen",
"message.sent",
"message.unarchived",
"message.undelivered",
"message.unread",
"message.interacted",
"message.unseen",
"message.unread",
"message.unarchived",
]
"""The type of event that occurred."""

Expand Down
10 changes: 5 additions & 5 deletions src/knockapi/types/workflow_cancel_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
class WorkflowCancelParams(TypedDict, total=False):
cancellation_key: Required[str]
"""
An optional key that is used to reference a specific workflow trigger request
when issuing a [workflow cancellation](/send-notifications/canceling-workflows)
request. Must be provided while triggering a workflow in order to enable
subsequent cancellation. Should be unique across trigger requests to avoid
unintentional cancellations.
A key that is used to reference a specific workflow trigger request when issuing
a [workflow cancellation](/send-notifications/canceling-workflows) request. Must
be provided while triggering a workflow in order to enable subsequent
cancellation. Should be unique across trigger requests to avoid unintentional
cancellations.
"""

recipients: Optional[SequenceNotStr[RecipientReferenceParam]]
Expand Down
8 changes: 5 additions & 3 deletions src/knockapi/types/workflow_recipient_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from pydantic import Field as FieldInfo

from .._models import BaseModel
from .recipient import Recipient
from .recipient_reference import RecipientReference

__all__ = ["WorkflowRecipientRun", "TriggerSource"]
Expand Down Expand Up @@ -74,8 +73,11 @@ class WorkflowRecipientRun(BaseModel):
single trigger.
"""

actor: Optional[Recipient] = None
"""A recipient of a notification, which is either a user or an object."""
actor: Optional[RecipientReference] = None
"""
A reference to a recipient, either a user identifier (string) or an object
reference (ID, collection).
"""

error_count: Optional[int] = None
"""The number of errors encountered during the workflow recipient run."""
Expand Down
10 changes: 5 additions & 5 deletions src/knockapi/types/workflow_trigger_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ class WorkflowTriggerParams(TypedDict, total=False):

cancellation_key: Optional[str]
"""
An optional key that is used to reference a specific workflow trigger request
when issuing a [workflow cancellation](/send-notifications/canceling-workflows)
request. Must be provided while triggering a workflow in order to enable
subsequent cancellation. Should be unique across trigger requests to avoid
unintentional cancellations.
A key that is used to reference a specific workflow trigger request when issuing
a [workflow cancellation](/send-notifications/canceling-workflows) request. Must
be provided while triggering a workflow in order to enable subsequent
cancellation. Should be unique across trigger requests to avoid unintentional
cancellations.
"""

data: Optional[Dict[str, object]]
Expand Down
Loading